熟悉InnoDB的朋友都知道,innodb的history list长度代表了有多少undo日志还没有被清理掉,可以通过show engine innodb status
命令来获得。如果发现history list的长度越大,要么就是实例的复杂非常高,要么就是可能有大查询,或者事务没提交,导致Undo log无法分析。
但如果仔细观察,大家是否发现,history list居然无法降到0,即使做一次slow shutdown也不行。因为理论上来说,如果undo日志都已经purge干净了,理论上应该能下降为0。
为了更好的理解,我们先普及几个概念。首先innodb支持多个rollback segment,每个segment包含约1024个slot。
当事务开启时,会给它指定使用哪个rollback segment,然后在真正执行操作时,分配具体的slot,通常会有两种slot:
通常如果事务内只包含一种操作类型,则只使用一个slot。但也有例外,例如insert操作,如果insert的记录在page上已经存在了,但是是无效的,那么久可以直接通过更新这条无效记录的方式来实现插入,这时候使用的是update_undo.
为什么要分成两种undo slot,而不是只用一个slot处理所有呢?这是因为在提交阶段的undo处理不同:
对于Insert undo, 有两种处理方式
那么回到最初的问题,既然undo log都加到history list了,为啥在undo purge完成后,未重置为0呢?
我们来看看如下函数
trx_purge_truncate
trx_purge_truncate_history
trx_purge_truncate_rseg_history
在函数trx_purge_truncate_rseg_history
中,有如下代码段:
if ((mach_read_from_2(seg_hdr + TRX_UNDO_STATE) == TRX_UNDO_TO_PURGE)
&& (mach_read_from_2(log_hdr + TRX_UNDO_NEXT_LOG) == 0)) {
/* We can free the whole log segment */
mutex_exit(&(rseg->mutex));
mtr_commit(&mtr);
trx_purge_free_segment(rseg, hdr_addr, n_removed_logs);
n_removed_logs = 0;
} else {
mutex_exit(&(rseg->mutex));
mtr_commit(&mtr);
}
这里做了特殊判断,只有状态为PURGE的undo log才做了free segment清理。对于cached状态的undo留在原地。个人猜测是因为这些undo log可以留作重用, 在重用之后,再做一次性清理。
为了验证猜测,修改函数trx_undo_set_state_at_finish
,使undo log状态,要么为TRX_UNDO_TO_FREE, 要么为TRX_UNDO_TO_PURGE。
在给实例加了一定的负载,再做一次slow shutdown重启后,history list length的长度果然变成了0。验证了其无法重置为0是由于cached undo导致。