InnoDB 的 Redo Log
2023-3-9|2023-3-16
麦兜
type
Post
status
Published
date
Mar 9, 2023
slug
summary
tags
数据库
InnoDB
category
学习思考
password
icon
mini-transaction 是内部更低级的概念,简称mtr,修改Page、新增Page等都需要写入Rodo log,用于Crash之后恢复。在全局有一个
log_sys 对象,作为redo log元配置对应的结构是log0sys.h的log_t 。log_sys 作为redo log的物理和逻辑(redo buffer)枢纽。LSN(Log Sequeue Number):日志序号,初始值
8704 按照实际写入的日志量加上占用的log block header和log block trailer来计算。SN(Sequeue Number):
log buffer 写入位置。LSN和SN之间的换算关系:
constexpr inline lsn_t log_translate_sn_to_lsn(lsn_t sn) { return (sn / LOG_BLOCK_DATA_SIZE * OS_FILE_LOG_BLOCK_SIZE + sn % LOG_BLOCK_DATA_SIZE + LOG_BLOCK_HDR_SIZE); }
日志结构

事务流程
mtr.start() mlog_*() //不同类型的日志 mtr.commit()
mini-transaction start
mini-transaction 结构
struct mtr_t { struct Impl { //事务锁 mtr_buf_t m_memo; //事务的本地日志是一个动态分配的内存空间 mtr_buf_t m_log; bool m_made_dirty; bool m_inside_ibuf; bool m_modifications; bool m_marked_nolog; size_t m_shard_index; uint32_t m_n_log_recs; mtr_state_t m_state; Flush_observer *m_flush_observer; }; }
mtr.start() 方法初始化会把 m_log 初始化 new (&m_impl.m_memo) mtr_buf_t();mini-transaction 数据插入
操作之前会获取各种锁。
//mtr_t.Impl.m_memo的结构 /** mini-transaction memo stack slot. */ struct mtr_memo_slot_t { void *object; /* 加锁的对象. */ ulint type; /* 持有的锁类型,W or R. */ };
mlog_open
首先标记 buf 数据已被修改
mtr m_impl.m_modifications = true;获取
mtr_t.Impl.m_log 的指针。有可能会扩容。mlog_write_initial_log_record_fast
向
m_log中写入type,space id,page no,并增加m_n_log_recs的数量。mach_write_compressed
根据数字的具体大小,选择从1到4个字节记录整数,写入
m_log 。mlog_close
//实际上是间接调用dyn的close() //dyn0buf.h /** Closes the buffer returned by open. @param ptr end of used space */ void close(const byte *ptr) { ut_ad(UT_LIST_GET_LEN(m_list) > 0); block_t *block = back(); m_size -= block->used(); block->close(ptr); m_size += block->used(); //统计整个dyn使用内存 }
mini-transaction commit
commit 实际上是间接调用
Command.execute 方法,首先执行prepare_write(); 做写日志的准备,如果只生成一条redo log 会把Flag 设置 MLOG_SINGLE_REC_FLAG ,如果是多条会在尾部添加 MLOG_MULTI_REC_END 标志。MLOG_MULTI_REC_END 用于多条日志原子恢复。prepare_write();这个方法会返回redo log的大小。- 如果
prepare_write();返回的的长度大于0接下来调用log_buffer_reserve()获取handle。
log_buffer_reserve() 首先提前计算插入redo buffer的start_sn 和 end_lsn 。(这是从MySQL 8.0开始,设计了一套无锁的写log机制。)/* Reserve space in sequence of data bytes: */ const sn_t start_sn = log.sn.fetch_add(len); //原子增加
算完
start_sn 和end_lsn 判断是否比buffer的limit大if (unlikely(end_sn > log.buf_limit_sn.load())) { log_wait_for_space_after_reserving(log, handle); }
等待之前日志写入回收空间
static void log_wait_for_space_in_log_buf(log_t &log, sn_t end_sn) { lsn_t lsn; Wait_stats wait_stats; //当前已经写入文件的 sn const sn_t write_sn = log_translate_lsn_to_sn(log.write_lsn.load()); log_sync_point("log_wait_for_space_in_buf_middle"); const sn_t buf_size_sn = log.buf_size_sn.load(); if (end_sn + OS_FILE_LOG_BLOCK_SIZE <= write_sn + buf_size_sn) { return; } /* We preserve this counter for backward compatibility with 5.7. */ srv_stats.log_waits.inc(); lsn = log_translate_sn_to_lsn(end_sn + OS_FILE_LOG_BLOCK_SIZE - buf_size_sn); //等待 lsn 之前的 redo log 被写入. wait_stats = log_write_up_to(log, lsn, false); MONITOR_INC_WAIT_STATS(MONITOR_LOG_ON_BUFFER_SPACE_, wait_stats); ut_a(end_sn + OS_FILE_LOG_BLOCK_SIZE <= log_translate_lsn_to_sn(log.write_lsn.load()) + buf_size_sn); }
对于整个日志长度大于当前整个
redo log buffer 的 redo log 需要Resize 设置 redo log bufferm_impl->m_log.for_each_block(write_log);按块写入bufferlog_buffer_write()调用std::memcpy(ptr,str, len)进行写入redo buffer 。log_buffer_write_completed()更新log_t中的recent_written,即(start_lsnend_lsn)组成的list。add_dirty_blocks_to_flush_list()假如产生了 redo log,则将数据页的newest_modification修改为end_lsn。- 假如该 Block 是第一次被修改,就需要插入 Buffer Pool 的
flush_list将涉及修改的数据页添加到 Buffer Pool 的flush_list(buf_flush_insert_into_flush_list()).(利用block->page.oldest_modification来判断是否为第一次修改)
对m_log中的每一个 512 字节的 Block 调用
mtr_write_log_t()(需要注意的是mtr_write_log_t()是运算符()的重载)log_flusher
通知log_flusher线程,log_flusher 线程会调
fsync 将REDO刷盘,至此完成了REDO完整的写入过程。默认情况下
innodb_flush_log_at_trx_commit = 1,线程会block 等待需要等REDO完成刷盘。innodb_flush_log_at_trx_commit = 2,只REDO只写入系统Page Cache。