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

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

相关推荐
IT_陈寒3 小时前
Java并发编程避坑指南:7个常见陷阱与性能提升30%的解决方案
前端·人工智能·后端
云博客-资源宝3 小时前
【防火墙源码】WordPress防火墙插件1.0测试版
linux·服务器·数据库
牧码岛3 小时前
服务端之NestJS接口响应message编写规范详解、写给前后端都舒服的接口、API提示信息标准化
服务器·后端·node.js·nestjs
星秀日4 小时前
框架--SpringBoot
java·spring boot·后端
金色天际线-5 小时前
mysql全量+增量备份脚本及计划任务配置
数据库·mysql
zym大哥大5 小时前
MySQL用户管理
数据库·mysql
musenh5 小时前
mysql学习---事务
学习·mysql
对着晚风做鬼脸5 小时前
MySQL 运维知识点(十六)---- 读写分离
运维·数据库·mysql·adb
musenh5 小时前
mysql学习--DCL
学习·mysql·adb