解决多个事务同时读写数据时的冲突问题,主要用于实现事务的两个隔离级别:
-
读已提交 (Read Committed)
-
可重复读 (Repeatable Read) (这是 MySQL InnoDB 默认的隔离级别)
【可以理解为主要是实现读已提交,只能读已经被事务提交后产生的版本,在实现读已提交的基础上,将read view生成时机从每一次select之后改成只是第一次select的时候生成read view,后面的select不再生成新的read view,就可以实现可重复读了】
实现原理:版本链(每一行数据有一个版本链,历史版本数据组成的链)+readview
1.版本链:
在每行数据中加了三个隐藏字段,这前两个隐藏字段可以帮助构造一个版本链
-
DB_TRX_ID
: 最近一次修改这行数据的事务ID。 -
DB_ROLL_PTR
: 回滚指针,指向这行数据上一个版本在undo log
中的位置。
3.DB_ROW_ID
: 隐藏的行ID,当表没有主键时,InnoDB会用它来生成聚簇索引(这个不用关注)。
sql
account表:
//假设99号事务创建了这条记录,undo log里面存储
ROW: {id:1, name:'张三', balance:1000} HIDDEN: {DB_TRX_ID: 99, DB_ROLL_PTR: null}
// 第一次修改,101号事务将余额改成800
ROW: {id:1, name:'张三', balance:800} HIDDEN: {DB_TRX_ID: 101, DB_ROLL_PTR: --> 指向undo_log_v1}
|
| (DB_ROLL_PTR)
v
undo_log_v1: {balance:1000, DB_TRX_ID: 99, DB_ROLL_PTR: null}
//第二次修改,102事务将余额改成500(即使事务还没提交,只要执行了update语句undo log的版本链就会有这个版本)
ROW: {id:1, name:'张三', balance:500} HIDDEN: {DB_TRX_ID: 102, DB_ROLL_PTR: --> 指向undo_log_v2}
|
| (DB_ROLL_PTR)
v
undo_log_v2: {balance:800, DB_TRX_ID: 101, ROLL_PTR: -->undo_log_v1}
|
| (DB_ROLL_PTR)
v
undo_log_v1: {balance:1000, DB_TRX_ID: 99, DB__ROLL_PTR: null}
2.Read view:
此时一个新的事务103查询余额,为了决定它能看到哪个版本,数据库会为它生成一个 Read View
:
sql
creator_trx_id: 103 -- 创建这个Read View的事务ID
m_ids: [102] --创建时,数据库里所有未提交的事务ID列表 (假设事务101已提交,事务102正在运行)
min_trx_id: 102 -- m_ids列表中的最小值
max_trx_id: 104 -- 创建时,数据库下一个将要分配的事务ID
把所有事务分成了三类:
-
已提交的"过去": 事务ID < min_trx_id (小于102) 的,比如事务99和101。
-
未知的"未来": 事务ID >= max_trx_id (大于等于104) 的。
-
正在发生的"现在"(但是还没有提交): 事务ID 在 min_trx_id 和 max_trx_id 之间,并且在 m_ids 列表里(即事务102)。
3.遍历版本链中每个版本,和read view做比对
核心就是:只能读已经提交的数据,不能读还没提交的数据,可以读的数据是已经提交的版本(<max_trx_id,而且不在 m_ids列表里面的事务 id 创建的数据)
事务103拿着它的 Read View,去读取张三那行数据对应的版本链,然后和版本链上每个节点进行比较:
sql
ROW: {id:1, name:'张三', balance:500} HIDDEN: {DB_TRX_ID: 102, DB_ROLL_PTR: --> 指向undo_log_v2}
|
| (DB_ROLL_PTR)
v
undo_log_v2: {balance:800, DB_TRX_ID: 101, ROLL_PTR: -->undo_log_v1}
|
| (DB_ROLL_PTR)
v
undo_log_v1: {balance:1000, DB_TRX_ID: 99, DB__ROLL_PTR: null}
链条上的第一个节点:这个版本是102事务创建的
(1)是我103事务创建的吗,不是
(2) 比min_trx_id小吗,不是
(3)比max_trx_id大吗,不是
(4) m_ids: [102] 里面有102吗,有
结论:102在 m_ids 列表,说明这个版本是在我创建read view时还没提交,因此我不能看这个版本的数据
链条上的第二个节点:这个版本是101事务创建的
"101比 min_trx_id (102) 小吗?" -> 是!
说明创建这个版本的事务已经提交了,因此可以看到这条数据
4.如何实现读已提交和可重复读
按照上面的实现,已经可以实现读已提交
将read view生成时机从每一次select之后改成只是第一次select的时候生成read view,后面的select不再生成新的read view,就可以实现可重复读了
5.既然可重复读这么容易实现,为什么还会存在读已提交这个隔离级别呢?
-
"可重复读"为了维持那个"不变的快照",是有成本的。对于一个长事务,只要这个事务不结束,这个旧版read view就要一直保存,这样如果要读很多行的数据,这样就要保存很多read view文件(读已提交则可以随时删掉,下一次select的时候会重新生成最新的),造成undo log文件非常大,增加数据库后台清理线程 (
purge thread
) 的工作负担 -
"读已提交"并非"可重复读"的降级版,而是一种不同的设计选择。它放宽了对一致性的要求,以换取更高的数据新鲜度(可以看到更实时的数据)、更好的并发性能和更低的系统资源消耗。