Redo Log

MySQL版本: 8.0

Redo log用来记录每次数据操作,用于crash之后做恢复的操作,而每一条Redo Log都是由mini transaction原子提交的.

Redo Log的数据结构

这并不是单个Redo Log的数据结构,而是管理Redo Log元信息,Redo Log Buffer等操作的系统单元.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct alignas(INNOBASE_CACHE_LINE_SIZE) log_t {
atomic_sn_t sn; // 目前log buffer申请的空间大小
aligned_array_pointer<byte, OS_FILE_LOG_BLOCK_SIZE> buf; // Log Buffer的内存区
Link_buf<lsn_t> recent_written; // 解决并发插入Redo Log Buffer后刷入ib_logfile存在空洞的问题
Link_buf<lsn_t> recent_closed; // 解决并发插入flush_list后确认checkpoint_lsn的问题
atomic_lsn_t write_lsn; // write_lsn之前的数据已经写入系统的Cache, 但不保证已经Flush
atomic_lsn_t flushed_to_disk_lsn; // 已经被flush到磁盘的数据
size_t buf_size; // log buffer缓冲区的大小

lsn_t available_for_checkpoint_lsn; // 在此lsn之前的所有被添加到buffer pool的flush list的log数据已经被flsuh, 下一次checkpoint可以make在这个lsn. 与last_checkpoint_lsn的区别是该lsn尚未被真正的checkpoint.

lsn_t requested_checkpoint_lsn; // 下次需要进行checkpoint的lsn

atomic_lsn_t last_checkpoint_lsn; // 目前最新的checkpoint的lsn
}

Mini transaction

Mini transaction具体流程

1
2
3
4
5
6
mtr_t mtr
mtr.start()
...
// 写入数据至mini transaction的m_log
...
mtr.commit()

Mini transaction的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
struct mtr_t {
// mtr_t 内嵌一个结构体Impl
struct Impl {
mtr_buf_t m_memo; // 一个被加锁的对象以及它加的锁
mtr_buf_t m_log; // mini transaction的log
bool m_made_dirty; // 是否修改了Buffer Pool中的页为脏页
bool m_modifications; // 是否修改了Buffer Pool中的页
ib_uint32_t m_n_log_recs; // 该mini transaction包含多少条log.
mtr_log_t m_log_mode; // mini transaction的操作类型(MTR_LOG_ALL, MTR_LOG_NO_REDO,MTR_LOG_NONE)
mtr_state_t m_state; // mini transaction的状态: MTR_STATE_INIT, MTR_STATE_ACTIVE, MTR_STATE_COMMITTING, MTR_STATE_COMMITTED
}
}

其中m_memo中元素是mtr_memo_slot_t, 记录加锁的对象和加锁的类型.

1
2
3
4
5
6
/** Mini-transaction memo stack slot. */
struct mtr_memo_slot_t {
void *object; // 加锁的对象

ulint type; // 持有的锁类型,W or R
};

Mini transaction的Start()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** Start a mini-transaction.
@param sync true if it is a synchronous mini-transaction
@param read_only true if read only mini-transaction */
void mtr_t::start(bool sync, bool read_only) {
UNIV_MEM_INVALID(this, sizeof(*this));

UNIV_MEM_INVALID(&m_impl, sizeof(m_impl));

m_sync = sync;

m_commit_lsn = 0;

new (&m_impl.m_log) mtr_buf_t(); // 分配记录redo log的buffer空间
new (&m_impl.m_memo) mtr_buf_t();

// 初始化字段
m_impl.m_mtr = this;
m_impl.m_log_mode = MTR_LOG_ALL;
m_impl.m_inside_ibuf = false;
m_impl.m_modifications = false;
m_impl.m_made_dirty = false;
m_impl.m_n_log_recs = 0;
m_impl.m_state = MTR_STATE_ACTIVE;
m_impl.m_flush_observer = NULL;

ut_d(m_impl.m_magic_n = MTR_MAGIC_N);
}

不同的Mini transaction如何互斥?

在操作数据前,会根据锁类型,加不同类型的锁,之后将object和锁类型存入m_memo:

1
mtr_memo_push(mtr, object, type);

Commit完成之后调用release_latches(RELEASE_ALL)将数据上的锁释放.

Mini transaction插入数据

  • byte *mlog_open(mtr_t *mtr, ulint size): 打开mtrm_log
  • mlog_write_initial_log_record_low()函数向m_log中写入typespace idpage no,并增加m_n_log_recs的数量
  • mtr->get_log()->push()按不同的类型写数据
  • mlog_close(): 更新m_log中的位置

Mini transaction的Commit过程

Commit过程将mini transaction的m_log数据拷贝到Redo Log Buffer中. 将m_state设置为MTR_STATE_COMMITTING后,调用mtr_t::Command::execute():

mtr_t::Command::execute()

  • prepare_write(): 根据mtr的类型m_impl->m_log_mode, 计算redo log的长度. 假如Log记录数目n_recs为1时,设置Flag为MLOG_SINGLE_REC_FLAG, Log记录不止一条时,Flag置为MLOG_MULTI_REC_END.

  • 假如Redo Log的长度不为0时:

    • log_buffer_reserve():

      • 自增Redo Log Buffer中的snsn代表目前Redo Log Buffer已经预留的空间,由sn_lock锁保护.

        sn是一个全局维护的递增LSN编号.

      • 获得handler,计算写入Redo Log的start_lsnend_lsn,即实际写入的数据大小, lsnsn的关系其实就是LOG_BLOCK_DATA_SIZEOS_FILE_LOG_BLOCK_SIZE(512字节)是否带LOG_BLOCK_HDR_SIZELOG_BLOCK_TRL_SIZE的转换

      • 假如需要扩展Redo Log Buffer的空间长度, 即end_lsn大于sn_limit_for_end. log_wait_for_space_after_reserving会进行扩展以及一系列的参数检查.

        • 因为Redo Log Buffer是环形的,假如写的长度需要回环,所以需要log_wait_for_space_in_log_buf(log, start_sn)等待start_sn之前的Redo Log已经被写入.

          1
          2
          3
          4
          // end_sn + OS_FILE_LOG_BLOCK_SIZE - buf_size_sn是回环的sn编号
          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);

          这里可能会和Redo Log Buffer允许空洞产生歧义,需要注意的是Redo Log Buffer允许的空洞是write_lsn之后的Redo Log Buffer允许空洞,现在的情况是因为一条Redo Log的长度超过了Redo Log Buffer的剩余长度需要回环,所以在此之前的Redo Log必须保证写入完成.

        • log_write_up_to()需要wait在log_t中的write_events. 当log.write_lsn.load() >= lsn, 即对应于Redo Log Buffer中的slot空间已经完成了写入则被唤醒.

        • log_buffer_resize_low会Resize设置Redo Log Buffer的长度, 释放旧长度的Redo Log Buffer空间,重新分配新长度的Redo Log Buffer空间,并且重新拷贝Redo Log内容.

    • 对m_log中的每一个512字节的block调用mtr_write_log_t()(需要注意的是mtr_write_log_t()是运算符()的重载)

      • log_buffer_write()使用memcpy()写Redo Log Buffer.
      • log_buffer_write_completed()更新log_t中的recent_written,即(start_lsn, end_lsn)组成的list.
    • 调用add_dirty_blocks_to_flush_list().

  • log_wait_for_space_in_log_recent_closed()查看recent_closed链表是否符合规则的位置留给该条Redo Log.

  • 假如Redo Log的长度为0时:

    • 直接调用add_dirty_blocks_to_flush_list().
  • add_dirty_blocks_to_flush_list():

    • 假如产生了Redo Log,则将Block的newest_modification修改为end_lsn.
    • 假如该Block是第一次被修改,就需要插入Buffer Pool的flush_list. 将涉及修改的Block添加到Buffer Pool的flush_list(buf_flush_insert_into_flush_list()).(利用block->page.oldest_modification来判断是否为第一次修改)
  • log_buffer_close(): 更新log_t中的recent_closed链表.

  • release_resources()释放资源, 将m_state置为MTR_STATE_COMMITTED

Redo Log Buffer

Redo Log Buffer是一段内存区域用来存放需要写入ib_logfile的数据. Redo Log Buffer的大小buf_size可以通过innodb_log_buffer_size来控制, 默认16MB.

Redo Log Buffer的Resize过程

redo_log_buffer_resize
Redo Log Buffer就是我们通常所说的回环Buffer, 而是在Resize的过程中log_buffer_resize_low().

Redo Log模块的线程

  • log_writer(): 完成Redo Log Buffer的写入, 即写入ib_logfile. (log/log0write.cc)

    • wait在log.writer_event, 直到log.write_lsn.load() < log.recent_written.tail()或者线程退出.

    • 当有数据需要写入的时候即log.write_lsn.load() <log.recent_written.tail(),调用log_writer_write_buffer()来计算Redo写入Redo Log Buffer的offset等等.

    • 真正的写入在log_files_write_buffer(),该函数会计算写入的数据在文件内即ib_logfile中的物理偏移位置,假如需要切换文件,需要设定set_next_file().
    • write_blocks来调用fil_redo_io()来完成文件写入(写入操作系统的Page Cache)
    • 更新log.write_lsn.
    • 调用notify_about_advanced_write_lsn()唤醒对应slot正在wait的线程, 这里其实就是mini transaction的Commit阶段写入Redo Log Buffer中,需要等待log.write_lsn.load() >= lsn的部分.
    • 唤醒Redo Log的Flush线程(os_event_set(log.flusher_event))

    redo_log_buffer

    上图表示Redo Log Buffer,这里需要考虑的是log.recent_written.tail()也是由log_writer()线程来更新的,因为mtr的Commit过程根据lsn计算拷贝至Redo Log Buffer的位置,这里是允许空洞的,所以为了能Flush至文件时能Batch无空洞写入,这里由log.recent_writtentail来保证之前tail之前的Buffer是不存在空洞的.

  • log_flusher(): 将Redo Log Buffer中的日志进行Flush, 这里无关数据脏页的Flush,数据脏页的Flush由Buffer Pool刷脏线程处理.

    • log_fluser()根据srv_flush_log_at_trx_commit来选择不同的wait方式:

      • 假如srv_flush_log_at_trx_commit=1

        1
        os_event_wait_time_low(log.flusher_event, flush_every_us - time_elapsed_us, 0);
      • 否则:

        1
        const auto wait_stats = os_event_wait_for(log.flusher_event, max_spins, srv_log_flusher_timeout, stop_condition);
    • 假如last_flush_lsn < log.write_lsn.load(),即需要进行刷盘.

    • fil_system->flush_file_redo()进行文件刷盘.
    • 更新log.flushed_to_disk_lsn.
    • 唤醒wait在该slot[last_flush_lsn, flush_up_to_lsn]的用户线程.
  • log_closed(): 更新log.recent_closedtail.

  • log_checkpointer(): 用来进行checkpoint的线程.

    • 更新available_for_checkpoint_lsn, 即目前可以安全进行checkpoint的lsn.

      • 扫描所有Buffer Pool的flush_list,获取最旧的一条Redo Log的lsn(bpage = UT_LIST_GET_LAST(buf_pool->flush_list). 这里最旧的lsn并不代表lsn是最小的,因为

        插入flush_list是允许并发插入的,所以无法保证flush_list中的Redo Log按照lsn的顺序排列.

      • flush_list中最旧的lsn减去recent_closed的长度,然后与上次checkpoint的lsn进行比较, 选较大的lsn_t lwm_lsn = (std::max(checkpoint_lsn, lsn - lag)).

      • recent_closed.tail比较(const lsn_t dpa_lsn = log.recent_written.tail()),选较小的(lwm_lsn = std::min(lwm_lsn, dpa_lsn).

      • log.flushed_to_disk_lsn比较,选较小的(std::min(lwm_lsn, flushed_lsn)).

      • 更新log.available_for_checkpoint_lsn.

    • 计算current_lsn, 与log.available_for_checkpoint_lsn比较,假如在此期间又有脏页被刷入flush list, 则一并进行预Flush. 之后更新log.available_for_checkpoint_lsn.

    • 检查是否需要checkpoint

    • log_checkpoint(log)进行checkpoint, 其中就是调用接口将checkpoint的信息写入指定的文件.

recent_written与recent_closed的作用

recent_written

  • MySQL 8.0通过直接计算每一条Redo在Redo Log Buffer的offset来并发插入Redo Log Buffer, 这里是允许Redo Log Buffer存在空洞的,而写入ib_logfile不允许,所以利用recent_written.tail来保证在此之前的Redo Log Buffer是不存在空洞的,从而完成ib_logfile的完整写入.

recent_closed

  • MySQL 8.0 允许并发插入flush_list, 为了能安全的进行checkpoint,需要选择一个已经被Flush的lsn,所以选择所有Buffer Pool的flush_list中最旧的一个lsn, 减去recent_closed的长度,可以确认是一个安全的checkpoint_lsn.

Redo Log中Record的格式

MLOG_REC_INSERT的Record格式

insert_record_format