什么是MVCC

什么是MVCC?#

MVCC,全称为Multi-Version Concurrency Control,即多版本并发控制。主要的目的是为了提高数据库的性能,更好地处理读写冲突,即使出现了读写冲突也可以通过不加锁的方式来解决。
多版本是指数据库中的数据存在多个版本,

当前读和快照读#

在介绍MVCC前,需要先了解当前读和快照读两个概念。

当前读#

在当前读中,事务只能读取已经提交的数据。这意味着如果一个事务正在对数据行做出修改,而另一个事务也要读取这一行,后者会等待前者完成修改。因此,当前读会提供更加一致的视图。
MySQL中提供了两种当前读的实现:

  • 一致性读
    在可重复读的隔离等级下,MySQL使用一致性读来实现当前都。在事务开始时,会创建一个一致性视图,这个视图是事务开始时刻数据库的快照,在事务执行期间,无论其他事务对数据做了何种修改,当前事务都只会从这个一致性视图中读取数据,保证了读取的一致性。
  • 锁定读
    当使用锁定读时,会对数据上一把共享锁或者排他锁,从而保证了数据的一致性。
    锁定读适用于需要严格控制并发的场景,而且加锁带来的性能开销较大。
当前读会使用到的语法
select ... lock in share mode
select ... for update
update
delete
insert

快照读#

快照读允许事务读取一个事务开始时的数据快照,而不受其他事务的影响。这意味着事务在其执行期间看到的数据保持一致,即使其他事务在此期间对数据进行了修改。这种方法通常通过数据库中的一种快照机制来实现,因此不会阻塞其他事务对数据的修改。通常的select操作都是快照读。

MVCC初步#

MySQL中的每行数据,除了用户定义的外,还会有一些自带的隐藏字段。

字段 含义
DB_ROW_ID 自增id(隐藏主键),用于唯一地标识一行数据。
DB_TRX_ID 当前所属的事务id,每个事务在数据库中都有一个唯一的事务id,通过这个字段,可以追踪行数据和事务的所属关系。
DB_ROLL_PTR 保存回滚指针,指向了回滚事务的Undo日志记录。

Undo日志是能实现MVCC的关键所在,其主要有两个作用:

  • 事务回滚,当事务回滚时,会用Undo日志中的旧值还原事务的开始状态。
  • MVCC实现,通过Undo日志,可以为每个事务都提供一个独立的事务视图。

版本链#

在MVCC中,对于每次更新操作,旧值会被保存到一条undo日志中,看作是该记录的旧版本。随着更新次数的增加,所有的版本都会通过roll_pointer属性连接成一个链表,称之为版本链。
版本链的头节点代表当前记录的最新值。此外,每个版本还包含生成该版本的事务ID。

一致性视图 ReadView#

用来判断版本链中哪个版本对当前事务可见。事务在进行快照读操作时生成的读视图,在该事务执行快照读的时刻,会生成一个数据库系统当前的快照,记录并维护系统当前活跃事务的id(每个事务开启时,都会被分配一个id,这个id是递增的)。另外,MVCC只在读提交和可重复读隔离等级下才有效。
ReadView会维护以下几个字段

字段 说明
m_ids 视图创建时,会将当前还未提交的事务id记录下来,后续即使它们修改了记录行的数据,对于当前事务也是不可见的。m_ids不包括当前自身的事务和已提交的事务。
m_creator_trx_id 当前事务自身的事务id
m_low_limit_id 当前出现过的最大的事务id+1,即下一个将被分配的事务id。大于等于这个id的数据版本均不可见。
m_up_limit_id 活跃事务列表m_ids中最小的事务id,如果m_ids为空,则m_low_limit_id为m_up_limit_id,小于这个id的版本均可见。

通过这几个字段和行数据的隐藏字段进行比对就可完成版本控制:

  • 如果被访问的版本的DB_TRX_ID比视图中的m_creator_trx_id相同,说明正在访问自己修改的记录,因此数据的该版本可以被该事务访问。
  • 如果DB_TRX_ID小于视图的m_up_limit_id,说明生成该版本数据的事务已经在之前提交了,因此该版本的数据也可以被访问到。
  • 如果DB_TRX_ID大于或等于m_low_limit_id,说明生成该版本的事务在当前事务之后才提交的,因此该版本不能被当前事务访问到。
  • 如果DB_TRX_ID位于m_up_limit_idm_low_limit_id之间,则需要进一步检查DB_TRX_ID是否在m_ids列表中。如果在列表中,说明在创建视图时生成该版本的事务仍未提交,则不能访问;如果不在,则说明已经提交过了,那么就可以访问。

一致性视图遵循一个可见性原则:将要修改的数据的DB_TRX_ID和当前其他活跃的事务的id进行比对,如果DB_TRX_ID不可以被访问,那么就顺着DB_ROLL_PTR回滚指针去取出Undo日志中的DB_TRX_ID再进行对比,直到找到满足特定条件的DB_TRX_ID,此时DB_TRX_ID对应的记录就是当前事务可以看到的最新版本。

读提交隔离等级下的视图#

读提交和可重复读下的视图有所差别。
读提交的每一次读取数据都会生成一个当前的视图;而可重复读只会在事务开启时生成一个视图,后面的读取都会在这个视图上操作。这就导致了两个隔离等级下看到的数据有所差别。
举例说明,当前有这样一条数据

DB_ROW_ID DB_TRX_ID DB_ROLL_PTR id name
1 1 1 小明

现在有三个事务并发执行。


事务A(事务id:100) 事务B(事务id:200) 事务C(事务id:300)
T1 begin
T2 begin begin
T3 update user set name = "小王" where id = 1
T4 update user set name = "小红" where id = 1 select * from user where id = 1
T5 commit update user set name = "小军" where id = 1
T6 update user set name = "小白" where id = 1 select * from user where id = 1
T7 commit
T8 select * from user where id = 1
T9 commit

在T4时刻,事务A和事务B都没有提交,所以是活跃的事务id,即m_ids = { 100, 200 },四个字段的值分别为

字段
m_ids
m_creator_trx_id 300
m_low_limit_id 400
m_up_limit_id 100

T4时刻的版本链是这样的:

T4时刻版本链

那么,在T4时刻事务C能看到的数据应该是name = “小明”。
同理,在T6时刻,事务C看到的数据是name = “小红”,因为此时事务A已经提交了,而在提交读隔离等级下,每次读取都会创建一个新的视图,所以可以看到事务A提交的记录。
此时的视图四个字段值是

字段
m_ids
m_creator_trx_id 300
m_low_limit_id 400
m_up_limit_id 200

那么,在T8时刻,事务C看到的数据就是name = “小白”.
此时的视图四个字段值是

字段
m_ids
m_creator_trx_id 300
m_low_limit_id 400
m_up_limit_id 400

可重复读隔离等级下的视图#

同样的事务执行流程,正如前面说的,可重复读隔离等级下每次都会使用事务开启时的视图,m_ids始终都是{ 100, 200 },那么事务C每次读出来的数据都是name = “小明”。

作者:cwtxx

出处:https://www.cnblogs.com/cwtxx/p/18718210

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   cwtxx  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示