Lab 5.4 WAL 运行机制
上一小节的Lab你已经实现了单条WAL记录Record的设计, 这一小节我们将整合Record, 完成WAL组件的设计。
1 WAL 文件设计
首先,WAL文件的内容本质上分就是Record的数组。但这里却不仅仅是对Record的简单存储,而是需要考虑WAL文件的时效性对其进行清理, 以及写入文件的方式。设计要点包括:
- 刷盘的高效性
- 我们都知道,当一个事务完成时,必须保证其对应的
WAL记录被写入磁盘,否则在系统崩溃时,事务的修改将无法恢复。因此,WAL记录的写入必须保证原子性。但保证原子性的开销是什么呢? 你需要保证你的WAL组件写入磁盘时的效率(例如设置缓冲区, 或者是异步刷盘)
- 我们都知道,当一个事务完成时,必须保证其对应的
- 过时
WAL记录的清理- 事务操作的记录都会记录到
WAL文件中进行持久化, 但其本身对数据库的操作也会随着刷盘形成SST完成真正的持久化, 此时之前的WAL记录已经不再需要, 需要被清理。因此,WAL文件需要有一个机制来清理过时的WAL记录。
- 事务操作的记录都会记录到
2 WAL 组件的设计思路
老规矩, 我们先看看WAL组件的定义:
class WAL {
public:
WAL(const std::string &log_dir, size_t buffer_size,
uint64_t max_finished_tranc_id, uint64_t clean_interval,
uint64_t file_size_limit);
~WAL();
static std::map<uint64_t, std::vector<Record>>
recover(const std::string &log_dir, uint64_t max_finished_tranc_id);
// 将记录添加到缓冲区
void log(const std::vector<Record> &records, bool force_flush = false);
// 写入 WAL 文件
void flush();
private:
void cleaner();
protected:
std::string active_log_path_;
FileObj log_file_;
size_t file_size_limit_;
std::mutex mutex_;
std::vector<Record> log_buffer_;
size_t buffer_size_;
std::thread cleaner_thread_;
uint64_t max_finished_tranc_id_;
uint64_t clean_interval_;
};
这里我们定义了WAL组件的几个关键成员变量和接口, 其设计思路为:
active_log_path_: 当前写入的WAL文件路径log_file_: 当前写入的WAL文件对象file_size_limit_:WAL文件的大小限制(选择性使用)log_buffer_:WAL记录的缓冲区(选择性使用)buffer_size_: 缓冲区的大小(选择性使用)cleaner_thread_: 清理线程(选择性使用)
这里的成员变量只是给你一些提示, 你不一定需要使用, 但这些成员函数是必须的:
WAL: 构造函数, 初始化WAL组件~WAL(): 析构函数, 关闭WAL组件, 你需要保证析构时所有WAL内容都被持久化recover: 恢复WAL文件, 返回所有未完成的WAL记录(这是下一个Lab的内容)log: 将记录添加到缓冲区或者刷入磁盘 (取决于你的策略选择性使用)flush: 强制将缓冲区中的记录刷入磁盘 (取决于你的策略选择性使用)cleaner: 清理旧数据的线程(选择性使用)
3 代码实现
src/wal/wal.cppinclude/wal/wal.h(Optional)
3.1 WAL 组件的接口实现
你只需要实现下面几个必须实现的函数, 你可以选择性地添加其他功能函数:
WAL::WAL(const std::string &log_dir, size_t buffer_size,
uint64_t max_finished_tranc_id, uint64_t clean_interval,
uint64_t file_size_limit) {
// TODO Lab 5.4 : 实现WAL的初始化流程
}
WAL::~WAL() {
// TODO Lab 5.4 : 实现WAL的清理流程
}
void WAL::log(const std::vector<Record> &records, bool force_flush) {
// TODO Lab 5.4 : 实现WAL的写入流程
}
// commit 时 强制写入
void WAL::flush() {
// TODO Lab 5.4 : 强制刷盘
// ? 取决于你的 log 实现是否使用了缓冲区或者异步的实现
}
void WAL::cleaner() {
// TODO Lab 5.4 : 实现WAL的清理线程
}
3.2 TranContext 逻辑更新
之前你实现的TranContext的put, get,remove, commit和abort等函数中, 你的实现仅仅是将操作记录记录在了operations数组中(甚至没有记录, 因为那时你可能不知道这个成员变量是做什么的)。
现在你已经实现的WAL的刷盘接口, 因此你需要更新TranContext的这些函数, 使其能够将操作记录写入WAL文件中。不过这里你需要尤其注意冲突检测的问题, 不同的策略的冲突检测实现难度大不相同
commit时统一进行冲突检测并写入WAL文件, 这种方式实现最简单, 但性能较差put,get,remove时进行就分批写入WAL文件, 这种方式实现需要你在从图检测时需要考虑WAL文件中的记录的有效性控制, 实现难度较大, 但性能较好
你在更新TranContext的put, get,remove, commit和abort等函数中, 下面这个辅助函数也许对你有用:
bool TranManager::write_to_wal(const std::vector<Record> &records) {
// TODO: Lab 5.4
return true;
}
4 测试
WAL组件的测试代码在test/lab5/test_wal.cpp中, 你需要保证你的WAL组件能够通过这些测试, 但这个测试文件编写其实非常粗糙, 因为本节Lab对你的实现方案没有做任何限制, 因此你的实现的元数据也不好测试。因此, 这个测试看看就行, 在你完成下一小节(也是本章最后一个Lab)的逻辑后, 你可以通过test _lsm的LSMTest.Recover判断你的实现是否正确。