Mini transaction的定义和划分
Mysql中所有的事务在innodb存储引擎内部都是通过mini transaction完成的,将一个事务划分成多个mini transaction的依据是:每条语句都作为一个mini transaction来执行。
虽然,innodb将事务划分成了多个mini transaction,但是mini transaction和mysql中的事务并不一样。根据事务应该具有的ACID特性,事务是用来保证整个数据库中数据的ACID而存在的;但是mini transaction用来保证单个page的数据的一致性。事务对于数据一致性和持久性的保证在Innodb存储引擎内部是通过mini transaction来实现的。
mini transaction遵循的原则
- FIX Rules;
- Write-Ahead-Log;
- Force-log-at-commit;
1、Fix Rule
Fix Rule要求在访问或者修改一个page时,需要持有该page的latch,以此来保证并发情况下该page种数据的一致性。一般获取latch的操作称之为Fixing the page。当获得latch之后,称这个page已经Fixed。释放page的latch的操作,称为unfixing。
为了保证数据在并发情况下的一致性,当修改一个page的时候,需要持有该page的X latch;当访问一个page的时候,需要持有该page的X latch或者S latch。一个page只有当修改完成或者是访问完成之后才释放其持有的latch。
mini transaction虽然是用来保证单个page中数据的一致性,但是mini transaction可能需要修改多个page,那么该mini transaction必须持有多个page的latch,并在操作完成之后,按照获取latch相反的顺序释放latch。
在Innodb存储引擎中每个page都有一个buf_block_t对象,关于Fix Rules的变量如下所示:
struct buf_block_struct {
......
rw_block_t lock;
ulint buf_fix_count;
};
其中,变量lock实现对page的latch操作。变量buf_fix_count是一个引用计数的存在,表示有多少个操作在fix该page。当一个事务对该page进行访问或者读取时,该值自动增1。当将该page从buffer pool中刷到磁盘的时候,该变量必须为0,否则意味着还有其他事务引用该page。在Innodb存储引擎中,判断一个page是否被fix的正确操作是判断buf_fix_count是否为0。
2、Write-Ahead Log
Write-Ahead Log要求在一个page操作写入到持久化设备之前,首先必须将其在内存中的日志写入到持久化存储。实现如下:
1、每个page都有一个LSN,存储在page头部File Header的FIL_PAGE_LSN位置,每次对page进行修改的时候总是要修改该值;
2、当将一个page持久化到存储设备的时候,要求将内存中所有LSN小于该page的LSN的日志都持久化到存储设备,然后才开始将内存中的page持久化到存储设备;
3、将内存中的page写入到持久存储设备的时候,page需要fixed,以此保证page中数据的一致性;
3、Force-log-at-commit
Write-Ahead Log保证了日志总是先于内存中的数据page被刷新到磁盘,但是仅仅依靠WAL无法保证事务的持久性。为了能在数据库宕机的时候恢复已经提交的事务,应该保证已经提交的事务的日志能够及时落盘。Force-log-at-commit就是保证当事务提交的时候,对应事务的日志持久化到存储设备。那么,即使数据库发生宕机,也能根据日志恢复数据库中的数据。
Mini transaction的实现
mini transaction在Innodb中是通过mtr_struct来实现的。其对应的结构如下:
struct mtr_struct {
ulint state;
dyn_array_t memo;
dyn_array_t log;
ibool modification;
ulint n_log_recs;
ulint log_mode;
dulint start_lsn;
dulint end_lsn;
ulint magic_n;
};
state: mini transaction的状态信息,有效值为MTR_ACTIVE, MTR_COMMTING, MTR_COMMITTED;
memo: 持有的latch信息;
log: 产生的日志;
modification: 该mini transaction是否修改了数据page;
n_log_recs: 有多少page的日志被写入到变量log中,一个mini transacton可以同时修改多个page;
log_mode: MTR_LOG_ALL, MTR_LOG_NONE, MTR_LOG_SHORT_INSERTS;
start_lsn: mini transaction开始时的LSN;
end_lsn: mini transaction结束时的LSN;
magic_n: 仅在debug模式下使用;
变量memo保存着latch信息,也就是为了遵循FIX Rules规则而存在。它保存内容的数据结构为mtr_memo_slot_struct,其定义与说明下所示。
函数mtr_memo_release用于释放已经持有的latch对象,释放的顺序是后进先出。
Innodb的重做日志是物理逻辑日志,分为MLOG_SINGLE_REC和MLOG_MULTI_REC两种类型。通过在每条日志头部的type字段设置MLOG_SINGLE_REC_FLAG来标志该mini transaction操作是否只涉及一个page的修改。如果一个mini transaction需要同时维护多个page中数据的一致性,那么其在mini-transaction结束时会额外写入1个字节大小的MLOG_MULTI_REC_END信息,表示该mini-transaction产生了修改多个page的日志。当一个mini-transaction涉及到多个page的修改时,只有读到最后一个0x1F的日志,才应用前面所有的操作,否则丢弃前面所有的操作。
Mini transaction的使用
在Innodb存储引擎中,mini-transaction总是按照如下的顺序使用:
1、mtr_t mtr;
2、mtr_start(&mtr);
......
3、mtr_commit(&mtr);
函数mtr_start首先初始化mtr。函数mtr_commit按照顺序执行如下的步骤。
1、如果mtr->modified为true,调用函数log_reverse_and_write_fast或者log_write_low将mtr中保存的日志按照先进先出的顺序写入到重做日志缓冲,这个过程中需要持有锁log_sys->mutex;
2、调用函数mtr_memo_pop_all释放mtr持有的所有latch;
3、若mtr->modified为true,调用函数log_release释放步骤1所持有的log_sys->mutex;
innodb存储引擎首先修改缓冲池中的page,然后再释放log_sys->mutex锁。这是为了保证当释放log_sys->mutex时,所有的脏page都已经完成了更新。当执行函数log_checkpoint、以及将脏page插入到flush list时,page的LSN修改操作已经完成。
这里需要特别注意log_sys->mutex这个互斥量。mini-transaction写入重做日志缓冲需要持有log_sys->mutex这个互斥量,而从重做日志缓冲写入到重做日志文件时依然要持有该互斥量。因此,该互斥量成为innodb内部线程争夺的热点,或者说成为性能的瓶颈。另外,从重做日志缓冲写入到重做日志文件是缓冲写的方式,重做日志缓冲的数据首先写入到文件的页缓存中,在对文件做fsync的之前就已经释放了log_sys->mutex,因此可以实现事务的组提交。也就是一个事务在进行fsync时,其他事务就获得log_sys->mutex对象,并将事务的重做日志条目写入到重做日志缓冲中,待下一次事务提交时,可以将多个事务的重做日志一次性地写入到重做日志文件中。
当一个事务没有对page进行任何修改的时候,同样需要使用mini-transaction功能,这是因为访问数据同样需要符合FIX Rules规格,而mini-transaction包含此项功能。当进行mtr_commit的时候,由于page没有进行任何的修改,因此只对相应的page进行unfix操作。