深入理解MySQL的MVCC原理

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 深入理解MySQL的MVCC原理一、MVCC定义1、并发事务可能产生的问题2、当前读和快照读二、MVCC实现、原理1、隐藏字段2、版本链3、ReadView三、手动验证MVCC的原理1、事务隔离级别为RC(读已提交隔):2、事务隔离级别为RR(可重复读):

一、MVCC定义



MVCC(Mutil Version Concurrency Control)多版本并发控制,是一种并发控制的方法(而非具体实现),一般在数据库管理系统中,实现对数据库的并发访问。

上面的解释比较抽象,下面来一点一点分析。



1、并发事务可能产生的问题

当一个事务访问数据库的数据时,无论读、写,都不会产生并发问题。


当两个事务同时访问数据库中的相同数据时,可能有几种情况:


读:两个事务都查询数据。当两个事务对相同数据全部是读操作时,不会产生任何并发问题。

读+写:一个事务查询数据,一个事务修改数据。当两个事务对相同数据有读有写时,可能会产生脏读、不可重复度、幻读的问题(但发生脏读、不可重复度、幻读就并不一定表示有问题,具体还是要看场景,有些业务场景发生不可重复度是不允许的,但有些业务场景可能发生脏读也没啥大碍)。

写:两个事务都修改数据,当两个事务对相同数据全部是写操作时,可能产生数据丢失(回滚丢失、覆盖丢失)等问题。

多个事务同时访问数据库中相同的数据,也是一样的,可能存在【读】、【读+写】、【写】这三种情况。


那如何解决上面的问题呢?


并发事务对数据的读操作不会产生并发问题,所以不用解决;

并发事务对数据的读+写,常规操作一般会对要操作的数据加锁来解决并发读+写可能产生的问题,MySQL的InnoDB实现了MVCC来更好地处理读写冲突,可以做到即使存在并发读写,也可以不用加锁,实现"非阻塞并发读"。

并发事务对数据的写操作,只能通过加锁(乐观锁/悲观锁)来解决。

到了这里,我们脑子里需要有这么个印象:MySQL实现的MVCC,主要是用于在并发读写的情况下,保证 “读” 数据时无需加锁也可以读取到数据的某一个版本的快照,好处是可以避免加锁,降低开销,解决了读写冲突,增大了数据库的并发性能。


2、当前读和快照读


在进一步了解MySQL中实现MVCC的细节之前,还需要了解两个定义:


当前读:读取的数据是最新版本,读取数据时还要保证其他并发事务不会修改当前的数据,当前读会对读取的记录加锁。比如:select …… lock in share mode(共享锁)、select …… for update | update | insert | delete(排他锁)


快照读:每一次修改数据,都会在 undo log 中存有快照记录,这里的快照,就是读取undo log中的某一版本的快照。这种方式的优点是可以不用加锁就可以读取到数据,缺点是读取到的数据可能不是最新的版本。一般的查询都是快照读,比如:select * from t_user where id=1;在MVCC中的查询都是快照度。


二、MVCC实现、原理



MySQL中MVCC主要是通过行记录中的隐藏字段(隐藏主键 row_id、事务ID trx_id、回滚指针 roll_pointer)、undo log(版本链)、ReadView(一致性读视图)来实现的。

1、隐藏字段


MySQL中,在每一行记录中除了自定义的字段,还有一些隐藏字段:


row_id:当数据库表没定义主键时,InnoDB会以row_id为主键生成一个聚集索引。


trx_id:事务ID记录了新增/最近修改这条记录的事务id,事务id是自增的。


roll_pointer:回滚指针指向当前记录的上一个版本(在 undo log 中)。


2、版本链


简单提下 redo log 和 undo log。在修改数据的时候,会向 redo log 中记录修改的页内容(为了在数据库宕机重启后恢复对数据库的操作),也会向 undo log 记录数据原来的快照(用于回滚事务)。undo log有两个作用,除了用于回滚事务,还用于实现MVCC。


用一个简单的例子来画一下MVCC中用到的undo log版本链的逻辑图:


当事务100(trx_id=100)执行了 insert into t_user values(1,'张三',20);之后:

40.png


当事务102(trx_id=102)执行了 update t_user set name='李四' where id=1;之后:

41.png

当事务103(trx_id=103)执行了 update t_user set name='王五' where id=1;之后:

42.png



3、ReadView


在上面的例子中,多个事务对 id=1 的数据修改后,这行记录除了最新的数据,在 undo log 中还有多个版本的快照。那其他事务查询时能查到最新版本的数据吗?如果不能,能读到哪个版本的快照呢?这就要由ReadView来决定了。


ReadView 就是MVCC在对数据进行快照读时,会产生的一个”读视图“(翻译过来就是ReadView~哈哈哈)。


ReadView中有4个比较重要的变量(具体这几个变量名是啥我也不知道,不过不要在意这些细节,这里就随便定义一下……):


m_ids:活跃事务id列表,当前系统中所有活跃的(也就是没提交的)事务的事务id列表。


min_trx_id:m_ids 中最小的事务id。


max_trx_id:生成 ReadView 时,系统应该分配给下一个事务的id(注意不是 m_ids 中最大的事务id),也就是m_ids 中的最大事务id + 1 。


creator_trx_id:生成该 ReadView 的事务的事务id。


某个事务进行快照读时可以读到哪个版本的数据,ReadView 有一套算法:


(1)当【版本链中记录的 trx_id 等于当前事务id(trx_id = creator_trx_id)】时,说明版本链中的这个版本是当前事务修改的,所以该快照记录对当前事务可见。


(2)当【版本链中记录的 trx_id 小于活跃事务的最小id(trx_id < min_trx_id)】时,说明版本链中的这条记录已经提交了,所以该快照记录对当前事务可见。


(3)当【版本链中记录的 trx_id 大于下一个要分配的事务id(trx_id > max_trx_id)】时,该快照记录对当前事务不可见。


(4)当【版本链中记录的 trx_id 大于等于最小活跃事务id】且【版本链中记录的trx_id小于下一个要分配的事务id】(min_trx_id<= trx_id < max_trx_id)时,如果版本链中记录的 trx_id 在活跃事务id列表 m_ids 中,说明生成 ReadView 时,修改记录的事务还没提交,所以该快照记录对当前事务不可见;否则该快照记录对当前事务可见。


当事务对 id=1 的记录进行快照读时select * from t_user where id=1,在版本链的快照中,从最新的一条记录开始,依次判断这4个条件,直到某一版本的快照对当前事务可见,否则继续比较上一个版本的记录。


MVCC主要是用来解决RU隔离级别下的脏读和RC隔离级别下的不可重复读的问题,所以MVCC只在RC(解决脏读)和RR(解决不可重复读)隔离级别下生效,也就是MySQL只会在RC和RR隔离级别下的快照读时才会生成ReadView。区别就是,在RC隔离级别下,每一次快照读都会生成一个最新的ReadView;在RR隔离级别下,只有事务中第一次快照读会生成ReadView,之后的快照读都使用第一次生成的ReadView。


事务能否查询到其他事务修改的数据,取决于可见性算法,可见性算法又是取决于 ReadView 中的值,ReadView 在 RC 和 RR 两种隔离级别下生成的时机不同,所以导致两种隔离级别下,某个事务修改数据的可见性对其他事务是不同的(RC隔离级别下一个事务可以查询到其他事务在此期间修改并提交的数据;RR隔离级别下一个事务无法查询到其他事务在此期间修改并提交的数据)。


还是有点抽象?那就手动来亲自验证一下,之后就会清晰很多。(如果想要真正理解上面的算法,建议最好找个例子,亲自验证一下)


三、手动验证MVCC的原理



还是用上面的例子来说。

前提条件:事务100(trx_id=100)向表中插入了一条id=1的数据: insert into t_user values(1,'张三',20);并提交了事务。

之后又有3个事务(事务101、事务102、事务103)来对这条数据进行读写操作:


43.png44.png

在时间点 t1 ~ t6 时,整个版本链中只有一个快照,trx_id 为 100:

45.png


在时间点 t7 ~ t11 时,整个版本链中有两个快照,trx_id 为 102、100:


46.png

在时间点 t11 ~ t14 时,整个版本链中有三个快照,trx_id 为 103、102、100:


47.png


1、事务隔离级别为RC(读已提交隔):

当前事务隔离级别为RC(读已提交隔)时,每个事务每次查询对应生成的ReadView是这样的,跟着这张图来梳理一下:

48.png


在时间点t2,事务101查询时生成的ReadView内容为:


trx_list: 101
min_trx_id:101
max_trx_id:102
creator_trx_id:101
  • 当前时间点,版本链中只有一个快照(trx_id=100),因为 trx_id(100)< min_trx_id(101),符合算法的第(2)条规则,所以trx_id=100的这个快照对当前事务可见。
  • 在时间点t4,事务102查询时生成的ReadView内容为:
trx_list: 101,102
min_trx_id:101
max_trx_id:103
creator_trx_id:102


  • 当前时间点,版本链中只有一个快照(trx_id=100),因为 trx_id(100)< min_trx_id(101),符合算法的第(2)条规则,所以trx_id=100的这个快照对当前事务可见。
  • 在时间点t6,事务103查询时生成的ReadView内容为:
trx_list: 101,102,103
min_trx_id:101
max_trx_id:104
creator_trx_id:103


  • 当前时间点,版本链中只有一个快照(trx_id=100),因为 trx_id(100)< min_trx_id(101),符合算法的第(2)条规则,所以trx_id=100的这个快照对当前事务可见。
  • 在时间点t8,事务101查询时生成的ReadView内容为:
trx_list: 101,102,103
min_trx_id:101
max_trx_id:104
creator_trx_id:101

当前时间点,版本链中有两个快照(trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:


对于trx_id=102的快照,min_trx_id(101) <= trx_id(102) < max_trx_id(104) ,且trx_id(102) 在 trx_list(101,102,103) 中,说明当前事务生成ReadView时,修改该记录的事务仍然是活跃事务(还未提交),根据算法的第(4)条规则,trx_id=102的快照对当前事务不可见。这也就验证了在RC隔离级别下,事务102修改但未提交的数据对于事务101应该不可见。


对于trx_id=100的快照,因为 trx_id(100)< min_trx_id(101),符合算法的第(2)条规则,所以trx_id=100的这个快照对当前事务可见。


在时间点t9,事务102查询时生成的ReadView内容为:


trx_list: 101,102,103
min_trx_id:101
max_trx_id:104
creator_trx_id:102


当前时间点,版本链中有两个快照(trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:


对于trx_id=102的快照,因为 trx_id(102) = creator_trx_id(102),符合算法的第(1)条规则,所以trx_id=102的这个快照对当前事务可见。


在时间点t11,事务103查询时生成的ReadView内容为:


trx_list: 101,103
min_trx_id:101
max_trx_id:104
creator_trx_id:103


当前时间点,版本链中有两个快照(trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:


对于trx_id=102的快照,min_trx_id(101) <= trx_id(102) < max_trx_id(104) ,且trx_id(102) 不在 trx_list(101,103) 中,说明当前事务生成ReadView时,修改该记录的事务不是活跃事务(已经提交),根据算法的第(4)条规则,trx_id=102的快照对当前事务可见。这也就验证了在RC隔离级别下,事务102修改且提交的数据对于事务103是可见的。


在时间点t14,事务101查询时生成的ReadView内容为:


trx_list: 101
min_trx_id:101
max_trx_id:104
creator_trx_id:101


当前时间点,版本链中有三个快照(trx_id=103 -> trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:


对于trx_id=103的快照,min_trx_id(101) <= trx_id(103) < max_trx_id(104) ,且trx_id(103) 不在 trx_list(101) 中,说明当前事务生成ReadView时,修改该记录的事务不是活跃事务(已经提交),根据算法的第(4)条规则,trx_id=103的快照对当前事务可见。这也就验证了在RC隔离级别下,事务103修改且提交的数据对于事务101是可见的。


2、事务隔离级别为RR(可重复读):


当前事务隔离级别为RR(可重复读)时,每个事务每次查询对应生成的ReadView是这样的,跟着这张图来梳理一下:

49.png


上面说过,在RC隔离级别下,每一次快照读都会生成一个最新的ReadView;在RR隔离级别下,只有事务中第一次快照读会生成ReadView,之后的快照读都使用第一次生成的ReadView。


所以,事务101在 t8、t14 时刻查询时,使用的 ReadView 跟 t2 时刻一样;事务102在t9时刻查询时使用的ReadView 跟 t4 时刻一样;事务103在 t11 时刻查询时使用的ReadView 跟 t6 时刻一样。


文章到这里就结束了,有心的同学可以跟着上面【事务隔离级别为RC】时的步骤,来推演验证一下在每个时间点、每个事务查询都能查到哪个版本的快照数据,也能加深一下理解(为了有些同学推演后想对比答案,我就把答案也写在下面了)。




在时间点t2,事务101查询时生成的ReadView内容为:

trx_list: 101
min_trx_id:101
max_trx_id:102
creator_trx_id:101


  • 当前时间点,版本链中只有一个快照(trx_id=100),因为 trx_id(100)< min_trx_id(101),符合算法的第(2)条规则,所以trx_id=100的这个快照对当前事务可见。
  • 在时间点t4,事务102查询时生成的ReadView内容为:
trx_list: 101,102
min_trx_id:101
max_trx_id:103
creator_trx_id:102
  • 当前时间点,版本链中只有一个快照(trx_id=100),因为 trx_id(100)< min_trx_id(101),符合算法的第(2)条规则,所以trx_id=100的这个快照对当前事务可见。
  • 在时间点t6,事务103查询时生成的ReadView内容为
trx_list: 101,102,103
min_trx_id:101
max_trx_id:104
creator_trx_id:103


当前时间点,版本链中只有一个快照(trx_id=100),因为 trx_id(100)< min_trx_id(101),符合算法的第(2)条规则,所以trx_id=100的这个快照对当前事务可见。


在时间点t8,事务101查询时用的ReadView和在t2时间点生成的ReadView一样:

trx_list: 101
min_trx_id:101
max_trx_id:102
creator_trx_id:101


当前时间点,版本链中有两个快照(trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:


对于trx_id=102的快照,trx_id(102) >= max_trx_id(102) ,根据算法的第(3)条规则,trx_id=102的快照对当前事务不可见。这也验证了在RR隔离级别下,事务102修改但未提交的数据对于事务101应该不可见(RC都不可见了,更别说RR了)。


对于trx_id=100的快照,因为 trx_id(100)< min_trx_id(101),符合算法的第(2)条规则,所以trx_id=100的这个快照对当前事务可见。


在时间点t9,事务102查询时用的ReadView和在t4时间点生成的ReadView一样:


trx_list: 101,102
min_trx_id:101
max_trx_id:103
creator_trx_id:102


当前时间点,版本链中有两个快照(trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:


对于trx_id=102的快照,因为 trx_id(102) = creator_trx_id(102),符合算法的第(1)条规则,所以trx_id=102的这个快照对当前事务可见。


在时间点t11,事务103查询时用的ReadView和在t6时间点生成的ReadView一样:


trx_list: 101,102,103
min_trx_id:101
max_trx_id:104
creator_trx_id:103

当前时间点,版本链中有两个快照(trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:


对于trx_id=102的快照,min_trx_id(101) <= trx_id(102) < max_trx_id(104) ,且trx_id(102) 在 trx_list(101,102,103) 中,说明当前事务生成ReadView时,修改该记录的事务是活跃事务(还未提交),根据算法的第(4)条规则,trx_id=102的快照对当前事务不可见。这也就验证了在RR隔离级别下,事务102修改且提交的数据对于事务103是不可见的。


在时间点t14,事务101查询时生成的ReadView内容为:

trx_list: 101
min_trx_id:101
max_trx_id:102
creator_trx_id:101

当前时间点,版本链中有三个快照(trx_id=103 -> trx_id=102 -> trx_id=100),从版本链中的快照中,从最新的开始,依次判断:


对于trx_id=103的快照,trx_id(103) >= max_trx_id(102) ,根据算法的第(3)条规则,trx_id=103的快照对当前事务不可见。这也就验证了在RR隔离级别下,事务103修改且提交的数据对于事务101是不可见的。


对于trx_id=102的快照,trx_id(102) >= max_trx_id(102) ,根据算法的第(3)条规则,trx_id=102的快照对当前事务不可见。这也就验证了在RR隔离级别下,事务102修改且提交的数据对于事务101是不可见的。


对于trx_id=100的快照,trx_id(100) < min_trx_id(101) ,根据算法的第(2)条规则,trx_id=100的快照对当前事务可见。


相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
打赏
0
0
0
0
6
分享
相关文章
MySQL实现文档全文搜索,分词匹配多段落重排展示,知识库搜索原理分享
本文介绍了在文档管理系统中实现高效全文搜索的方案。为解决原有ES搜索引擎私有化部署复杂、运维成本高的问题,我们转而使用MySQL实现搜索功能。通过对用户输入预处理、数据库模糊匹配、结果分段与关键字标红等步骤,实现了精准且高效的搜索效果。目前方案适用于中小企业,未来将根据需求优化并可能重新引入专业搜索引擎以提升性能。
MySQL原理简介—6.简单的生产优化案例
本文介绍了数据库和存储系统的几个主题: 1. **MySQL日志的顺序写和数据文件的随机读指标**:解释了磁盘随机读和顺序写的原理及对数据库性能的影响。 2. **Linux存储系统软件层原理及IO调度优化原理**:解析了Linux存储系统的分层架构,包括VFS、Page Cache、IO调度等,并推荐使用deadline算法优化IO调度。 3. **数据库服务器使用的RAID存储架构**:介绍了RAID技术的基本概念及其如何通过多磁盘阵列提高存储容量和数据冗余性。 4. **数据库Too many connections故障定位**:分析了MySQL连接数限制问题的原因及解决方法。
RDS用多了,你还知道MySQL主从复制底层原理和实现方案吗?
随着数据量增长和业务扩展,单个数据库难以满足需求,需调整为集群模式以实现负载均衡和读写分离。MySQL主从复制是常见的高可用架构,通过binlog日志同步数据,确保主从数据一致性。本文详细介绍MySQL主从复制原理及配置步骤,包括一主二从集群的搭建过程,帮助读者实现稳定可靠的数据库高可用架构。
124 9
RDS用多了,你还知道MySQL主从复制底层原理和实现方案吗?
MySQL原理简介—9.MySQL索引原理
本文详细介绍了MySQL索引的设计与使用原则,涵盖磁盘数据页的存储结构、页分裂机制、主键索引设计及查询过程、聚簇索引和二级索引的原理、B+树索引的维护、联合索引的使用规则、SQL排序和分组时如何利用索引、回表查询对性能的影响以及索引覆盖的概念。此外还讨论了索引设计的案例,包括如何处理where筛选和order by排序之间的冲突、低基数字段的处理方式、范围查询字段的位置安排,以及通过辅助索引来优化特定查询场景。总结了设计索引的原则,如尽量包含where、order by、group by中的字段,选择离散度高的字段作为索引,限制索引数量,并针对频繁查询的低基数字段进行特殊处理等。
104 18
MySQL原理简介—9.MySQL索引原理
MySQL底层概述—6.索引原理
本文详细回顾了:索引原理、二叉查找树、平衡二叉树(AVL树)、红黑树、B-Tree、B+Tree、Hash索引、聚簇索引与非聚簇索引。
112 11
MySQL底层概述—6.索引原理
京东面试:MySQL MVCC是如何实现的?如何通过MVCC实现读已提交、可重复读隔离级别的?
1.请解释什么是MVCC,它在数据库中的作用是什么? 2.在MySQL中,MVCC是如何实现的?请简述其工作原理。 3.MVCC是如何解决读-写和写-写冲突的? 4.在并发环境中,当多个事务同时读取同一行数据时,MVCC是如何保证每个事务看到的数据版本是一致的? 5.MVCC如何帮助提高数据库的并发性能?
京东面试:MySQL MVCC是如何实现的?如何通过MVCC实现读已提交、可重复读隔离级别的?
MySQL原理简介—12.MySQL主从同步
本文介绍了四种为MySQL搭建主从复制架构的方法:异步复制、半同步复制、GTID复制和并行复制。异步复制通过配置主库和从库实现简单的主从架构,但存在数据丢失风险;半同步复制确保日志复制到从库后再提交事务,提高了数据安全性;GTID复制简化了配置过程,增强了复制的可靠性和管理性;并行复制通过多线程技术降低主从同步延迟,保证数据一致性。此外,还讨论了如何使用工具监控主从延迟及应对策略,如强制读主库以确保即时读取最新数据。
MySQL原理简介—12.MySQL主从同步
MySQL原理简介—7.redo日志的底层原理
本文介绍了MySQL中redo日志和undo日志的主要内容: 1. redo日志的意义:确保事务提交后数据不丢失,通过记录修改操作并在系统宕机后重做日志恢复数据。 2. redo日志文件构成:记录表空间号、数据页号、偏移量及修改内容。 3. redo日志写入机制:redo日志先写入Redo Log Buffer,再批量刷入磁盘文件,减少随机写以提高性能。 4. Redo Log Buffer解析:描述Redo Log Buffer的内存结构及刷盘时机,如事务提交、Buffer过半或后台线程定时刷新。 5. undo日志原理:用于事务回滚,记录插入、删除和更新前的数据状态,确保事务可完整回滚。
149 22
MySQL原理简介—8.MySQL并发事务处理
这段内容深入探讨了SQL语句执行原理、事务并发问题、MySQL事务隔离级别及其实现机制、锁机制以及数据库性能优化等多个方面。
MySQL原理简介—11.优化案例介绍
本文介绍了四个SQL性能优化案例,涵盖不同场景下的问题分析与解决方案: 1. 禁止或改写SQL避免自动半连接优化。 2. 指定索引避免按聚簇索引全表扫描大表。 3. 按聚簇索引扫描小表减少回表次数。 4. 避免产生长事务长时间执行。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等