一 序
之前的在整理redo log redo log用来保证事务持久性,通过undo log可以看到数据较早版本,实现MVCC,或回滚事务等功能。
二 mini transaction 简介
innodb存储引擎中的一个很重要的用来保证持久性的机制就是mini事务,在源码中用mtr(Mini-transaction)来表示,本书把它称做“物理事务”,这样叫是相对逻辑事务而言的,对于逻辑事务,做熟悉数据库的人都很清楚,它是数据库区别于文件系统的最重要特性之一,它具有四个特性ACID,用来保证数据库的完整性——要么都做修改,要么什么都没有做。物理事务从名字来看,是物理的,因为在innodb存储引擎中,只要是涉及到文件修改,文件读取等物理操作的,都离不开这个物理事务,可以说物理事务是内存与文件之间的一个桥梁。
mini transation 主要用于innodb redo log 和 undo log写入,保证两种日志的ACID特性
mini-transaction遵循以下三个协议:
The FIX Rules
Write-Ahead Log
Force-log-at-commit
The FIX Rules
修改一个页需要获得该页的x-latch
访问一个页是需要获得该页的s-latch或者x-latch
持有该页的latch直到修改或者访问该页的操作完成
Write-Ahead Log
持久化一个数据页之前,必须先将内存中相应的日志页持久化
每个页有一个LSN,每次页修改需要维护这个LSN,当一个页需要写入到持久化设备时,要求内存中小于该页LSN的日志先写入到持久化设备中
Force-log-at-commit
一个事务可以同时修改了多个页,Write-AheadLog单个数据页的一致性,无法保证事务的持久性
Force -log-at-commit要求当一个事务提交时,其产生所有的mini-transaction日志必须刷到持久设备中
这样即使在页数据刷盘的时候宕机,也可以通过日志进行redo恢复
三 源码简介
本文使用 MySQL 5.7.18 版本进行分析
mini transation 相关代码路径位于 storage/innobase/mtr/ 主要有 mtr0mtr.cc 和 mtr0log.cc 两个文件
另有部分代码在 storage/innobase/include/ 文件名以 mtr0 开头.
mini transaction 的信息保存在结构体 mtr_t 中,源码在/innobase/include/mtr0mtr.h
/** Mini-transaction handle and buffer */
struct mtr_t {
/** State variables of the mtr */
struct Impl {
/** memo stack for locks etc. */
mtr_buf_t m_memo;
/** mini-transaction log */
mtr_buf_t m_log;
/** true if mtr has made at least one buffer pool page dirty */
bool m_made_dirty;
/** true if inside ibuf changes */
bool m_inside_ibuf;
/** true if the mini-transaction modified buffer pool pages */
bool m_modifications;
/** Count of how many page initial log records have been
written to the mtr log */
ib_uint32_t m_n_log_recs;
/** specifies which operations should be logged; default
value MTR_LOG_ALL */
mtr_log_t m_log_mode;
#ifdef UNIV_DEBUG
/** Persistent user tablespace associated with the
mini-transaction, or 0 (TRX_SYS_SPACE) if none yet */
ulint m_user_space_id;
#endif /* UNIV_DEBUG */
/** User tablespace that is being modified by the
mini-transaction */
fil_space_t* m_user_space;
/** Undo tablespace that is being modified by the
mini-transaction */
fil_space_t* m_undo_space;
/** System tablespace if it is being modified by the
mini-transaction */
fil_space_t* m_sys_space;
/** State of the transaction */
mtr_state_t m_state;
/** Flush Observer */
FlushObserver* m_flush_observer;
#ifdef UNIV_DEBUG
/** For checking corruption. */
ulint m_magic_n;
#endif /* UNIV_DEBUG */
/** Owning mini-transaction */
mtr_t* m_mtr;
};
变量名 | 描述 |
---|---|
mtr_buf_t m_memo | 用于存储该mtr持有的锁类型 |
mtr_buf_t m_log | 存储redo log记录 |
bool m_made_dirty | 是否产生了至少一个脏页 |
bool m_inside_ibuf | 是否在操作change buffer |
bool m_modifications | 是否修改了buffer pool page |
ib_uint32_t m_n_log_recs | 该mtr log记录个数 |
mtr_log_t m_log_mode | Mtr的工作模式,包括四种: MTR_LOG_ALL:默认模式,记录所有会修改磁盘数据的操作;MTR_LOG_NONE:不记录redo,脏页也不放到flush list上;MTR_LOG_NO_REDO:不记录redo,但脏页放到flush list上;MTR_LOG_SHORT_INSERTS:插入记录操作REDO,在将记录从一个page拷贝到另外一个新建的page时用到,此时忽略写索引信息到redo log中。(参阅函数page_cur_insert_rec_write_log) |
fil_space_t* m_user_space | 当前mtr修改的用户表空间 |
fil_space_t* m_undo_space | 当前mtr修改的undo表空间 |
fil_space_t* m_sys_space | 当前mtr修改的系统表空间 |
mtr_state_t m_state | 包含四种状态: MTR_STATE_INIT、MTR_STATE_COMMITTING、 MTR_STATE_COMMITTED |
在修改或读一个数据文件中的数据时,一般是通过mtr来控制对对应page或者索引树的加锁,在5.7中,有以下几种锁类型(mtr_memo_type_t
):
变量名 | 描述 |
---|---|
MTR_MEMO_PAGE_S_FIX | 用于PAGE上的S锁 |
MTR_MEMO_PAGE_X_FIX | 用于PAGE上的X锁 |
MTR_MEMO_PAGE_SX_FIX | 用于PAGE上的SX锁,以上锁通过mtr_memo_push 保存到mtr中 |
MTR_MEMO_BUF_FIX | PAGE上未加读写锁,仅做buf fix |
MTR_MEMO_S_LOCK | S锁,通常用于索引锁 |
MTR_MEMO_X_LOCK | X锁,通常用于索引锁 |
MTR_MEMO_SX_LOCK | SX锁,通常用于索引锁,以上3个锁,通过mtr_s/x/sx_lock加锁,通过mtr_memo_release释放锁 |
四 一条insert语句涉及的 mini transaction
InnoDB的redo log都是通过mtr产生的,先写到mtr的cache中,然后再提交到公共buffer中,本小节以INSERT一条记录对page产生的修改为例,阐述一个mtr的典型生命周期。关于insert 的执行过程,参见之前整理的https://blog.csdn.net/bohu83/article/details/82903976。
入口函数在row_ins_clust_index_entry_low,innobase/row/row0ins.cc
开启MTR
row_ins_clust_index_entry_low(
/*==========================*/
ulint flags, /*!< in: undo logging and locking flags */
ulint mode, /*!< in: BTR_MODIFY_LEAF or BTR_MODIFY_TREE,
depending on whether we wish optimistic or
pessimistic descent down the index tree */
dict_index_t* index, /*!< in: clustered index */
ulint n_uniq, /*!< in: 0 or index->n_uniq */
dtuple_t* entry, /*!< in/out: index entry to insert */
ulint n_ext, /*!< in: number of externally stored columns */
que_thr_t* thr, /*!< in: query thread */
bool dup_chk_only)
/*!< in: if true, just do duplicate check
and return. don't execute actual insert. */
{
btr_pcur_t pcur;
btr_cur_t* cursor;
dberr_t err = DB_SUCCESS;
big_rec_t* big_rec = NULL;
mtr_t mtr;
mem_heap_t* offsets_heap = NULL;
ulint offsets_[REC_OFFS_NORMAL_SIZE];
ulint* offsets = offsets_;
rec_offs_init(offsets_);
DBUG_ENTER("row_ins_clust_index_entry_low");
ut_ad(dict_index_is_clust(index));
ut_ad(!dict_index_is_unique(index)
|| n_uniq == dict_index_get_n_unique(index));
ut_ad(!n_uniq || n_uniq == dict_index_get_n_unique(index));
ut_ad(!thr_get_trx(thr)->in_rollback);
mtr_start(&mtr);
mtr.set_named_space(index->space);
if (dict_table_is_temporary(index->table)) {
/* Disable REDO logging as the lifetime of temp-tables is
limited to server or connection lifetime and so REDO
information is not needed on restart for recovery.
Disable locking as temp-tables are local to a connection. */
ut_ad(flags & BTR_NO_LOCKING_FLAG);
ut_ad(!dict_table_is_intrinsic(index->table)
|| (flags & BTR_NO_UNDO_LOG_FLAG));
mtr.set_log_mode(MTR_LOG_NO_REDO);
}
...
mtr_start(&mtr);
mtr.set_named_space(index->space);
就是开启mtr。
mtr_start主要包括:
- 初始化mtr的各个状态变量
- 默认模式为MTR_LOG_ALL,表示记录所有的数据变更
- mtr状态设置为ACTIVE状态(MTR_STATE_ACTIVE)
- 为锁管理对象和日志管理对象初始化内存(mtr_buf_t),初始化对象链表
mtr.set_named_space 是5.7新增的逻辑,将当前修改的表空间对象fil_space_t保存下来:如果是系统表空间,则赋值给m_impl.m_sys_space, 否则赋值给m_impl.m_user_space。
在5.7里针对临时表做了优化,直接关闭redo记录: mtr.set_log_mode(MTR_LOG_NO_REDO)
定位插入位置
if (mode == BTR_MODIFY_LEAF && dict_index_is_online_ddl(index)) {
mode = BTR_MODIFY_LEAF | BTR_ALREADY_S_LATCHED;
mtr_s_lock(dict_index_get_lock(index), &mtr);
}
/* Note that we use PAGE_CUR_LE as the search mode, because then
the function will return in both low_match and up_match of the
cursor sensible values */
btr_pcur_open(index, entry, PAGE_CUR_LE, mode, &pcur, &mtr);
cursor = btr_pcur_get_btr_cur(&pcur);
cursor->thr = thr;
ut_ad(!dict_table_is_intrinsic(index->table)
|| cursor->page_cur.block->made_dirty_with_no_latch);
#ifdef UNIV_DEBUG
{
page_t* page = btr_cur_get_page(cursor);
rec_t* first_rec = page_rec_get_next(
page_get_infimum_rec(page));
ut_ad(page_rec_is_supremum(first_rec)
|| rec_n_fields_is_sane(index, first_rec, entry));
}
#endif /* UNIV_DEBUG */
...
btr_pcur_open方法,获取到这个新生成的index到底放到btr的哪个位置。这个位置,就由Cursor来标记标记。pcur是persistent cursor。因为btr是会分裂和变动的,当btr被分裂时,cursor的位置也会对应的进行变化。因此通过一层pcur的封装,将cursor的变化对外屏蔽,针对一个index,我们只需要通过一个固定的pcur去获取当前的cursor就可以了.(btr_pcur_open_low->btr_cur_search_to_nth_level)
获取到了真实的cursor后,就可以拿到对应的leaf节点,就是具体的page。就是btr_cur_get_page。
我们看看btr_cur_search_to_nth_level 对应的源码在 storage/innobase/btr/btr0cur.cc
函数的主要作用是将cursor移动到索引上待插入的位置,不展开看。
不管插入还是更新操作,都是先以乐观方式进行,因此先加索引S锁 mtr_s_lock(dict_index_get_lock(index),&mtr)
,对应mtr_t::s_lock
函数 如果以悲观方式插入记录,意味着可能产生索引分裂,在5.7之前会加索引X锁,而5.7版本则会加SX锁(但某些情况下也会退化成X锁) 加X锁: mtr_x_lock(dict_index_get_lock(index), mtr)
,对应mtr_t::x_lock
函数 加SX锁:mtr_sx_lock(dict_index_get_lock(index),mtr)
,对应mtr_t::sx_lock
函数,源码在 storage/innobase/include/mtr0mtr.ic
/**
Locks a lock in x-mode. */
void
mtr_t::x_lock(rw_lock_t* lock, const char* file, ulint line)
{
rw_lock_x_lock_inline(lock, 0, file, line);
memo_push(lock, MTR_MEMO_X_LOCK);
}
/**
Locks a lock in sx-mode. */
void
mtr_t::sx_lock(rw_lock_t* lock, const char* file, ulint line)
{
rw_lock_sx_lock_inline(lock, 0, file, line);
memo_push(lock, MTR_MEMO_SX_LOCK);
}
实际上就是加上对应的锁对象,然后将该锁的指针和类型构建的mtr_memo_slot_t对象插入到mtr.m_impl.m_memo中。
当找到预插入page对应的block,还需要加block锁,并把对应的锁类型加入到mtr:mtr_memo_push(mtr, block, fix_type)
如果对page加的是MTR_MEMO_PAGE_X_FIX或者MTR_MEMO_PAGE_SX_FIX锁,并且当前block是clean的,则将m_impl.m_made_dirty设置成true,表示即将修改一个干净的page。
如果加锁类型为MTR_MEMO_BUF_FIX,实际上是不加锁对象的,但需要判断临时表的场景,临时表page的修改不加latch,但需要将m_impl.m_made_dirty设置为true(根据block的成员m_impl.m_made_dirty来判断),这也是5.7对InnoDB临时表场景的一种优化。
同样的,根据锁类型和锁对象构建mtr_memo_slot_t加入到m_impl.m_memo中。
插入数据
先进性乐观插入,失败在执行悲观插入。
err = btr_cur_optimistic_insert(
flags, cursor,
&offsets, &offsets_heap,
entry, &insert_rec, &big_rec,
n_ext, thr, &mtr);
if (err == DB_FAIL) {
err = btr_cur_pessimistic_insert(
flags, cursor,
&offsets, &offsets_heap,
entry, &insert_rec, &big_rec,
n_ext, thr, &mtr);
}
在插入数据过程中,包含大量的redo写cache逻辑,例如更新二级索引页的max trx id、写undo log产生的redo(嵌套另外一个mtr)、修改数据页产生的日志。这里我们只讨论修改数据页产生的日志,进入函数page_cur_insert_rec_write_log:源码在innobase/page/page0cur.cc。这里不贴了。
Step 1: 调用函数mlog_open_and_write_index记录索引相关信息
Step 2: 写入记录在page上的偏移量,占两个字节
mach_write_to_2(log_ptr, page_offset(cursor_rec));
Step 3: 写入记录其它相关信息 (rec size, extra size, info bit,关于InnoDB的数据文件物理描述,参见淘宝数据库月报)
Step 4: 将插入的记录拷贝到redo文件,同时关闭mlog
memcpy(log_ptr, ins_ptr, rec_size);
mlog_close(mtr, log_ptr + rec_size);
通过上述流程,我们写入了一个类型为MLOG_COMP_REC_INSERT的日志记录。由于特定类型的记录都基于约定的格式,在崩溃恢复时也可以基于这样的约定解析出日志。
更多的redo log记录类型参见enum mlog_id_t 源码在innobase/include/mtr0types.h
在这个过程中产生的redo log都记录在mtr.m_impl.m_log中,只有显式提交mtr时,才会写到公共buffer中。
提交MTR log
当提交一个mini transaction时,需要将对数据的更改记录提交到公共buffer中,并将对应的脏页加到flush list上。
入口函数为mtr_t::commit(),当修改产生脏页或者日志记录时,调用mtr_t::Command::execute 源码在innobase/mtr/mtr0mtr.cc
/** Write the redo log record, add dirty pages to the flush list and release
the resources. */
void
mtr_t::Command::execute()
{
ut_ad(m_impl->m_log_mode != MTR_LOG_NONE);
if (const ulint len = prepare_write()) {
finish_write(len);
}
if (m_impl->m_made_dirty) {
log_flush_order_mutex_enter();
}
/* It is now safe to release the log mutex because the
flush_order mutex will ensure that we are the first one
to insert into the flush list. */
log_mutex_exit();
m_impl->m_mtr->m_commit_lsn = m_end_lsn;
release_blocks();
if (m_impl->m_made_dirty) {
log_flush_order_mutex_exit();
}
release_latches();
release_resources();
}
Step 1: mtr_t::Command::prepare_write()
主要是持有log_sys->mutex,做写入前检查
Step 2: mtr_t::Command::finish_write
将日志从mtr中拷贝到公共log buffer。
Step 3:如果本次修改产生了脏页,获取log_sys->log_flush_order_mutex,随后释放log_sys->mutex。
Step 4. 将当前Mtr修改的脏页加入到flush list上,脏页上记录的lsn为当前mtr写入的结束点lsn。基于上述加锁逻辑,能够保证flush list上的脏页总是以LSN排序。
Step 5. 释放log_sys->log_flush_order_mutex锁
Step 6. 释放当前mtr持有的锁(主要是page latch)及分配的内存,mtr完成提交。
至此 insert 语句涉及的 mini transaction 全部结束.
五 总结
上面可以看到加锁、写日志到 mlog 等操作在 mini transaction 过程中进行。解锁、把日志刷盘等操作全部在 mtr_commit 中进行,和事务类似。mini transaction 没有回滚操作, 因为只有在 mtr_commit 才将修改落盘,如果宕机,内存丢失,无需回滚;如果落盘过程中宕机,崩溃恢复时可以看出落盘过程不完整,丢弃这部分修改。
mtr_commit 主要包含以下步骤
- mlog 中日志刷盘
- 释放 mtr 持有的锁,锁信息保存在 memo 中,以栈形式保存,后加的锁先释放
- 清理 mtr 申请的内存空间,memo 和 log
- mtr—>state 设置为 MTR_COMMITTED
上面的步骤 1. 中,日志刷盘策略和 innodb_flush_log_at_trx_commit 有关
当设置该值为1时,每次事务提交都要做一次fsync,这是最安全的配置,即使宕机也不会丢失事务
当设置为2时,则在事务提交时只做write操作,只保证写到系统的page cache,因此实例crash不会丢失事务,但宕机则可能丢失事务
当设置为0时,事务提交不会触发redo写操作,而是留给后台线程每秒一次的刷盘操作,因此实例crash将最多丢失1秒钟内的事务
这篇也算是上篇 insert 执行过程的一个补充。
参考: