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

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

相关推荐
AlickLbc7 分钟前
DM-V8数据库证书过期处理过程记录
数据库
l1t8 分钟前
DeepSeek总结的五分钟测试:Ted Dunning 谈 DuckDB 为何让分析更轻松
数据库·sql·duckdb
超级新零售23 分钟前
零售 SaaS 与本地部署,技术责任边界究竟怎么分?
数据库·零售
考虑考虑41 分钟前
瀚高数据库
数据库·后端·postgresql
dishugj1 小时前
oracle日志切换三条命令区别
数据库
锋行天下1 小时前
DeepSeek写Langgraph就是个大傻逼,一本正经胡说八道
前端·人工智能·后端
Patrick在香港1 小时前
一个斜杠,把毫秒变成十几秒
android·数据库·python·性能优化·正则表达式·回溯·redos
悟道书生2 小时前
(一)Navicate为什么转存结构和数据只转存表而不包含建库?
数据库·oracle
赤龙ERP2 小时前
ERP周报:连接器开闸、国产GPU进场与智能工厂253家
数据库·人工智能·大模型·rpa·erp·企业数字化·builderpattern
lusklusklusk2 小时前
SQL Server补丁版本号为什么“日期新反而版本低”?——GDR、CU、CU+GDR 版本号规则与升级路径详解
数据库