为什么需要crash recovery
如果数据库是非正常模式退出(如kill -9等),这会导致的问题是磁盘上的页面状态与redo log的内容不一致,因为buffer pool中的page内容是异步刷脏至磁盘上,而这就需要我们在启动时将两者恢复到一致状态。
除了要将磁盘页面恢复到一个一致状态以外,还需要考虑到退出时的活跃事务处理:哪些事务需要提交,哪些事务需要回滚等等,这些也属于crash recovery的工作职责。
如何crash recovery
crash recovery的第一阶段是回放redo log以将磁盘上的数据页面恢复至最新状态,而回放的起始位点就是实例退出前记录的checkpoint lsn。因为checkpoint lsn保证了这之前的redo log对应的page更改一定已经被持久化。
在完成第一阶段后,接下来就是恢复逻辑状态,即处理那些未决事务。
接下来按照阶段分别描述其具体实现。
回放redo log
该阶段会首先读取checkpoint lsn,然后从此处开始扫描redo log至末尾,解析这些redo log,将它们加入至一个hash table中,然后回放这些redo log。
读取checkpoint
dberr_t
recv_recovery_from_checkpoint_start(
lsn_t flush_lsn)
{
err = recv_find_max_checkpoint(&max_cp_group, &max_cp_field);
log_group_header_read(max_cp_field);
buf = log_sys->checkpoint_buf;
checkpoint_lsn = mach_read_from_8(buf + LOG_CHECKPOINT_LSN);
checkpoint_no = mach_read_from_8(buf + LOG_CHECKPOINT_NO);
}
读取并解析redo log
static void
recv_recovery_begin(log_t &log, lsn_t *contiguous_lsn)
{
// 情况存放redo log的hash table
recv_sys_empty_hash();
...
while (!finished) {
// 批量读取redo log(64KB),然后解析redo log并放入hash table
lsn_t end_lsn = start_lsn + RECV_SCAN_SIZE;
recv_read_log_seg(log, log.buf, start_lsn, end_lsn);
finished = recv_scan_log_recs(log, max_mem, log.buf, RECV_SCAN_SIZE,
checkpoint_lsn, start_lsn, contiguous_lsn,
&log.scanned_lsn);
start_lsn = end_lsn;
}
}
而读取、解析redo log的实现全部位于函数 recv_scan_log_recs()
中,这个函数比较冗长:
bool
recv_scan_log_recs(...)
{
// 按照block扫描redo log并将其有效内容加入至parse buffer(recv_sys->buf)
do {
data_len = log_block_get_data_len(log_block);
scanned_lsn += data_len;
if (scanned_lsn > recv_sys->scanned_lsn) {
more_data = recv_sys_add_to_parsing_buf(
log_block, scanned_lsn);
recv_sys->scanned_lsn = scanned_lsn;
recv_sys->scanned_checkpoint_no
= log_block_get_checkpoint_no(log_block);
}
...
} while (log_block
// 开始解析parse buffer内的redo log
if (more_data && !recv_sys->found_corrupt_log) {
if (recv_parse_log_recs(checkpoint_lsn)) {
...
}
}
}
static void
recv_parse_log_recs(lsn_t checkpoint_lsn)
{
for (;;) {
byte *ptr = recv_sys->buf + recv_sys->recovered_offset;
byte *end_ptr = recv_sys->buf + recv_sys->len;
bool single_rec = !!(*ptr & MLOG_SINGLE_REC_FLAG);
// 分别解析single和multi类型redo log
if (single_rec) {
if (recv_single_rec(ptr, end_ptr)) {
return;
}
} else if (recv_multi_rec(ptr, end_ptr)) {
return;
}
}
}
无论是single还是multi redo log record,解析出log的type和body等信息后都会通过recv_add_to_hash_table将其插入至全局的hash table中。 recv_add_to_hash_table
的原理也比较简单,recv_sys对象内存在一个hash table,根据redo log的计算hash值并插入至hash table中即可。这里就不再罗列代码。
至此,所有需要回放的redo log都已经被添加至hash table,接下来就是回放这些redo log。
回放redo log
dberr_t
srv_start(bool create_new_db, const std::string &scan_directories)
{
...
err = recv_recovery_from_checkpoint_start(*log_sys, flushed_lsn);
...
if (srv_force_recovery recv_apply_hashed_log_recs(*log_sys, true);
}
...
}
void
recv_apply_hashed_log_recs(log_t &log, bool allow_ibuf)
{
for (const auto &space : *recv_sys->spaces) {
for (auto pages : space.second.m_pages) {
recv_apply_log_rec(pages.second);
}
}
}
回放的逻辑也比较简单,扫描之前构建的hash table内的每一个redo log,读取该redo log对应的page,然后根据redo log类型将其日志在物理页面上执行一遍,这里就不再赘述代码,详细可参考函数 recv_parse_or_apply_log_rec_body
。