MySQL中事务及MVCC原理

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样就可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个R
  • 在MySQL当中,只有使用了InnoDB存储引擎的数据库表才支持事务。
  • 有了事务就可以用来保证数据的完整以及一致性,保证成批的SQL语句要么全部执行,要么全部不执行。
  • 事务用来管理insert、update、delete语句。

1、四个特性(ACID):

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。


在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务务须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。

2、隔离级别

  • 读未提交(Read uncommitted),一个事务可以读取到其他事务中做出操作且还未提交的数据。会出现脏读,不可重复读,幻读现象。
  • 读已提交(Read committed),一个事务只能读取到其他事务中做出操作且已经做出提交的数据。会出现不可重复度,幻读现象。
  • 可重复读(Repeatable read),同一个事务内多次查询的数据保持一致。会出现幻读
  • 串行化(Serializable )是高的隔离级别,它求在选定对象上的读锁和写锁保持直到事务结束后才能释放,所以能防住上诉所有问题,但因为是串行化的,所以效率较低.

3、幻读、不可重复读、脏读

脏读:当一个事务读取到其他事务还未提交的数据,因为未提交的数据,不一定是最终有效的数据。所以我们称为读到脏数据了。也就是脏读。

不可重复读:一个事务A读取数据之后,另外一个事务B将此数据修改,此时事务A再次查询,发现数据不一样了。这就是不可重复读。也可以叫做幻读。

幻读:又叫"幻象读",是''不可重复读''的一种特殊场景:当事务1两次执行''SELECT ... WHERE''检索一定范围内数据的操作中间,事务2在这个表中创建了(如[[INSERT]])了一行新数据,这条新数据正好满足事务1的“WHERE”子句。

注:可能有点绕,一般情况下,“不可重复读”和“幻读”大致的意思相同。只不过不可重复度是在数据行上发生的,也就是发生了update操作,再去读取这条数据,出现不可重复读。而幻读是在数据表上发生的,也就是发生了insert与delete操作。再去读取这张表,出现数据条目或者行数(记录数)不一样。出现了幻觉一样。

4、MVCC(Multiversion Concurrency Control)多版本并发控制

数据库用于处理读写冲突的一种手段,目的在于提交数据库高并发场景下的吞吐性能。

版本链:

对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们

创建的表中有主键或者非NULL唯一键时都不会包含row_id列):

trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。

roll_pointer:每次对某条记录进行改动时,这个隐藏列会存一个指针,可以通过这个指针找到该记

录修改前的信息。

比如说现在有这样一张表:t

ID

Name

1

小李

我们先假设新增这条记录的事务ID为80,那么此时此刻这条记录的版本链表如下图(因为是新增,所以这条版本链对应的roll_pointer是空):

假如现在有两个事务ID分别为100、200,对这条记录进行update操作,具体走向流程如下:

贴心小课堂:

两个事务中不能交叉更新同一条记录哦?第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。

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

对这条记录每次更新后,都会将旧记录放入到undo日志中,就算是该记录的一个历史版本,随着更新次数的一次次增加,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为【版本链】,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id,这个ID(事务ID)非常重要,后续事务的隔离级别实现原理都是围绕这个ID(事务ID)来的。

ReadView

对于使用【读未提交READ_UNCOMMITTED】这种隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用【串行化SERIALIZABLE】隔离级别的事务来说,使用加锁的方式来访问记录。对于使用【读已提交READ COMMITTED】和【可重复读REPRATABLE_READ】隔离级别的事务来说,就需要用到我们上边所说的【版本链】了,核心的问题就是:我们需要判断版本链中的数据,哪个版本是当前事务可见的。所以设计MySQL官方提出了一个ReadView的概念,这个ReadView中主要包含当前MySQL中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids(一个数组)。这样在我们访问某一条记录时,只需要按照下边的步骤判断记录的某个版本是否可见(官方设计规则哦):

  • 如果被访问版本的trx_id属性值小于m_ids列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见的话,那就顺着版本链继续去找下一个版本的数据记录,依然按照我们上边所说的步骤判断数据是否可见,依此类推,一直到版本链中的最后一个版本数据,如果最后一个版本的数据我也不可见的话,那么也就意味着该条记录对该事务不可见,查询结果就不包含该记录。

在MySQL当中,READ COMMITTED(读已提交)和REPEATABLE READ(可重复读)隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同,我们来具体举例看一下喽。

按照上面我们画的版本链,来具体分析一下,这个版本链是怎么一步步生成的,以及我们查询的时候,MySQL是怎么来通过版本链决定数据我们是否可读(可见)的。

--[1]--【READ COMMITTED --- 每次读取数据前都生成一个ReadView】

假设说现在系统里有一个id为100的事务在执行:

# Transaction 100

BEGIN;

 

UPDATE t SET name = '小B' WHERE id = 1;

 

UPDATE t SET name = '小C' WHERE id = 1;

# 注意哦:我们这个事务,我并没有提交。没有commit指令哦

# Transaction 200

BEGIN;

 

# 更新了一些别的表的记录

...

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

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

千万注意,我上面事务100,还没提交哦,我可没有执行commit指令。

假设现在有一个使用READ COMMITTED(读已提交)隔离级别的事务开始执行:

# 使用READ COMMITTED隔离级别的事务(读已提交)

BEGIN;

# SELECT1:Transaction 100、200未提交

SELECT * FROM t WHERE id = 1; # 得到的列name的值为'小A'

这个SELECT1的执行流程如下:

  • 在执行SELECT语句时会首先生成一个ReadView,ReadView的m_ids数组列表的内容就是[100,200]。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是'小C',该版本的trx_id值为100,在m_ids列表内,所以不符合我们的可见性要求,根据roll_pointer跳到下一个版本。
  • 下一个版本的列name的内容是'小B',该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name的内容是'小A',该版本的trx_id值为80,小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为'小A'的记录。

之后,我们把事务id为100的这个事务提交一下,如下:

# Transaction 100

BEGIN;

 

UPDATE t SET name = '小B' WHERE id = 1;

 

UPDATE t SET name = '小C' WHERE id = 1;

 

COMMIT;    //提交了哦

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

# Transaction 200

BEGIN;

 

# 更新了一些别的表的记录

...

 

UPDATE t SET name = '小D' WHERE id = 1;

 

UPDATE t SET name = '小F' WHERE id = 1;

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

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

# 使用READ COMMITTED隔离级别的事务

BEGIN;

 

# SELECT1:Transaction 100、200均未提交的时候执行的查询

SELECT * FROM t WHERE id = 1; # 得到的列name的值为'小A'

 

# SELECT2:Transaction 100提交,Transaction 200未提交的时候执行的查询

SELECT * FROM t WHERE id = 1; # 得到的列name的值为'小C'

这个SELECT2的执行过程如下:

  • 在执行SELECT语句时会先生成一个ReadView,ReadView的m_ids列表的内容就是[200](事务id为100的那个事务已经提交了,所以生成快照时就没有它了)。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是'小F',该版本的trx_id值为200,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
  • 下一个版本的列name的内容是'小D',该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name的内容是'小C',该版本的trx_id值为100,比m_ids列表中最小的事务id200还要小,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为'小C'的记录。

以此类推,如果之后事务id为200的记录也提交了,再此在使用READ COMMITTED隔离级别的事务中查询表t中id值为1的记录时,得到的结果就是'小F'了,具体流程我们就不分析了。总结一下就是:使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。

说完了隔离级别为【读已提交】不知道你理解了没有?如果不理解,烦请联系我,我们一起进行探讨。

接下来我们就来看一下当事务隔离级别为【可重复读】的时候,MVCC是如何控制数据可见性的。

--[2]--【REPEATABLE READ ---在第一次读取数据时生成一个ReadView】

对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。我们还是用例子看一下是什么效果。

比方说现在系统里有两个id分别为100、200的事务在执行:

# Transaction 100

BEGIN;

 

UPDATE t SET name = '小B' WHERE id = 1;

 

UPDATE t SET name = '小C' WHERE id = 1;

# Transaction 200

BEGIN;

 

# 更新了一些别的表的记录

...

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

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

# 使用REPEATABLE READ隔离级别的事务

BEGIN;

 

# SELECT1:Transaction 100、200未提交

SELECT * FROM t WHERE id = 1; # 得到的列name的值为'小A'

这个SELECT1的执行过程如下:

  • 在执行SELECT语句时会先生成一个ReadView,ReadView的m_ids列表的内容就是[100, 200]。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是'小C',该版本的trx_id值为100,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
  • 下一个版本的列name的内容是'小B',该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name的内容是'小A',该版本的trx_id值为80,小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为'小A'的记录。

之后,我们把事务id为100的事务提交一下,就像这样:

# Transaction 100

BEGIN;

 

UPDATE t SET name = '小B' WHERE id = 1;

 

UPDATE t SET name = '小C' WHERE id = 1;

 

COMMIT;

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

# Transaction 200

BEGIN;

 

# 更新了一些别的表的记录

...

 

UPDATE t SET name = '小D' WHERE id = 1;

 

UPDATE t SET name = '小F' WHERE id = 1;

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

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

# 使用REPEATABLE READ隔离级别的事务

BEGIN;

 

# SELECT1:Transaction 100、200均未提交

SELECT * FROM t WHERE id = 1; # 得到的列name的值为'小A'

 

# SELECT2:Transaction 100提交,Transaction 200未提交

SELECT * FROM t WHERE id = 1; # 得到的列name的值仍为'小A'

这个SELECT2的执行过程如下:

  • 因为之前已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView中的m_ids列表就是[100, 200]。
  • 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是'小F',该版本的trx_id值为200,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
  • 下一个版本的列name的内容是'小D',该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本。
  • 下一个版本的列name的内容是'小C',该版本的trx_id值为100,而m_ids列表中是包含值为100的事务id的,所以该版本也不符合要求,同理下一个列name的内容是'小B'的版本也不符合要求。继续跳到下一个版本。
  • 下一个版本的列name的内容是'小A',该版本的trx_id值为80,80小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列name为'小A'的记录。

也就是说我们的两次SELECT查询得到的数据结果是一样(重复)的,列name值都是'小A',这就是【可重复读的含义。如果我们之后再把事务id为200的记录也提交了,之后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个id为1的记录,得到的结果还是'小A'。

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就好了。


回到我们的标题:

MySQL到底能不能解决幻读?或者说MySQL是如何解决幻读的?

现在你明白了吗?

欢迎一起讨论



相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
7天前
|
存储 关系型数据库 MySQL
MySQL MVCC全面解读:掌握并发控制的核心机制
【10月更文挑战第15天】 在数据库管理系统中,MySQL的InnoDB存储引擎采用了一种称为MVCC(Multi-Version Concurrency Control,多版本并发控制)的技术来处理事务的并发访问。MVCC不仅提高了数据库的并发性能,还保证了事务的隔离性。本文将深入探讨MySQL中的MVCC机制,为你在面试中遇到的相关问题提供全面的解答。
34 2
|
13天前
|
存储 SQL 关系型数据库
MySQL的事务隔离级别
【10月更文挑战第17天】MySQL的事务隔离级别
89 43
|
10天前
|
存储 关系型数据库 MySQL
MySQL主从复制原理和使用
本文介绍了MySQL主从复制的基本概念、原理及其实现方法,详细讲解了一主两从的架构设计,以及三种常见的复制模式(全同步、异步、半同步)的特点与适用场景。此外,文章还提供了Spring Boot环境下配置主从复制的具体代码示例,包括数据源配置、上下文切换、路由实现及切面编程等内容,帮助读者理解如何在实际项目中实现数据库的读写分离。
MySQL主从复制原理和使用
|
27天前
|
缓存 算法 关系型数据库
Mysql(3)—数据库相关概念及工作原理
数据库是一个以某种有组织的方式存储的数据集合。它通常包括一个或多个不同的主题领域或用途的数据表。
45 5
Mysql(3)—数据库相关概念及工作原理
|
7天前
|
存储 关系型数据库 MySQL
MySQL MVCC深度解析:掌握并发控制的艺术
【10月更文挑战第23天】 在数据库领域,MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种重要的并发控制机制,它允许多个事务并发执行而不产生冲突。MySQL作为广泛使用的数据库系统,其InnoDB存储引擎就采用了MVCC来处理事务。本文将深入探讨MySQL中的MVCC机制,帮助你在面试中自信应对相关问题。
19 3
|
26天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1601 14
|
10天前
|
SQL 关系型数据库 MySQL
Mysql中搭建主从复制原理和配置
主从复制在数据库管理中广泛应用,主要优点包括提高性能、实现高可用性、数据备份及灾难恢复。通过读写分离、从服务器接管、实时备份和地理分布等机制,有效增强系统的稳定性和数据安全性。主从复制涉及I/O线程和SQL线程,前者负责日志传输,后者负责日志应用,确保数据同步。配置过程中需开启二进制日志、设置唯一服务器ID,并创建复制用户,通过CHANGE MASTER TO命令配置从服务器连接主服务器,实现数据同步。实验部分展示了如何在两台CentOS 7服务器上配置MySQL 5.7主从复制,包括关闭防火墙、配置静态IP、设置域名解析、配置主从服务器、启动复制及验证同步效果。
Mysql中搭建主从复制原理和配置
|
19天前
|
SQL 关系型数据库 MySQL
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
尼恩,一位40岁的资深架构师,通过其丰富的经验和深厚的技術功底,为众多读者提供了宝贵的面试指导和技术分享。在他的读者交流群中,许多小伙伴获得了来自一线互联网企业的面试机会,并成功应对了诸如事务ACID特性实现、MVCC等相关面试题。尼恩特别整理了这些常见面试题的系统化解答,形成了《MVCC 学习圣经:一次穿透MYSQL MVCC》PDF文档,旨在帮助大家在面试中展示出扎实的技术功底,提高面试成功率。此外,他还编写了《尼恩Java面试宝典》等资料,涵盖了大量面试题和答案,帮助读者全面提升技术面试的表现。这些资料不仅内容详实,而且持续更新,是求职者备战技术面试的宝贵资源。
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
|
30天前
|
存储 SQL 关系型数据库
mysql中主键索引和联合索引的原理与区别
本文详细介绍了MySQL中的主键索引和联合索引原理及其区别。主键索引按主键值排序,叶节点仅存储数据区,而索引页则存储索引和指向数据域的指针。联合索引由多个字段组成,遵循最左前缀原则,可提高查询效率。文章还探讨了索引扫描原理、索引失效情况及设计原则,并对比了InnoDB与MyISAM存储引擎中聚簇索引和非聚簇索引的特点。对于优化MySQL性能具有参考价值。
|
20天前
|
存储 关系型数据库 MySQL
Mysql(4)—数据库索引
数据库索引是用于提高数据检索效率的数据结构,类似于书籍中的索引。它允许用户快速找到数据,而无需扫描整个表。MySQL中的索引可以显著提升查询速度,使数据库操作更加高效。索引的发展经历了从无索引、简单索引到B-树、哈希索引、位图索引、全文索引等多个阶段。
55 3
Mysql(4)—数据库索引