前文有聊到事务的基本概念,基本操作,以及最后抛出来个隔离级别,本文继续讲解事务隔离级别中读提交(RC)和可重复读(RR)的实现原理。
补充知识
- 每个事务都有一个唯一的事务ID,并且越早创建的事务ID越小。
- 事务有着自己的生命周期。
预备知识
1. 表中的四个隐藏字段
- DB_TRX_ID:6 byte,事务ID
- DB_ROLL_ID:7 byte,回滚指针,指向这条记录的上一个版本。
- DB_ROW_ID:6 byte,隐藏主键
- 删除flag的隐藏字段:删除一条记录时,是表修改该记录的flag,而不是真正意义上的消失。
解释一下什么是隐藏主键,每张表都会有一个默认的隐藏主键,如果该表没有设置主键,InnoDB会自动以 DB_ROW_ID 产生一个聚簇索引,在展示数据时,就会按照默认主键遍历每个B+树中的数据叶子节点。
假设有张表stu,且只有一条数据,id为主键
id | name | gender | DB_TRX_ID(修改该事务的id) | DB_ROLL_ID | DB_ROW_ID | flag |
---|---|---|---|---|---|---|
1 | 张三 | 男 | null | null | null | false |
DB_ROLL_ID 为null,为什么?是因为一旦事务提交(commit)了,就没有历史版本了,所以为空
DB_ROW_ID 为空,为什么?因为有主键 id
2. undo日志
什么是undo日志?
一段内容缓冲区,用来存储日志数据。(主要用于回滚和隔离)
修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的副本,我们采用头插方式,插入undo log。
这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。
上面的一个一个版本,我们可以称之为一个一个的快照。
针对update
,delete
都是修改内容,都会形成版本链,如果是insert
呢?因为insert
是插入,也就是之前没有数据,那么insert
也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务commit了,那么这个undo log 的历史insert记录就可以被清空了。
最后,当没有其他事务访问数据的时候,undo log里的版本链才会被刷新释放。
3. ReadView
MySQL中的一个类,用于管理事务并发情况下的可见性
- 补充知识:select的查询分为当前读和快照读
当前读:就是查询的结果为最新版本记录。
# 当前读查询语句
select ... for update;
快照读:查询的结果为旧版本记录。
# 快照读查询语句(就是普通的select)
select ...;
在认识ReadView前,我们先对这个类中的重要成员先理解一下
字段 | 说明 |
---|---|
m_ids | 当前时间段活跃的事务集,是一个列表 |
up_limit_id | 最小活跃事务id,即m_ids中的最小值 |
low_limit_id | 全局事务id最大值+1 |
creator_trx_id | 创建ReadView的事务id |
- 活跃事务:启动了,但未提交的事务
对于上面的成员,如图所示:
一个事务进行快照读的时候,才会创建ReadView对象。ReadView对象会记录当前事务的信息,包括在这之前完成的最大事务id(也就是正在运行的最小事务id),这一时间段内正在运行的所有事务和未开始的事务id
ReadView会根据事务的id大小给不同时间段内的事务访问不一样的事务版本内容,从而实现事务的隔离性。
RC和RR的区别
区别
RC:每次快照读都会生成一个新的ReadView读视图
RR:仅在第一次快照读时生成ReadView读视图,且不再改变
正是因为RC每次快照读都生成新ReadView对象,所以对象里面的数据也一直在更新,故会根据事务id筛选读到不同的版本,这就是不可重复读问题的由来。