Task 1:
cd build
make log_manager_test
./test/log_manager_test
Log Manager 有一个全局的实例,负责实现日志的写入和将日志 flush 到磁盘中。
日志类型有固定的多种,当一个事件发生时,程序会调用 log_manager
写入相应类型的日志。日志类型有如下:
/** The type of the log record. */
enum class LogRecordType {
INVALID = 0,
INSERT,
MARKDELETE,
APPLYDELETE,
ROLLBACKDELETE,
UPDATE,
BEGIN,
COMMIT,
ABORT,
/** Creating a new page in the table heap. */
NEWPAGE,
};
比如当 buffer pool 调用 NewPage() 时,该函数会通过 log_manager 来追加一个 NEWPAGE 类型的日志记录。
log_manager 主要负责两件事情:
AppendLogRecord()
方法来追加日志到日志缓冲区中代码主要写在 recovery/log_manager.cpp
文件中。
这里需要实现两个函数:
RunFlushThread()
:启动 Flush Thread 的运行StopFlushThread()
:停止 Flush Thread 的运行该任务的难点在于多线程代码的编写,包括线程间的通信和同步等。我们一步步来实现。
log_manager 存在两个缓冲区:log buffer
和 flush buffer
,每次追加日志时向 log buffer 追加数据,当运行 flush 时,Flush Thread 需要将两个 buffer 互换,将互换后的 flush buffer 写入磁盘。
这里涉及到 LogManager 的如下几个成员变量:
log_buffer_
:用于追加数据的 bufferflush buffer_
:Flush Thread 在往磁盘刷入数据时的 bufferlog_buffer_offset_
:log buffer 的偏移量flush_buffer_offset_
:flush buffer 的偏移量persistent_lsn_
:记录目前已写入磁盘的日志的序列号代码实现:
void LogManager::FlushOnce() {
if (log_buffer_offset_ > 0) {
// 互换 log buffer 和 flush buffer
std::swap(log_buffer_, flush_buffer_);
std::swap(log_buffer_offset_, flush_buffer_offset_);
// 将 flush buffer 写入磁盘,并重置 flush buffer offset
disk_manager_->WriteLog(flush_buffer_, flush_buffer_offset_);
flush_buffer_offset_ = 0;
// 更新 persisten_lsn,表示已写入磁盘的日志的序列号
persistent_lsn_ = next_lsn_ - 1;
}
}
我们需要写一个函数 FlushLoopTask()
,在 Flush Thread 线程创建后运行这个函数,它通过不断调用 FlushOnce()
来完成在后台定期地将日志缓冲区的数据刷入磁盘。
实验介绍中提到了需要运行 FlushOnce 的三种情况:
log_timeout
秒时因此该线程运行的逻辑应该是:不断循环,等待满足以上三种条件之一,满足后调用一次 FlushOnce()
来将日志缓冲区的数据刷入磁盘。
为了实现“等待满足条件”这个逻辑,这里的代码实现使用了 C++ 的 condition variable
补充:condition variable 的用法
_______________________________________
首先使用 std::mutex 创建一个 std::unique_lock:std::mutex mu; std::unique_lock<std::mutex> lock(mu);
然后创建一个 condition variable:
std::condition_variable cv;
接着调用 condition variable 的 wait_for 方法来等待:cv.wait_for(lock, std::chrono::seconds(log_timeout), func);
这一条 wait_for 会一直等待直到满足以下条件之一:
- 有其他线程调用了
cv.notify_all();
- 过了
log_timeout
秒- 函数
func()
会返回 true以上条件不满足时,wait_for 会释放掉锁
mu
,当条件一直满足时,wait_for() 便会返回,同时锁住mu
。
借助 condition variable,可以实现代码:
void LogManager::FlushLoopTask() {
std::unique_lock<std::mutex> lock(latch_);
while (true) {
cv_.wait_for(lock, std::chrono::seconds(log_timeout), [&] { return need_flush_.load(); });
if (!enable_logging) {
return;
}
// flush
this->FlushOnce();
need_flush_ = false;
flush_once_cv_.notify_all();
}
}
使用独占锁 latch_
来防止 flush 线程和追加日志线程同时运行导致冲突。每次完成 FlushOnce 后,都会再调用另一个条件变量 flush_once_cv_.notify_all()
来告诉别人已完成 flush。
函数 RunFlushThread()
负责开启 Flush Thread 并在后台一直运行,同时需要设置 enable_logging = true
来开启日志记录:
void LogManager::RunFlushThread() {
if (enable_logging) {
return;
}
enable_logging = true;
flush_thread_ = new std::thread(&LogManager::FlushLoopTask, this);
}
函数 StopFlushThread()
负责停止 Flush Thread,并设置 enable_logging = false
:
/*
* Stop and join the flush thread, set enable_logging = false
*/
void LogManager::StopFlushThread() {
if (!enable_logging) {
return;
}
enable_logging = false;
flush_thread_->join();
delete flush_thread_;
latch_.lock();
FlushOnce();
latch_.unlock();
}
注意这里使用 join() 来等待线程结束,同时再次调用一次 FlushOnce()
保证日志缓冲区的数据全被刷入磁盘中。
该函数是供其他组件调用,用于触发一次 flush:
void LogManager::TriggerFlush() {
need_flush_ = true;
cv_.notify_all();
}
按照要求,当 buffer pool 从 replacer 换出一个 Page 时,就需要进行一次日志的 flush,因此 buffer_pool_manager 就是通过调用这个 TriggerFlush()
来触发日志的 flush。因此需要修改 BufferPoolManager 的代码:
auto BufferPoolManager::VictimByReplacer(frame_id_t *frame_id) -> bool {
bool success = replacer_->Victim(frame_id);
if (!success) {
return false;
}
// flush log to disk when victim a dirty page
if (enable_logging) {
auto &page = pages_[*frame_id];
if (page.IsDirty() && page.GetLSN() > log_manager_->GetPersistentLSN()) {
log_manager_->TriggerFlush();
}
}
return true;
}
当换出的页是脏页,并且这个页的最新 LSN 大于已持久化的 LSN 的话,就需要进行一次 log flush,从而保证数据库的修改写入磁盘时,相应的日志也被持久化了。
该函数实现日志的追加,其他组件就是在需要记录日志时,构造一个 LogRecord 对象并传入 AppendLogRecord()
来完成日志的追加。
这个函数是将日志序列化到 log buffer 中,同时注意维护好 log_buffer_offset。
不同类型的日志具有不同的序列化格式,具体格式要求可以参考实验说明。
auto LogManager::AppendLogRecord(LogRecord *log_record) -> lsn_t {
std::unique_lock<std::mutex> lock(latch_);
// 更新 LSN
log_record->lsn_ = next_lsn_;
next_lsn_++;
// 校验 buffer 的大小,如果 buffer 满了的话,需要进行一次 log flush
if (log_buffer_offset_ + log_record->GetSize() > LOG_BUFFER_SIZE) {
need_flush_ = true;
cv_.notify_all();
flush_once_cv_.wait(lock, [&] { return log_buffer_offset_ + log_record->GetSize() <= LOG_BUFFER_SIZE; });
}
// serialize the must have fields(20 bytes in total)
memcpy(log_buffer_ + log_buffer_offset_, reinterpret_cast<char*>(log_record), LogRecord::HEADER_SIZE);
int pos = log_buffer_offset_ + LogRecord::HEADER_SIZE;
const auto log_record_type = log_record->GetLogRecordType();
if (log_record_type == LogRecordType::INSERT) {
AppendInsertLogRecord(log_record, pos);
} else if (log_record_type == LogRecordType::MARKDELETE || log_record_type == LogRecordType::APPLYDELETE ||
log_record_type == LogRecordType::ROLLBACKDELETE) {
AppendDeleteLogRecord(log_record, pos);
} else if (log_record_type == LogRecordType::UPDATE) {
AppendUpdateLogRecord(log_record, pos);
} else if (log_record_type == LogRecordType::NEWPAGE) {
AppendNewPageLogRecord(log_record, pos);
}
log_buffer_offset_ += log_record->GetSize();
return log_record->GetLSN();
}
void LogManager::AppendInsertLogRecord(LogRecord *log_record, int pos) {
memcpy(log_buffer_ + pos, &log_record->GetInsertRID(), sizeof(RID));
pos += sizeof(RID);
const Tuple &insert_tuple = log_record->GetInserteTuple();
insert_tuple.SerializeTo(log_buffer_ + pos);
}
void LogManager::AppendDeleteLogRecord(LogRecord *log_record, int pos) {
memcpy(log_buffer_ + pos, &log_record->GetDeleteRID(), sizeof(RID));
pos += sizeof(RID);
const Tuple &delete_tuple = log_record->delete_tuple_;
delete_tuple.SerializeTo(log_buffer_ + pos);
}
void LogManager::AppendUpdateLogRecord(LogRecord *log_record, int pos) {
memcpy(log_buffer_ + pos, &log_record->update_rid_, sizeof(RID));
pos += sizeof(RID);
const Tuple &old_tuple = log_record->old_tuple_;
const Tuple &new_tuple = log_record->new_tuple_;
const auto old_tuple_size = old_tuple.GetLength();
old_tuple.SerializeTo(log_buffer_ + pos);
pos += static_cast<int>(sizeof(old_tuple_size) + old_tuple_size);
new_tuple.SerializeTo(log_buffer_ + pos);
}
void LogManager::AppendNewPageLogRecord(LogRecord *log_record, int pos) {
memcpy(log_buffer_ + pos, &log_record->prev_page_id_, sizeof(page_id_t));
pos += sizeof(page_id_t);
memcpy(log_buffer_ + pos, &log_record->page_id_, sizeof(page_id_t));
}
完成 AppendLogRecord()
后,需要在几个事件发生时,通过调用该函数来记下日志。本实验中,TablePage
类的几个方法如 TablePage::InsertTuple
等都已实现日志的记录,我们需要为事务开始、提交或者终止时添加日志的记录,所需要修改的函数如下:
TransactionManager::Begin
TransactionManager::Commit
TransactionManager::Abort
实现以上代码后,可以顺利通过 log_manager_test。
本实验使用一种粗暴的方式完成从日志的系统恢复:
读取日志时,我们需要从之前序列化到磁盘的日志数据反序列化为一个个的 LogRecord 对象,因此我们需要先实现 DeserializeLogRecord()
方法:
/*
* deserialize a log record from log buffer
* @return: true means deserialize succeed, otherwise can't deserialize cause
* incomplete log record
*/
auto LogRecovery::DeserializeLogRecord(const char *data, LogRecord *log_record) -> bool {
const auto *const src_record = reinterpret_cast<const LogRecord *>(data);
if (src_record->size_ < 20 || data + src_record->size_ > log_buffer_ + LOG_BUFFER_SIZE) {
return false;
}
memcpy(reinterpret_cast<char *>(log_record), data, LogRecord::HEADER_SIZE);
int offset = LogRecord::HEADER_SIZE;
if (log_record->GetLogRecordType() == LogRecordType::INVALID) {
return false;
}
const auto log_record_type = log_record->GetLogRecordType();
if (log_record_type == LogRecordType::INSERT) {
log_record->insert_rid_ = *(reinterpret_cast<const RID *>(data + offset));
offset += sizeof(RID);
log_record->insert_tuple_.DeserializeFrom(data + offset);
} else if (log_record_type == LogRecordType::MARKDELETE || log_record_type == LogRecordType::APPLYDELETE ||
log_record_type == LogRecordType::ROLLBACKDELETE) {
log_record->delete_rid_ = *(reinterpret_cast<const RID *>(data + offset));
offset += sizeof(RID);
log_record->delete_tuple_.DeserializeFrom(data + offset);
} else if (log_record_type == LogRecordType::UPDATE) {
log_record->update_rid_ = *(reinterpret_cast<const RID *>(data + offset));
offset += sizeof(RID);
log_record->old_tuple_.DeserializeFrom(data + offset);
offset += static_cast<int>(sizeof(uint32_t) + log_record->old_tuple_.GetLength());
log_record->new_tuple_.DeserializeFrom(data + offset);
} else if (log_record_type == LogRecordType::NEWPAGE) {
log_record->prev_page_id_ = *(reinterpret_cast<const page_id_t *>(data + offset));
offset += static_cast<int>(sizeof(page_id_t));
log_record->page_id_ = *(reinterpret_cast<const page_id_t *>(data + offset));
}
return true;
}
Redo 阶段从头遍历每个日志,并根据日志的类型采取相应的重放动作。
在重放 NEWPAGE 日志时注意,崩溃前已经创建出了磁盘的页,也就是磁盘中已经分配了相应的空间,因此 Redo 重放时不需要重新在磁盘中申请分配空间,只需要对这块空间进行重新初始化就好了。由于重放 NEWPAGE 行为,我们需要得知是初始化哪个 page,而原代码没有在日志中记录这个 page_id,因此我们需要修改一下原来的代码:
void TablePage::Init(page_id_t page_id, uint32_t page_size, page_id_t prev_page_id, LogManager *log_manager,
Transaction *txn) {
// Set the page ID.
memcpy(GetData(), &page_id, sizeof(page_id));
// Log that we are creating a new page.
if (enable_logging) {
LogRecord log_record = LogRecord(txn->GetTransactionId(), txn->GetPrevLSN(), LogRecordType::NEWPAGE, prev_page_id, page_id);
lsn_t lsn = log_manager->AppendLogRecord(&log_record);
SetLSN(lsn);
txn->SetPrevLSN(lsn);
}
....
下面开始编写 Redo 的逻辑,这个过程需要维护两个数据结构:
lsn_mapping_
:将日志 LSN 映射到该日志在磁盘中的 offsetactive_txn_
:即 ATT 表,记录了数据库崩溃时仍然活跃的事务,以及这些事务的最后一条日志的 LSN代码:
void LogRecovery::Redo() {
offset_ = 0;
LogRecord record;
while (disk_manager_->ReadLog(log_buffer_, LOG_BUFFER_SIZE, offset_)) {
size_t buffer_offset = 0;
while (DeserializeLogRecord(log_buffer_ + buffer_offset, &record)) {
const auto lsn = record.GetLSN();
const auto record_size = record.GetSize();
buffer_offset += record_size;
lsn_mapping_.insert({lsn, offset_}); // lsn -> 该日志在磁盘中的 offset
offset_ += record_size;
active_txn_[record.GetTxnId()] = lsn; // 记录 ATT 表,txn_id -> 最后一条日志的 lsn
const auto record_type = record.GetLogRecordType();
switch (record_type) {
case LogRecordType::COMMIT:
case LogRecordType::ABORT:
RedoCommitOrAbortLog(record);
break;
case LogRecordType::INSERT:
RedoInsertLog(record);
break;
case LogRecordType::MARKDELETE:
RedoMarkedDeleteLog(record);
break;
case LogRecordType::APPLYDELETE:
RedoApplyDeleteLog(record);
break;
case LogRecordType::ROLLBACKDELETE:
RedoRollbackDeleteLog(record);
break;
case LogRecordType::UPDATE:
RedoUpdateLog(record);
break;
case LogRecordType::NEWPAGE:
RedoNewPageLog(record);
break;
default:
break;
}
}
}
}
Commit 和 Abort 代表事务的结束,因此需要将该事务从 ATT 表中移除:
void LogRecovery::RedoCommitOrAbortLog(LogRecord &log_record) {
active_txn_.erase(log_record.GetTxnId());
}
重新执行 insert:
void LogRecovery::RedoInsertLog(LogRecord &log_record) {
auto &rid = log_record.GetInsertRID();
const auto page_id = rid.GetPageId();
auto *page = reinterpret_cast<TablePage*>(buffer_pool_manager_->FetchPage(page_id));
bool is_dirty = false;
if (page->GetLSN() < log_record.GetLSN()) {
is_dirty = true;
page->WLatch();
page->InsertTuple(log_record.insert_tuple_, &rid, nullptr, nullptr, nullptr);
page->WUnlatch();
}
buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
void LogRecovery::RedoMarkedDeleteLog(LogRecord &log_record) {
auto &rid = log_record.GetDeleteRID();
const auto page_id = rid.GetPageId();
auto *page = buffer_pool_manager_->FetchPage(page_id);
bool is_dirty = false;
if (page->GetLSN() < log_record.GetLSN()) {
is_dirty = true;
auto *table_page = reinterpret_cast<TablePage *>(page);
table_page->WLatch();
table_page->MarkDelete(rid, nullptr, nullptr, nullptr);
table_page->WUnlatch();
}
buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
void LogRecovery::RedoApplyDeleteLog(LogRecord &log_record) {
auto &rid = log_record.GetDeleteRID();
const auto page_id = rid.GetPageId();
auto *page = buffer_pool_manager_->FetchPage(page_id);
bool is_dirty = false;
if (page->GetLSN() < log_record.GetLSN()) {
is_dirty = true;
auto *table_page = reinterpret_cast<TablePage *>(page);
table_page->WLatch();
table_page->ApplyDelete(rid, nullptr, nullptr);
table_page->WUnlatch();
}
buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
void LogRecovery::RedoRollbackDeleteLog(LogRecord &log_record) {
auto &rid = log_record.GetDeleteRID();
const auto page_id = rid.GetPageId();
auto *page = buffer_pool_manager_->FetchPage(page_id);
bool is_dirty = false;
if (page->GetLSN() < log_record.GetLSN()) {
is_dirty = true;
auto *table_page = reinterpret_cast<TablePage *>(page);
table_page->WLatch();
table_page->RollbackDelete(rid, nullptr, nullptr);
table_page->WUnlatch();
}
buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
void LogRecovery::RedoUpdateLog(LogRecord &log_record) {
auto &rid = log_record.update_rid_;
const auto page_id = rid.GetPageId();
auto *page = buffer_pool_manager_->FetchPage(page_id);
bool is_dirty = false;
if (page->GetLSN() < log_record.GetLSN()) {
is_dirty = true;
auto *table_page = reinterpret_cast<TablePage *>(page);
table_page->WLatch();
table_page->UpdateTuple(log_record.new_tuple_, &log_record.old_tuple_, rid, nullptr, nullptr, nullptr);
table_page->WUnlatch();
}
buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
void LogRecovery::RedoNewPageLog(LogRecord &log_record) {
auto *page = buffer_pool_manager_->FetchPage(log_record.page_id_);
const auto page_id = log_record.page_id_;
auto *table_page = reinterpret_cast<TablePage *>(page);
bool is_dirty = false;
if (table_page->GetLSN() < log_record.GetLSN()) {
is_dirty = true;
const auto prev_page_id = log_record.prev_page_id_;
table_page->WLatch();
table_page->Init(page_id, PAGE_SIZE, prev_page_id, nullptr, nullptr);
table_page->WUnlatch();
if (prev_page_id != INVALID_PAGE_ID) {
auto *prev_page = buffer_pool_manager_->FetchPage(prev_page_id);
auto *prev_table_page = reinterpret_cast<TablePage *>(prev_page);
prev_table_page->WLatch();
prev_table_page->SetNextPageId(page_id);
prev_table_page->WUnlatch();
buffer_pool_manager_->UnpinPage(prev_page_id, true);
}
}
buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
Undo 阶段遍历 ATT 表的每个事务,对每个事务倒序遍历其对应的所有日志,逐个进行撤销。
这里所说的”撤销“,其实就是做一个补偿操作,比如对 Insert 日志的补偿操作就是 ApplyDelete。实验说明中列出了如下需要做的补偿操作:Insert <-> ApplyDelete, MarkDelete <-> RollBackDelete, Update <-> Update, NewPage <-> (do nothing)
void LogRecovery::Undo() {
LogRecord log_record;
for (const auto& item: active_txn_) {
auto lsn = item.second;
while (lsn != INVALID_LSN) {
const auto offset = lsn_mapping_[lsn];
disk_manager_->ReadLog(log_buffer_, LOG_BUFFER_SIZE, offset);
DeserializeLogRecord(log_buffer_, &log_record);
const auto log_record_type = log_record.log_record_type_;
if (log_record_type == LogRecordType::BEGIN) {
break;
}
switch (log_record_type) {
case LogRecordType::INSERT:
UndoInsertLog(log_record);
break;
case LogRecordType::MARKDELETE:
UndoMarkDeleteLog(log_record);
break;
case LogRecordType::UPDATE:
UndoUpdateLog(log_record);
break;
case LogRecordType::NEWPAGE:
UndoNewPageLog(log_record);
break;
default:
break;
}
lsn = log_record.GetPrevLSN();
}
}
}
void LogRecovery::UndoInsertLog(LogRecord &log_record) {
const auto &rid = log_record.GetInsertRID();
const auto page_id = rid.GetPageId();
auto *page = reinterpret_cast<TablePage*>(buffer_pool_manager_->FetchPage(page_id));
page->WLatch();
page->ApplyDelete(rid, nullptr, nullptr);
page->WUnlatch();
buffer_pool_manager_->UnpinPage(page_id, true);
}
void LogRecovery::UndoMarkDeleteLog(LogRecord &log_record) {
const auto &rid = log_record.GetDeleteRID();
const auto page_id = rid.GetPageId();
auto *page = reinterpret_cast<TablePage*>(buffer_pool_manager_->FetchPage(page_id));
page->WLatch();
page->RollbackDelete(rid, nullptr, nullptr);
page->WUnlatch();
buffer_pool_manager_->UnpinPage(page_id, true);
}
void LogRecovery::UndoUpdateLog(LogRecord &log_record) {
const auto &rid = log_record.update_rid_;
const auto page_id = rid.GetPageId();
auto *page = reinterpret_cast<TablePage*>(buffer_pool_manager_->FetchPage(page_id));
page->WLatch();
page->UpdateTuple(log_record.old_tuple_, &log_record.new_tuple_, rid, nullptr, nullptr, nullptr);
page->WUnlatch();
buffer_pool_manager_->UnpinPage(page_id, true);
}
void LogRecovery::UndoNewPageLog(LogRecord &log_record) {}
完成以上代码后 pass 所有关于 Redo 和 Undo 的测试。
这个 task 很简单,代码量也很小。本实验的思路是,做快照时阻塞住所有的事务,将所有日志、buffer pool 的数据全部刷入磁盘中,快照结束后再恢复事务的继续执行。
void CheckpointManager::BeginCheckpoint() {
// Block all the transactions and ensure that both the WAL and all dirty buffer pool pages are persisted to disk,
// creating a consistent checkpoint. Do NOT allow transactions to resume at the end of this method, resume them
// in CheckpointManager::EndCheckpoint() instead. This is for grading purposes.
transaction_manager_->BlockAllTransactions();
log_manager_->StopFlushThread();
buffer_pool_manager_->FlushAllPages();
}
void CheckpointManager::EndCheckpoint() {
// Allow transactions to resume, completing the checkpoint.
log_manager_->RunFlushThread();
transaction_manager_->ResumeTransactions();
}
测试:
至此,算是通关了 CMU 15445 的 Fall 2019 的全部实验了~