MVCC 多版本并发控制

解决多个事务同时读写数据时的冲突问题,主要用于实现事务的两个隔离级别:

  • 读已提交 (Read Committed)

  • 可重复读 (Repeatable Read) (这是 MySQL InnoDB 默认的隔离级别)

【可以理解为主要是实现读已提交,只能读已经被事务提交后产生的版本,在实现读已提交的基础上,将read view生成时机从每一次select之后改成只是第一次select的时候生成read view,后面的select不再生成新的read view,就可以实现可重复读了】

实现原理:版本链(每一行数据有一个版本链,历史版本数据组成的链)+readview

1.版本链:

在每行数据中加了三个隐藏字段,这前两个隐藏字段可以帮助构造一个版本链

  1. DB_TRX_ID: 最近一次修改这行数据的事务ID。

  2. 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) 的工作负担

  • "读已提交"并非"可重复读"的降级版,而是一种不同的设计选择。它放宽了对一致性的要求,以换取更高的数据新鲜度(可以看到更实时的数据)、更好的并发性能和更低的系统资源消耗。

相关推荐
哲Zheᗜe༘1 天前
了解学习Redis主从复制
数据库·redis·学习
你的人类朋友1 天前
设计模式有哪几类?
前端·后端·设计模式
一条懒鱼6661 天前
Redis Sentinel哨兵集群
数据库·redis·sentinel
Yeats_Liao1 天前
Go Web 编程快速入门 10 - 数据库集成与ORM:连接池、查询优化与事务管理
前端·数据库·后端·golang
金仓拾光集1 天前
金仓数据库替代MongoDB实战:政务电子证照系统的国产化转型之路
数据库·mongodb·政务·数据库平替用金仓·金仓数据库
麦麦大数据1 天前
F036 vue+flask中医热性药知识图谱可视化系统vue+flask+echarts+mysql
vue.js·python·mysql·flask·可视化·中医中药
你的人类朋友1 天前
适配器模式:适配就完事了bro!
前端·后端·设计模式
BullSmall1 天前
一键部署MySQL
数据库·mysql
间彧1 天前
SpringBoot集成RocketMQ事务消息
后端