MySQL 事务 MVCC 版本链

该博客为转载内容,转载自https://www.cnblogs.com/xibuhaohao/p/11065532.html ,涉及数据库相关知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 
版本链
 
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含row_id列):
 1)trx_id:每次对某条聚簇索引记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
 2)roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

比方说我们的表t现在只包含一条记录:
mysql> SELECT * FROM t;
+----+--------+
| id | c      |
+----+--------
+|  1 | 刘备   |
+----+--------+
1 row in set (0.01 sec)
 
假设插入该记录的事务id为80,那么此刻该条记录的示意图如下所示:
 

 

 
假设之后两个id分别为100、200的事务对这条记录进行UPDATE操作,操作流程如下:
 
 

 


小贴士: 能不能在两个事务中交叉更新同一条记录呢?哈哈,这是不可以滴,第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。本篇文章不是讨论锁的,有关锁的更多细节我们之后再说。

 
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:
 

 

 
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id,这个信息很重要,我们稍后就会用到。
 
ReadView
 
对于使用READ UNCOMMITTED隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用SERIALIZABLE隔离级别的事务来说,使用加锁的方式来访问记录。对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,就需要用到我们上边所说的版本链了,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。所以设计InnoDB的大叔提出了一个ReadView的概念,这个ReadView中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids。这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
 1)如果被访问版本的trx_id属性值小于m_ids列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问  
 2)如果被访问版本的trx_id属性值大于m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
 3)如果被访问版本的trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
4)如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。
在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同,我们来看一下。
 
READ COMMITTED --- 每次读取数据前都生成一个ReadView
 
比方说现在系统里有两个id分别为100、200的事务在执行:

 


小贴士: 事务执行过程中,只有在第一次真正修改记录时(比如使用INSERT、DELETE、UPDATE语句),才会被分配一个单独的事务id,这个事务id是递增的。

 
此刻,表t中id为1的记录得到的版本链表如下所示:
 

 

 
假设现在有一个使用READ COMMITTED隔离级别的事务开始执行:

 

 
这个SELECT1的执行过程如下:
 1)在执行SELECT语句时会先生成一个ReadView,ReadView的m_ids列表的内容就是[100, 200]。
 2)然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列c的内容是'张飞',该版本的trx_id值为100,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
 3)下一个版本的列c的内容是'关羽',该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
 4)下一个版本的列c的内容是'刘备',该版本的trx_id值为80,小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为'刘备'的记录。
 
 
之后,我们把事务id为100的事务提交一下,就像这样:

 

 
然后再到事务id为200的事务中更新一下表t中id为1的记录:

 

此刻,表t中id为1的记录的版本链就长这样:
 

 

 
然后再到刚才使用READ COMMITTED隔离级别的事务中继续查找这个id为1的记录,如下:

 

 
这个SELECT2的执行过程如下:
 1)在执行SELECT语句时会先生成一个ReadView,ReadView的m_ids列表的内容就是[200](事务id为100的那个事务已经提交了,所以生成快照时就没有它了)。
 2)然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列c的内容是'诸葛亮',该版本的trx_id值为200,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
 3)下一个版本的列c的内容是'赵云',该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
 4)下一个版本的列c的内容是'张飞',该版本的trx_id值为100,比m_ids列表中最小的事务id200还要小,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为'张飞'的记录。
 
 
以此类推,如果之后事务id为200的记录也提交了,再此在使用READ COMMITTED隔离级别的事务中查询表t中id值为1的记录时,得到的结果就是'诸葛亮'了,具体流程我们就不分析了。总结一下就是:使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。
 
REPEATABLE READ ---在第一次读取数据时生成一个ReadView
 
对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。我们还是用例子看一下是什么效果。
 
比方说现在系统里有两个id分别为100、200的事务在执行:

 

 
此刻,表t中id为1的记录得到的版本链表如下所示:
 
 

 

 
假设现在有一个使用REPEATABLE READ隔离级别的事务开始执行:

 

 
这个SELECT1的执行过程如下:
 1)在执行SELECT语句时会先生成一个ReadView,ReadView的m_ids列表的内容就是[100, 200]。
 2)然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列c的内容是'张飞',该版本的trx_id值为100,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
 3)下一个版本的列c的内容是'关羽',该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
 4)下一个版本的列c的内容是'刘备',该版本的trx_id值为80,小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为'刘备'的记录。
之后,我们把事务id为100的事务提交一下,就像这样:

 

 
然后再到事务id为200的事务中更新一下表t中id为1的记录:
此刻,表t中id为1的记录的版本链就长这样:

 

 
然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个id为1的记录,如下:
这个SELECT2的执行过程如下:
 1)因为之前已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView中的m_ids列表就是[100, 200]。
 2)然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列c的内容是'诸葛亮',该版本的trx_id值为200,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
 3)下一个版本的列c的内容是'赵云',该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
 4)下一个版本的列c的内容是'张飞',该版本的trx_id值为100,而m_ids列表中是包含值为100的事务id的,所以该版本也不符合要求,同理下一个列c的内容是'关羽'的版本也不符合要求。继续跳到下一个版本。
 5)下一个版本的列c的内容是'刘备',该版本的trx_id值为80,80小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为'刘备'的记录。
 
 
也就是说两次SELECT查询得到的结果是重复的,记录的列c值都是'刘备',这就是可重复读的含义。如果我们之后再把事务id为200的记录提交了,之后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个id为1的记录,得到的结果还是'刘备',具体执行过程大家可以自己分析一下。
 
MVCC总结
 
从上边的描述中我们可以看出来,所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复这个ReadView就好了。
---------------------
作者:wust_zwl
来源:优快云
原文:https://blog.youkuaiyun.com/qq_38538733/article/details/88902979
版权声明:本文为博主原创文章,转载请附上博文链接!

转载于:https://www.cnblogs.com/xibuhaohao/p/11065532.html

### MySQL 事务MVCC 实现原理 #### 一、事务的概念及其特性 在数据库环境中,事务是指一系列作为一个整体执行的操作序列。这一系列操作要么全部成功完成并永久生效;如果其中任何一个环节失败,则整个事务都会回滚至初始状态,确保数据一致性[^1]。 - **原子性(Atomicity)**:事务中的所有操作视为单一工作单元,该单元内各项更改应同生共死。 - **一致性(Consistency)**:事务前后需保持一致的状态转换,即遵循预定义规则。 - **隔离性(Isolation)**:各并发运行的事务间相互独立不受干扰。 - **持久性(Durability)**:一旦提交,即使发生系统崩溃也能够恢复成果。 #### 二、MVCC概述 为了提升并发处理能力的同时保障读写的正常交互,MySQL引入了多版本并发控制(MVCC)[^2]。其核心在于维护同一份记录的不同时间点上的多个版本,以此支持不同类型的读取行为——快照读与当前读: - **快照读(Snapshot Read)**:无需加锁即可获取指定时刻的数据视图,适用于大多数SELECT查询场景; - **当前读(Current Read)**:涉及锁定机制以防止其他会话修改正在访问的对象实例,常用于UPDATE/DELETE以及特定条件下带有FOR UPDATE或LOCK IN SHARE MODE修饰符的SELECT语句中。 #### 三、InnoDB存储引擎下的MVCC实现细节 作为MySQL默认使用的高性能事务型表管理器,InnoDB实现了上述提到的两种主要读取模式,并通过以下组件共同作用达成高效稳定的并发控制效果[^3]: - **隐藏列**:每行记录额外携带两个隐含属性`DB_TRX_ID`(最近更新此条目的事务ID)`DB_ROLL_PTR`(指向undo日志的位置),用作判断可见性的依据之一; - **Undo Log**:当某笔交易对现有资料进行了变更时,旧版会被复制到undo log区域保存起来直到不再需要为止(比如超出了必要的保留期限或是相关联的事物已经结束); - **ReadView**:每当启动一个新的只读类事物之前都要构建一个read view对象,里面包含了创建瞬间活跃着的所有活动事物列表以及其他必要信息用来决定哪些历史版本是可以被看见的。 #### 四、可见性逻辑判定流程 针对某个给定的记录版本R,假设现在有一个正在进行中的事务T想要对其进行读取,那么根据如下准则来确定是否能观察到它: 1. 若R是在T开启之后才产生的变动,则不可见; 2. 假设R是由另一个尚未完结且位于T之前的事务S所引起的变化,此时要看S是否处于已准备提交但还未正式commit阶段: - 是 -> R暂时不可见; - 否 -> 继续下一步骤; 3. 检查产生R的那个事务U是否存在于当前T持有的readview之内: - 存在-> 不可见 ; - 缺失-> 可见; 此外还有专门针对某些特殊情况设计的小于最低限度id比较规则等辅助手段帮助更精准地界定可视范围边界条件[^4]。 ```sql -- 示例SQL展示如何查看当前系统的最小未分配事务ID SHOW ENGINE INNODB STATUS\G ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值