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 buffer
m_impl->m_log.for_each_block(write_log);
按块写入buffer
log_buffer_write()
调用std::memcpy(ptr,
str
, len)
进行写入redo buffer 。log_buffer_write_completed()
更新log_t
中的recent_written
,即(start_lsn
end_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。