没错,update 产生的 undolog 不会马上删除,因为可能有别的事务需要访问之前的版本,所以不能删。这样就串成了一个版本链,可以看到记录本身加上两条 undolog,这条 id 为 1 的记录共有三个版本。
版本链搞清楚了,这时候还需要知道一个概念 readView,这个 readView 就是用来判断哪个版本对当前事务可见的,这里有四个概念:
- creator_trx_id,当前事务ID。
- m_ids,生成 readView 时还活跃的事务ID集合,也就是已经启动但是还未提交的事务ID列表。
- min_trx_id,当前活跃ID之中的最小值。
- max_trx_id,生成 readView 时 InnoDB 将分配给下一个事务的 ID 的值(事务 ID 是递增分配的,越后面申请的事务ID越大)
对于可见版本的判断是从最新版本开始沿着版本链逐渐寻找老的版本,如果遇到符合条件的版本就返回。
判断条件如下:
- 如果当前数据版本的 trx_id == creator_trx_id 说明修改这条数据的事务就是当前事务,所以可见。
- 如果当前数据版本的 trx_id < min_trx_id,说明修改这条数据的事务在当前事务生成 readView 的时候已提交,所以可见。
- 如果当前数据版本的 trx_id 大小在 min_trx_id 和 max_trx_id 之间,此时 trx_id 若在 m_ids 中,说明修改这条数据的事务此时还未提交,所以不可见,若不在 m_ids 中,表明事务已经提交,可见。
- 如果当前数据版本的 trx_id >= max_trx_id,说明修改这条数据的事务在当前事务生成 readView 的时候还未启动,所以不可见(结合事务ID递增来看)。
来看一个简单的案例,练一练上面的规则。
读已提交隔离级别下的MVCC
现在的隔离级别是读已提交。
假设此时上文的事务1已经提交,事务 5 已经执行,但还未提交,此时有另一个事务在执行update YY where id 2
,也未提交,它的事务 ID 为 6,且也是现在最大的事务 ID。
现在有一个查询开启了事务,语句为select name where id 1
,那么这个查询语句:
- 此时 creator_trx_id 为 0,因为一个事务只有当有修改操作的时候才会被分配事务 ID。
- 此时 m_ids 为 [5,6],这两个事务都未提交,为活跃的。
- 此时 min_trx_id,为 5。
- 此时 max_trx_id,为 7,因为最新分配的事务 ID 为 6,那么下一个就是7,事务 ID 是递增分配的。
由于查询的是 ID 为 1 的记录,所以先找到 ID 为 1 的这条记录,此时的版本如下:
此时最新版本的记录上 trx_id 为 5,不比 min_trx_id 小,在 m_ids 之中,表明还是活跃的,未提交,所以不可访问,根据 roll_pointer 找到上一个版本。
于是找到了图上的那条 undolog,这条log上面记录的 trx_id 为 1,比 min_trx_id 还小,说明在生成 readView 的时候已经提交,所以可以访问,因此返回结果 name 为 XX。
然后事务 5 提交。
此时再次查询select name where id 1
,这时候又会生成新的 readView。
- 此时 creator_trx_id 为 0,因为还是没有修改操作。
- 此时 m_ids 为 [6],因为事务5提交了。
- 此时 min_trx_id,为 6。
- 此时 max_trx_id,为 7,此时没有新的事务申请。
同样还是查询的是 ID 为 1 的记录,所以还是先找到 ID 为 1 的这条记录,此时的版本如下(和上面一样,没变):
此时最新版本的记录上 trx_id 为 5,比 min_trx_id 小,说明事务已经提交了,是可以访问的,因此返回结果 name 为 NO。
这就是读已提交的 MVCC 操作,可以看到一个事务中的两次查询得到了不同的结果,所以也叫不可重复读。
可重复读隔离级别下的MVCC
现在的隔离级别是可重复读。
可重复读和读已提交的 MVCC 判断版本的过程是一模一样的,唯一的差别在生成 readView 上。
上面的读已提交每次查询都会重新生成一个新的 readView ,而可重复读在第一次生成 readView 之后的所有查询都共用同一个 readView 。
也就是说可重复读只会在第一次 select 时候生成一个 readView ,所以一个事务里面不论有几次 select ,其实看到的都是同一个 readView 。
套用上面的情况,差别就在第二次执行select name where id 1
,不会生成新的 readView,而是用之前的 readView,所以第二次查询时:
- m_ids 还是为 [5,6],虽说事务 5 此时已经提交了,但是这个readView是在事务5提交之前生成的,所以当前还是认为这两个事务都未提交,为活跃的。
- 此时 min_trx_id,为 5。
(对于判断过程有点卡顿的同学可以再拉上去看看,判断版本的过程和读已提交一致)。
所以在可重复级别下,两次查询得到的 name 都为 XX,所以叫可重复读。
说完之后,我对面试官挑了挑眉。
面试官瞥了我一眼:可以,那按你这么说其实 undolog 算是热点资源,多个事务不就会争抢 undolog 了吗?
我:对呀,所以为了提高 undolog 的写入性能,每个事务都有属于自己的 undolog 页面链表,这样就提高了写入并发度啦,再细一点就是 insert 类型的 undolog 和 update 类型的 undolog 属于不同的链表。
面试官:还能细吗?
我:再细一点就是普通表和临时表各有一条 insert 类型的 undolog 和 update 类型的 undolog ,所以最多一个事务可以有四条 undolog 页面链表,之所以分普通表和临时表是因为普通表的 undolog 写入是需要记录到redolog 中的需要保证崩溃恢复,而临时表则不需要记录,反正就是临时的。
面试官:对了,你上面说 insert 和 update ,那 delete 呢?
我:delete 其实是属于 update 的,不过分了好几种情况,反正 delete 只会给记录上打个标记,表明这条记录被删除了,不会马上删除这条记录,因为记录还得存着给别的事务作为版本链访问呢。
面试官:那这条被删除的记录就永远存在了?
我:不会的,后台有一个 purge 线程,如果探测出当前没有事务会访问这个记录了,就会把它真正的删除。
面试官:你这么细,应该没有女朋友的吧?
我:(???,不对,面试官应该没有人身攻击我,只是说我天天刻苦学习,没时间找女朋友,但是我还是有点不爽)没呢,面试官您头发这么多,应该也还没找到吧?
“我在仰望,月亮之上....”,此时面试官手机响起。
面试官:“喂,亲爱的,来了来了,马上下班了,待会老地方见哈。”那啥我有点事,你先回去吧。
我:(???小丑竟是我自己)好嘞好嘞。
面试官:对了,这一面还没结束,undolog看你挺熟的,下次详细问你,还有 MySQL 锁啊我都还没问,等通知下次再来吧。
我:(还给我布置家庭作业呢?)我一定回去好好准备准备,等待您的宠幸。
这老面试官可以,竟然没折磨到他,等着,下次 undolog 和 MySQL 锁我一定好好招待他!
推荐阅读:《从根儿上理解 MySQL》
微信搜「 yes的练级攻略 」第一时间阅读,回复【算法】有我准备一份拿到 ssp offer 20W 字的算法笔记,更多可看GitHub github.com/yessimida/y… ,欢迎Star。