什么是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_id
和m_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时刻事务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 = “小明”。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~