在标题二中我们提出这样的一个调用栈:
innobase_commit_low(trx)
-> trx_commit_for_mysql()
-> trx_commit(trx)
-> trx_commit_low()
-> trx_commit_in_memory()
-> lock_trx_release_locks()
在lock_trx_release_locks()这个函数中执行如下重要代码,所幸的是,这段代码中有段很重要的注释,帮助我们回答了本标题提出的问题。
/*********************************************************************//**
Releases a transaction's locks, and releases possible other transactions waiting because of these locks. Change the state of the transaction to TRX_STATE_COMMITTED_IN_MEMORY. */ //这段注释表明了lock_trx_release_locks()函数的功能
void
lock_trx_release_locks(
/*===================*/
trx_t* trx) /*!< in/out: transaction */
{...
/* The following assignment makes the transaction committed in memory //这段注释很重要,需要重点理解
and makes its changes to data visible to other transactions. //在内存里提交后,本事务的数据即对其他事务可见
NOTE that there is a small discrepancy from the strict formal //存在的一个问题:违反了WAL预写日志的机制
visibility rules here: a human user of the database can see //注释在说:即使违反了WAL预写日志的机制,InnoDB也能保证正确性
modifications made by another transaction T even before the necessary
log segment has been flushed to the disk. If the database happens to
crash before the flush, the user has seen modifications from T which //在日志被刷出前,恰巧数据库引擎崩溃,而事务T被标识已经提交
will never be a committed transaction. However, any transaction T2 //即使事务T2看到了事务T崩溃前且还没有刷出的数据,事务T2要想使
which sees the modifications of the committing transaction T, and //自己的修改生效,T2需要获取一个比事务T的LSN更大的一个LSN
which also itself makes modifications to the database, will get an lsn //当系统恢复的时候,事务T因为没有预写日志而被回滚,而事务T2也只能回滚(暗含之意是LSN会被用于识别并发事务的提交顺序)
larger than the committing transaction T. In the case where the log //刷出日志时需要使用LSN判断合法性
flush fails, and T never gets committed, also T2 will never get
committed. */
/*--------------------------------------*/
trx->state = TRX_STATE_COMMITTED_IN_MEMORY; //此状态一旦设置,则本事务修改的数据则可以被其他事务所见(此时日志还没有被刷出到外存)
...
lock_release(trx); //释放事务锁(事务状态已经被设置,表明提交已经完成,本事务的数据可以被其他事务所见到,所以可以释放锁,这就是SS2PL中提交点应该在何时设置的技术本质)
...
}
这段代码的注释表明,InnoDB知道自己事务提交的规则“可能”不符合预写日志的规则也知道这样做带来的问题(可反复阅读上面的注释和解读),所以也提供了相应的解决问题的方式。解决方式如下面的代码调用栈:
innobase_commit()
{
innobase_commit_low(trx)
{
-> trx_commit_for_mysql()
-> trx_commit(trx)
-> trx_commit_low()
-> trx_commit_in_memory()
{
-> lock_trx_release_locks()
{
trx->state = TRX_STATE_COMMITTED_IN_MEMORY; //内存中设置事务提交的标志,本事务的数据即刻被其他事务可见
... //省略一些代码
lock_release(trx); //在设置事务提交已经完成的标志后才释放锁。锁在设置提交标志后才释放,符合SS2PL协议
}
...
lsn_t lsn = mtr->commit_lsn();//获得最新的LSN
if (lsn == 0) {
/* Nothing to be done. */
} else if (trx->flush_log_later) {
/* Do nothing yet */
trx->must_flush_log_later = true;
} else if (srv_flush_log_at_trx_commit == 0 //innodb_flush_log_at_trx_commit参数值为0,则不进行“预写日志”
|| thd_requested_durability(trx->mysql_thd)
== HA_IGNORE_DURABILITY) {
/* Do nothing */
} else { //否则,innodb_flush_log_at_trx_commit参数值为1,一定进行“预写日志”
trx_flush_log_if_needed(lsn, trx); //第一次写日志的机会,此时写日志,则符合预写日志机制
}
trx->commit_lsn = lsn; //把LSN赋值到事务结构体,待下面执行trx_commit_complete_for_mysql(trx)时再刷出日志
}
}
...
if (!read_only) {
trx_commit_complete_for_mysql(trx); //重要的步骤:刷出日志(刷出日志的过程可参见下一节“日志落盘”中的标题二)
{ //第二次写日志的机会,此时写日志,则不符合预写日志机制
trx_flush_log_if_needed(lsn, trx) -> trx_flush_log_if_needed_low() -> log_write_up_to(lsn, flush);
}
}
}
从前面的分析可以看出,InnoDB是先设置了事务完成的状态,然后才刷出日志(第一次和第二次刷出日志的机会均在设置事务完成的状态之后),所以我们说InnoDB不符合预写日志机制。
下面请看预写日志(WAL)的定义[1]:
In computer science, write-ahead logging (WAL) is a family of techniques for providing atomicity and durability (two of the ACID properties) in database systems.
In a system using WAL, all modifications are written to a log before they are applied. Usually both redo and undo information is stored in the log.
上面第二句话是说,在被修改的数据被应用之前日志要刷出,被修改的数据被应用即是数据被其他事务可见(注意不应理解为被修改的数据被刷出到外存),可见对应的就是事务状态被设置为已经提交。所以我们才说InnoDB不符合预写日志机制。