1、问题引入
假设我今天买彩票中了五百万元,正高兴着准备往家人账号进行转账的时候,此时这个转账动作分为下面几步:
- 从数据库中读取我的余额
- 将我的余额减去转账的金额
- 将我修改后的余额更新到数据库里
- 从数据库读取对方的余额
- 将对方的余额加上转账的金额
- 将对方修改后的余额更新到数据库中
可以看到这个转账的过程涉及到了两次修改数据库的操作。
假设在执行第三步骤之后,服务器突然掉电了,就会发生一个蛋疼的事情,我的账户扣了一百万,但是钱并没有到家人的账户上,也就是说这一百万消失了!
所以我们在日常生活中必须解决这个问题,不然秩序就乱了!要解决这个问题,就要保证转账业务里的所有数据库的操作是完整的、不可分割的,要么全部执行成功 ,要么全部失败,不允许出现中间状态的数据!
数据库中的事务(Transaction)就能达到这样的效果。
我们在转账操作前先开启事务,等所有数据库操作执行完成后,才提交事务,对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,如果中途发生发生中断或错误,那么该事务期间对数据库所做的修改将会被回滚到没执行该事务之前的状态。
2、什么是事务
事务是由一组 DML 语句组成的,这些语句在逻辑上存在相关性,这一组 DML 语句要么全部成功,要么全部失败,是一个整体。 MySQL 提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。假设一种场景:你毕业了,学校的教务系统后台 MySQL 中,不在需要你的数据,要删除你的所有信息(一般不会),那么要删除你的基本信息(姓名,电话,籍贯等)的同时,也删除和你有关的其他信息,比如:你的各科成绩,你在校表现,甚至你在论坛发过的文章等。这样,就需要多条 MySQL 语句构成,那么所有这些操作合起来,就构成了一个事务。
正如我们上面所说,一个 MySQL 数据库,可不止你一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL ,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题,就是数据不安全问题了。甚至,因为事务由多条 SQL 构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢❓❓❓
所以,一个完整的事务,绝对不是简单的 sql 集合,还需要满足如下四个属性:
-
原子性(
Atomicity):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。- 就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。
-
一致性(
Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏,保持着一致性状态。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。- 比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元),那就错误了。
-
隔离性(
Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时会相互干扰,所以每个事务都要有一个完整的数据空间,这样子对其他并发事务来说才是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。- 事务隔离分为不同级别,这些我们后面都会接触到,包括读未提交(
Readuncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)四个等级。
- 事务隔离分为不同级别,这些我们后面都会接触到,包括读未提交(
-
持久性(
Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
上面四个属性,简称为 ACID 。
事务是由 MySQL 的引擎来实现的,我们常见的 InnoDB 引擎它是支持事务的。不过并不是所有的引擎都能支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,也正是这样,所以大多数 MySQL 的引擎都是用 InnoDB。
那 InnoDB 引擎通过什么技术来保证事务的这四个特性的呢❓❓❓
- 持久性是通过
redo log(重做日志)来保证的 - 原子性是通过
undo log(回滚日志)来保证的 - 隔离性是通过
MVCC(多版本并发控制)和锁机制来保证的 - 而一致性,是通过持久性 + 原子性 + 隔离性共同来保证
3、为什么会出现事务
事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题,功能就相当于一个系统调用。可以想象一下当我们使用事务时,要么不是提交,就是回滚,我们也不会去考虑网络异常了、服务器宕机了、同时更改一个数据会怎么样对不对!
因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。
- 备注:我们后面把
MySQL中的一行信息,称为一行记录。
4、事务的版本支持
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务,而 MyISAM 不支持。
我们可以查看一下当前 MySQL 中的存储引擎的信息:

5、事务的提交方式
在 MySQL 中,自动提交(Auto Commit)和手动提交(Manual Commit)是两种不同的事务提交方式。
- 自动提交:默认情况下,
MySQL处于自动提交模式。这意味着每个SQL语句都会被立即提交到数据库,无需手动执行提交操作。当执行一条SQL语句后,该语句的结果就会立即生效,且无法回滚。自动提交模式适用于简单的操作,如查询和更新单个记录。 - 手动提交:手动提交模式需要显式地执行提交操作,以将一组相关的
SQL语句作为一个事务进行提交。在手动提交模式下,多个SQL语句可以作为一个原子操作进行提交,要么全部成功提交,要么全部回滚。手动提交模式适用于需要保证数据的一致性和完整性的复杂操作,如插入、更新、删除多个相关记录。
也就是说,我们之前的操作,都是自动提交的方式,而现在我们要进行事务管理,那么使用的方式就是手动提交方式,这样子的话我们才能在事务中使用多条 SQL 语句并且同时将它们更新到数据库中!
手动提交模式的使用步骤大概如下所示:
- 执行
set autocommit=0命令,将自动提交模式关闭(其实不关闭也行,因为启动事务后,mysql就会自动切换为手动提交模式,这个我们下面做实验会看到效果!) - 开始一个事务,使用
start transaction或begin命令 - 执行一系列
SQL语句,包括插入、更新、删除等操作 - 如果所有操作都成功,执行
commit命令提交事务 - 如果出现错误或需要回滚,执行
rollback命令回滚事务
手动提交模式允许在一组相关操作中进行回滚,以保证数据的一致性。同时,手动提交模式也可以提高性能,因为可以将多个操作作为一个事务进行提交,减少了提交的次数。
需要注意的是,执行「开始事务」命令,并不意味着启动了事务。在
MySQL有两种开启事务的命令,分别是:
- 第一种:
begin/start transaction命令;- 第二种:
start transaction with consistent snapshot命令;这两种开启事务的命令,事务的启动时机是不同的:
- 执行了
begin/start transaction命令后,并不代表事务启动了。只有在执行了增删查改操作的SQL语句,才会启动事务。- 执行了
start transaction with consistent snapshot命令,就会马上启动事务。
查看当前提交方式如下所示:
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec) 我们可以使用 set 关键字来改变 MySQL 的自动提交模式:
mysql> set autocommit=0; # 表示禁止自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)
mysql> set autocommit=1; # 表示开启自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)Ⅱ. 事务常见操作方式
1、准备工作
首先,为了便于演示,我们将 mysql 的默认隔离级别设置成读未提交:(具体操作我们后面专门会讲,现在以使用为主)
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ | # 表示可重复读级别
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> set global transaction isolation level read uncommitted; #将mysql的默认隔离级别设置成读未提交
Query OK, 0 rows affected (0.00 sec)
mysql> quit; # 需要重启终端才能生效
Bye
[root@VM-8-7-centos ~]# mysql -uroot -p
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED | # 更改成功
+------------------+
1 row in set, 1 warning (0.00 sec) 然后创建一个测试表:
create table if not exists account(
id int primary key,
name varchar(50) not null default '',
blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8; 接下来的测试,我们会用到两个终端,这里我们就用 xshell 直接开启两个会话去连接 mysql。如果要查看当前有什么用户在连接 mysql 的话,可以使用我们之前讲过的 show processlist 进行查看:
mysql> show processlist;
+----+------+-----------+------+---------+------+----------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------+------+---------+------+----------+------------------+
| 3 | root | localhost | test | Query | 0 | starting | show processlist |
| 4 | root | localhost | NULL | Sleep | 13 | | NULL |
+----+------+-----------+------+---------+------+----------+------------------+
2 rows in set (0.00 sec) 我们先来介绍一下几个下面会用到的操作语句:
-- 开始一个事务的操作,有两种方式,更推荐begin
start transaction;
begin;
-- 创建一个保存点
savepoint 保存点名;
-- 回滚操作
rollback [to 保存点];
2、正常情况:证明事务的开始与回滚
首先我们依次插入数据,然后查看另一端中该表的数据是否变化:
可以看到,一旦我们回滚之后,数据就全回滚不见了!
下面我们来改变一下时机,先进行提交,再回滚,看看效果:
启动事务后,事务将一直处于活动状态,直到显式地提交(commit),表示确认将之前在事务中所做的更改永久保存到数据库中,事务才会成功结束。在提交之后,数据库将释放事务所占用的资源,并且对其他会话可见。
此外,回滚操作并不会结束事务,而是将事务恢复到一个之前的状态,这样子以便继续在同一个事务中执行其他操作。
下面我们再试试保存点的操作:
3、异常情况一:证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
4、异常情况二:证明commit之后客户端崩溃,MySQL数据不会在受影响,已经持久化
5、异常情况三:证明启动事务会自动更改提交方式为手动提交,不会受MySQL是否自动提交的影响
上面我们的操作都是基于自动提交的设置,那下面我们来试试看设置为手动提交,看看什么效果:
可以看到,和在基于自动提交的设置情况下,是没有差别的,下面我们再试试看手动提交后再奔溃的情况:
从这其实我们就可以总结出来了,只要我们进入了事务模式,那么此时就已经是手动提交的状态了,不管之前我们设置的是自动提交,但是其实本质上还是手动提交,因为只要我们启动了事务,mysql 就会帮我们自动切换为手动提交的模式!
6、不启动事务情况下,自动提交和手动提交的区别
此外我们可以稍微测试一下,如果不启动事务的话,那么自动提交和手动提交的区别是什么样的,如下所示:
首先是自动提交的情况:
然后是手动提交的情况:
7、结论
- 从上面的例子,我们能看到事务本身的原子性(
rollback)、持久性(commit) - 只要输入
begin或者start transaction,事务便必须要通过手动提交,最终才能持久化,这与是否设置了set autocommit无关 - 如果一个事务被提交了(
commit),则不可以回退(rollback) - 在事务还没有提交之前,如果没有设置保存点,也可以回滚,但是只能回滚到事务的开始
- 事务可以手动回滚,但是当出现异常情况如客户端崩溃的时候,
MySQL会自动回滚 - 在自动提交情况下,对于
InnoDB每一条SQL语言都默认封装成事务,进行自动提交(select有特殊情况,因为MySQL有MVCC,后面会讲)
Ⅲ. 并行事务引发的问题
MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。
那么在并发处理多个事务的时候,就可能出现脏读、不可重复读、幻读的问题。
接下来,通过举例子说明这些问题是如何发生的。
1、脏读dirty read
脏读是指一个事务读取了另一个事务尚未提交的数据。当一个事务读取了另一个事务的未提交数据时,如果另一个事务最终回滚,则读取到的数据实际上是无效的。脏读可能导致不一致的结果。
假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库中读取利刃的余额数据,然后再执行更新操作,如果此时事务 A 还没有提交事务,而此时正好事务 B 也从数据库中读取利刃的余额数据,那么事务 B 读取到的余额数据是刚才事务 A 更新后的数据,即使没有提交事务。

因为事务 A 是还没提交事务的,也就是它随时可能发生回滚操作,如果事务 A 发生了回滚,那么事务 B 刚才得到的数据就是过期的数据,这种现象就被称为脏读。
2、不可重复读non-repeatable read
不可重复读是指在一个事务内,多次读取同一数据时,得到的结果不一致。这是因为在事务执行期间,其他事务可能修改了被读取的数据。不可重复读可能导致数据的不一致性。
假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库中读取利刃的余额数据,然后继续执行代码逻辑处理,在这过程中如果事务 B 更新了这条数据,并提交了事务,那么当事务 A 再次读取该数据时,就会发现前后两次读到的数据是不一致的,这种现象就被称为不可重复读。

3、幻读phantom read
幻读是指在一个事务内,多次执行同一个查询时,得到的结果集不一致。当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 select 执行了两次,但第二次返回了第一次没有返回的行,则该行是 “幻像” 行。
这是因为在事务执行期间,其他事务可能插入或删除了符合查询条件的数据。幻读可能导致查询结果的不一致性。
需要注意的是,MySQL 5.7中解决了幻读(phantom read)现象。在此之前的版本中,MySQL 使用的是可重复读(repeatable read)的隔离级别,但仍然存在幻读问题。在 MySQL 5.7 中,引入了新的隔离级别"可序列化"(serializable),该隔离级别解决了幻读问题!
虽然在可重复读隔离级别中引入了 MVCC 和next-key lock两种解决方案(后面会讲到),但其实本质还是避免不了幻读的问题,只是在很大程度上避免了幻读问题,要想真正解决幻读问题,还是需要使用可序列化隔离级别!
幻读和不可重复读其实是有区别的,如下所示:
- 不可重复读 主要涉及到并发事务中的更新操作,而 幻读 主要涉及到并发事务中的插入和删除操作。
- 不可重复读 关注的是查询结果的稳定性,而 幻读 关注的是查询结果的一致性。
- 这里的稳定性和一致性也是有区别和关联的:
- 稳定性 是指系统在面对各种异常情况时的表现。一个稳定的系统能够在面对负载增加、网络故障、节点故障等情况下,仍然能够正常运行,并且不会导致数据丢失或不一致。
- 一致性 是指在分布式系统中的多个节点之间,对于相同的操作序列,最终达到相同的状态。换句话说,无论在哪个节点上执行操作,最终系统都会保持一致的状态。一致性是分布式系统中的一个重要属性,确保数据的正确性和可靠性。
- 稳定性 关注的是系统的可靠性和鲁棒性,而 一致性 关注的是数据的正确性和一致性。
- 一个系统可以是一致的但不稳定,也可以是稳定的但不一致。
假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库查询账户余额大于 100 万的记录,发现共有 5 条,然后事务 B 也按相同的搜索条件也是查询出了 5 条记录。
接下来,事务 A 插入了一条余额超过 100 万的账号,并提交了事务,此时数据库超过 100 万余额的账号个数就变为 6。

然后事务 B 再次查询账户余额大于 100 万的记录,此时查询到的记录数量有 6 条,发现和前一次读到的记录数量不一样了,就感觉发生了幻觉一样,这种现象就被称为幻读。
4、总结
这三个现象的严重性排序如下:
简单地说,脏读是读取了未提交的数据,不可重复读是读取同一数据时结果不一致,幻读是查询结果集不一致。这些问题都是由于并发事务引起的,解决方法通常是通过锁机制或隔离级别来保证数据的一致性。
Ⅳ. 事务的隔离
1、隔离性的理解
MySQL 服务可能会同时被多个客户端进程或者线程访问,以事务方式执行操作,而一个事务可能由多条 SQL 构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
但是毕竟所有事务都有执行过程,时间长度不一,那么在多个事务各自执行多个 SQL 的时候,就有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据的时候,此时增删查改的操作就会影响到其它事务。
就如同一些导师说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你导师不关心。此时你的学习对你导师来讲,就是原子的。而在你学习的过程中,是很容易受别人干扰的,此时,就需要将你的学习环境与其它环境隔离开,保证你的学习环境是健康的。
在数据库中,为了保证事务执行过程中尽量不受干扰,引入了一个重要的特性:隔离性。
在数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别。
2、隔离级别
- 读未提交(
read uncommitted)- 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。
- 读提交(
read committed)- 该隔离级别是大多数数据库的默认的隔离级别(不是
MySQL默认的隔离级别)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次select, 可能得到不同的结果。
- 该隔离级别是大多数数据库的默认的隔离级别(不是
- 可重复读(
repeatable read)- 这是
MySQL默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是最终隔离级别存在幻读问题。
- 这是
- 串行化(
serializable)- 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争(这种隔离级别太极端,导致并发效率不高,实际生产基本不使用)
按隔离级别高低排序如下:

针对不同的隔离级别,并发事务时可能发生的现象也会不同:
那隔离级别是如何实现的呢❓❓❓
其实隔离基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有表锁、行锁、读锁、写锁、间隙锁(GAP)、**Next-Key锁(GAP+**行锁)等等。不过,我们目前先有这个认识就行,先关注上层的使用,后面我们再来接触这些素各种各样的锁!
3、查看和修改隔离级别
① 全局隔离级别
默认情况下,MySQL 使用全局的隔离级别,这意味着所有会话都将使用相同的隔离级别。
mysql> select @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| READ-UNCOMMITTED | # 默认是读未提交级别
+-----------------------+
1 row in set, 1 warning (0.00 sec)② 当前会话隔离级别
一共有两种方式,它们是等价的,只不过第二种写法是第一种写法的缩写而已!
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED |
+------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)③ 设置隔离级别
set [session | global] transaction isolation level
{read uncommitted | read committed | repeatable read | serializable}; 其中可以选择是修改当前会话的隔离级别 session 还是修改全局隔离级别 global,然后通过隔离级别选项选择即可!比较简单,这里就不演示了!
4、读未提交read uncommitted
在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等。
上面在做实验的时候,为了有效果,我们也是使用了该隔离等级,现在我们再测试一遍:
可以看到,此时这种并发处理的时候,一端修改的数据被另一端直接就能看到了,这种现象就是脏读,这我们在上面介绍过!几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用!
5、读提交read committed
该隔离级别是大多数数据库的默认的隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读以及幻读问题,即一个事务执行时,如果进行多次的增删查改, 可能得到不同的结果。
下面我们来做测试,首先将全局隔离等级改为读提交,然后进行测试:
上面的情况虽然避免了脏读问题,但是此时还在当前事务中,并未 commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中),读取到了不同的值,这种现象叫做不可重复读!
有人可能会问,不可重复读这种问题,有啥错误吗❓❓❓
其实是有的。举个例子,比如一个公司在发年终奖品的时候,可能按照当年的工资来划分区间进行不同奖品的发放;此时员工小王本来工资是三千块钱,只能领个水杯,但是小王就在发年终奖品前去找了老板升工资,老板也同意了升工资,将小王工资升到了四千五,这个时候小王的奖品应该对应的是一部手机,然后老板就吩咐小李去更新一下小王的工资数据。而我们知道,此时负责划分区间来发奖品的小瓦需要对公司的数据做划分,那么也肯定是通过事务来执行多条筛选语句划分区间。此时问题来了,当小瓦在事务中执行 select 语句筛选工资为 [3000, 4000] 的员工的时候,此时因为小李还没更新小王的数据,那么小王还是 3000 块钱,理所应当的分发一个水杯,于是就被记录下来了发放一个水杯;此时刚好小李更新了小王的数据,那么小瓦在执行下一条 select 语句筛选 [4000, 5000] 工资的员工的时候,发现小王又出现在了名单里面,于是小王就被统计发送了两个奖品,这是非常不合理的,也就是因为不可重复读的问题,才会导致小瓦在执行事务的时候做不到数据的隔离性而导致这种问题的发生!
6、可重复读repeatable read
这是 mysql 的默认隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是最终隔离级别存在幻读问题(后面我们会来专门讲一下 mysql 中一些尽可能避免幻读问题的解决方案)。
话不多说,来做测试。首先将全局隔离等级修改未可重复读,再进行下面的读写操作:

在终端**2进行多次查看,发现终端1在对应事务中 insert 的数据,在终端2**的事务周期中,也没有什么影响,也符合可重复的特点。
但其实一般的数据库在「可重复读」情况的时候,无法屏蔽其他事务 insert 的数据,这是为什么❓❓❓
因为隔离性实现是对数据加锁完成的,而 insert 待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题。这会造成虽然大部分内容是可重复读的,但是 insert 的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象叫做幻读(phantom read)。
而 MySQL 的「可重复读」隔离级别,引入了 MVCC 和 next-key lock,在很大程度上避免了幻读问题,但是却无法完全解决幻读问题,要想解决幻读问题,还得靠下面要讲的「串行化」隔离级别。(但是一般我们不会使用这种极端隔离级别,效率太低)
7、串行化serializable
这是事务的最高隔离级别,它通过强制事务排序,使事务之间不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争。因为这种隔离级别太极端,导致并发效率不高,实际生产基本不使用
串行化的演示如果用图片不太好展示效果,这里直接讲解一下其大概过程:
首先事务 B 在执行将余额 100 万修改为 200 万时,由于此前事务 A 执行了读操作,这样就发生了读写冲突,于是就会被锁住,直到事务 A 提交后,事务 B 才可以继续执行,所以从 A 的角度看,余额 V1、V2 的值是 100 万,余额 V3 的值是 **200**万。
8、隔离级别的小总结
- 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点
- 不可重复读的重点是修改:同样的条件,你读取过的数据,再次读取出来发现值不一样了
- 幻读的重点在于新增和删除:同样的条件,第**
1次和第2**次读出来的记录数不一样 mysql默认的隔离级别是可重复读,一般情况下不要修改- 事务也有长短事务这样的概念,而事务间互相影响,指的是事务在并行执行的时候,即在没有
commit之前,影响会比较大

推荐阅读:
- https://www.jianshu.com/p/398d788e1083
- https://tech.meituan.com/2014/08/20/innodb-lock.html
- /notes-img/ca0a3933_9177978.html.png
9、一致性的理解
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。
其实一致性和用户的业务逻辑强相关,一般 MySQL 提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是说,一致性是由程序员决定的。
而在技术上,一致性是通过「原子性+隔离性+持久性」来保证的!
Ⅴ. MVCC多版本并发控制机制
1、数据库并发的场景
- 读-读 :不存在任何问题,也不需要并发控制
- 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读的情况
- 写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失问题
在数据库中,读写模式更为常见,所以下面我们会以读-写模式进行切入讲解!
在此之前,我们先谈一些共识:
- 每个事务都要有自己的事务
ID,这样子的话mysql可以根据事务ID的大小,来决定事务到来的先后顺序,一般都是ID越小,说明事务越早产生!mysqld可能会面临处理多个事务的情况,所以事务也需要有自己的生命周期,而mysqld要对这些事务进行管理,就得先描述,再组织,也就是说事务也有自己的一套结构体!
2、实现MVCC的三个前提
首先,MVCC(Multi-Version Concurrency Control)是一种并发控制机制,用于在数据库系统中处理并发访问和修改数据的问题,其允许多个事务同时读取和修改数据库,而不会相互干扰或产生冲突。
在 MVCC 中,每个事务在开始时会获得一个唯一的事务 ID,并且每个数据行都会有一个版本号或时间戳。当事务对数据进行修改时,会创建一个新的数据版本,并将事务 ID 和版本号关联起来。这样,其他事务仍然可以读取旧版本的数据,而不会受到正在进行的修改的影响。
所以 MVCC 可以为数据库解决以下问题:
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。
- 同时还可以解决脏读、不可重复读、幻读等事务隔离问题,但不能解决更新丢失问题。
而要实现上述内容,其实我们就要引入三个前提知识:
- 记录中的隐藏字段
undo log回滚日志Read View读视图
下面我们会分别来介绍上述知识!
① 记录中的隐藏字段
首先先强调一点,我们把 MySQL 中的一行信息,称为一行记录!那么一行记录中的隐藏字段,其实就是我们平时查表的时候看不到的字段!
分别是以下四个隐藏字段(前两个对于 MVCC 来说比较重要):
-
DB_TRX_ID- 占
6字节,表示最近一次改动(修改/插入)该记录的事务ID。 - 当一个事务对某条聚簇索引记录进行改动(插入/修改)时,就会把该事务的事务
id记录在DB_TRX_ID隐藏列里; - 通过
DB_TRX_ID可以知道该记录是被哪个事务修改的。
- 占
-
DB_ROLL_PTR- 占
7字节,是一个回滚指针,指向这条记录的上一个版本,而这些历史版本一般放在undo log中。 - 每次对某条聚簇索引记录进行改动时,
InnoDB都会将旧版本的记录写入到undo log中,然后这个隐藏列是个指针(不要把指针死板的理解为指针类型),指向每一个旧版本记录,于是就可以通过它找到修改前的记录。 - 通过
DB_ROLL_PTR指针可以将这些回滚日志串成一个链表,这个链表就被称为版本链。
- 占
-
DB_ROW_ID- 占
6字节,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引,也就是构建一棵 **B+**树。
- 占
-
实际还有一个删除
flag隐藏字段- 其用来记录被更新或删除的状态,也就是说我们平时的删除并不代表真的删除了,只是内存中该记录的
flag字段被设置为删除,只有当数据库进行持久化刷新之后,数据才会真的从磁盘文件中删除!
- 其用来记录被更新或删除的状态,也就是说我们平时的删除并不代表真的删除了,只是内存中该记录的
举个例子,假设有下面一张表和一行记录:
mysql> create table if not exists student(
name varchar(11) not null,
age int not null
);
mysql> insert into student (name, age) values ('张三', 28);
Query OK, 1 row affected (0.05 sec)
mysql> select * from student;
+--------+-----+
| name | age |
+--------+-----+
| 张三 | 28 |
+--------+-----+
1 row in set (0.00 sec) 此时加上隐藏字段后的记录是这样子的:

因为我们目前并不知道创建该记录的事务**ID**、隐式主键,我们就分别默认设置成 null 和 1。
② undo日志
我们在执行执行一条“增删改”语句的时候,虽然没有输入 begin 开启事务和 commit 提交事务,但是 mysql 会 隐式开启事务 来执行 “增删改” 语句(这我们前面都是实验过的),执行完就自动提交事务的,这样就保证了执行完 “增删改” 语句后,我们可以及时在数据库表看到 “增删改” 的结果。
执行一条语句是否自动提交事务,是由 autocommit (自动提交)参数决定的,其默认是开启状态。
所以,执行一条 update 语句也是会使用事务的。
那么,考虑一个问题。一个事务在执行过程中,在还没有提交事务之前,如果 MySQL 发生了崩溃,要怎么回滚到事务之前的数据呢❓❓❓
如果我们每次在事务执行过程中,都记录下回滚时需要的信息到一个日志里,那么在事务执行中途发生了 MySQL 崩溃后,就不用担心无法回滚到事务之前的数据,我们可以通过这个日志回滚到事务之前的数据。
实现这一机制就是 undo log(回滚日志),它记录了事务执行期间对数据所做的修改,以便在需要时可以撤销或回滚这些修改,这保证了事务的 ACID 特性中的原子性(Atomicity)。
其实它的本质就是 Buffer Pool 中的一段内存缓冲区,如下图所示:
undo log 的两大重要作用:
- 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执行了
rollback语句,MySQL可以利用undo log中的历史数据将数据恢复到事务开始之前的状态。 - 实现
MVCC(多版本并发控制)关键因素之一。MVCC是通过ReadView+undo log实现的。undo log为每条记录保存多份历史数据,MySQL在执行快照读(普通select语句)的时候,会根据事务的Read View里的信息,顺着undo log的版本链找到满足其可见性的记录。
每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:
- 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
- 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。
简单地说,在发生回滚时,就读取 undo log 里的数据,然后进行原先操作的相反操作。不同的操作,需要记录的内容也是不同的,所以不同类型的操作产生的 undo log 的格式也是不同的。
undo log是如何刷盘(持久化到磁盘)的❓❓❓
其实 undo 页和数据页的刷盘策略是一样的,都需要通过 redo 页保证持久化。
我们说过,在 buffer pool 中存在有 undo 页,而对 undo 页的修改也都会记录到 redo页。redo 页会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的。
🎏 简单模拟MVCC流程
下面在介绍 read view 之前,我们先利用前两个前提知识,进行简单的模拟 MVCC 的工作流程,这可以帮助我们理解上面的内容!
最开始的记录如下所示:
此时有一个事务 ID 为 10(这里的数据是随意设定的)的事务,对上面创建的的记录进行修改:将张三改成李四。
此时该记录会先被拷贝一份放到 undo log 中进行保存,然后原来的记录作为新版本,将对应字段改成目标内容,然后将 DB_TRX_ID 更新为最近修改记录的事务 ID 也就是 10,而 DB_ROLL_PTR 则指向在 undo log 中的旧版本,也就是上一个版本,更新如下图所示:
此时又来一个事务,ID 为 11,也是进行修改操作,将年龄从 28 改成 38。
还是一样,原纪录会先拷贝一份放到 undo log 中,作为上一个版本,然后对原记录进行修改,将 DB_TRX_ID 改成最近更改该记录的事务 ID,即 11;然后将 DB_ROLL_PTR 改成上一个版本在 undo log 中的位置指针!
这样一来,我们就有了一个基于链表记录的历史版本链,而所谓的回滚,无非就是用历史数据,覆盖当前数据罢了!
而 undo log 中的每一个版本,我们可以称之为一个一个的快照!(就好像原记录被拍照后保存下来的样子!)
🎏 快照读 && 当前读
- 快照读
- 就是读取一个记录的历史版本。读取历史版本的话,是不受加锁限制的,也就是多个事务可以并行读取历史版本!换言之,快照读提高了并发效率,这也是
MVCC的意义所在。 - 针对快照读(普通
select语句),通过MVCC方式解决幻读问题,因为在可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
- 就是读取一个记录的历史版本。读取历史版本的话,是不受加锁限制的,也就是多个事务可以并行读取历史版本!换言之,快照读提高了并发效率,这也是
- 当前读
- 就是读取最新的记录的,都叫做当前读,而
select也有可能是当前读。在多个事务同时删改查的时候,都是当前读,这是要加锁的。如果同时有select语句事务过来的话,并且读取的是最新版本记录(当前读),那么也就需要加锁,这就是串行化。 - 针对当前读(
select ... for update等语句),通过next-key lock(记录锁+间隙锁)方式解决幻读,因为当执行select ... for update语句的时候,会加上next-key lock,如果有其他事务在next-key lock锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
- 就是读取最新的记录的,都叫做当前读,而
那到底是什么决定了事务是执行快照读还是当前读呢❓❓❓
答案是由隔离级别来决定!这也就是为什么我们使用不同的隔离级别后,不同的事务因为看到不同版本的快照而互相隔离的原因。
比如说使用读不提交级别,那么只要我们执行了任何操作,那么其它事务看到的其实是当前读,因为此时那些被修改的记录都会被其它事务看到;而如果使用可重复读级别的话,那么其它是位于看到的其实是一个历史版本也就是
undo log中的旧版本,此时最先执行的事务执行的操作,是在新版本上执行的,这和其它看到的历史版本也就是快照是独立的,互不影响的,这就产生了隔离的效果,如下图所示:
③ Read View读视图
前面我们讲了两个前提知识:记录中的隐藏字段、回滚日志。我们也进行了简单的 MVCC 的模拟流程,但是在模拟的过程中,我们好像还没讲清一个点,就是如何保证让不同的事务就一定能看到该看的内容呢❓❓❓
我们在前面提到是和隔离级别决定的,那问题又变成了,怎么让隔离级别来决定看到什么内容呢,这就是我们现在要学的 read view 的作用!
Read View 就是事务进行 “快照读” 操作的时候生产的读视图 (Read View)。
在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,在源码中用变量 creator_trx_id 来表示(当每个事务开启时,都会被分配一个 ID,这个 ID 是递增的,所以最新的事务,ID值越大,这和我们前面所说的 DB_TRX_ID 是不一样的!)
此外,Read View 在 MySQL 源码中就是一个封装的类,本质是用来进行可见性判断的。 只有当我们某个事务执行快照读的时候,才会对该记录创建一个 Read View 读视图,也就是一个类对象,我们可以通过类内的字段来以及上面所提到的记录中的隐藏字段进行对比,从而达到当前事务可以看到哪些快照的目的!
还要强调的是,Read View 一创建就被初始化,并且不会再被重新赋值,除非是重新生成新的 Read View!
下面是 mysql 源码中 Read View 的类定义,这里为了降低学习成本,已经简化了:
class ReadView
{
// 省略...
private:
// 高水位,大于等于这个ID的事务均不可见
trx_id_t m_low_limit_id
// 低水位:小于这个ID的事务均可见
trx_id_t m_up_limit_id;
// 创建该 Read View 的事务ID
trx_id_t m_creator_trx_id;
// 创建视图时的活跃事务id列表
ids_t m_ids;
// 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG */
trx_id_t m_low_limit_no;
// 标记视图是否被关闭
bool m_closed;
// 省略...
}; 其中最重要的就是前四个成员变量,下面单独把它们拎出来:

creator_trx_id:指的是创建该Read View的事务id。m_ids:指的是在创建Read View时,当前数据库中「活跃事务」的事务id列表,注意是一个列表!其中 ”活跃事务“ 指的就是启动了但还没提交的事务。low_limit_id:这个并不是m_ids的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的id值,即全局事务中最大的事务id值 +1。up_limit_id:指的是在创建Read View时,当前数据库中「活跃事务」中事务id最小的事务,也就是m_ids的最小值。
上图中的字段因为太长,所以省去了一些字符!说实话,源码中对于
low_limit_id和up_limit_id这两个字段的名称以及作用,我觉得是非常容易让人混淆的,我觉得使用min_id来代替up_limit_id,而用max_id来代替low_limit_id反而会更加的适合,不知道当时写源码的大佬是什么想法,反正我是有点懵的!![]()
此时有了上面的四个字段之后,再配合之前我们学习的两个前提知识:记录中的隐藏字段 DB_TRX_ID 和 DB_ROLL_PTR,以及 undo 日志,我们就能搞清楚 MVCC 到底是怎么运作的了!
其中最重要的点,就是拿 Read View 中的事务 ID 与记录中隐藏字段的 DB_TRX_ID 进行对比,判断到底哪些快照该看哪些不该看!
下面我们一起来揭开它的面纱!
3、Read View在MVCC里如何工作的❓❓❓
我们根据上面的知识,可以先将 Read View 按照时间线划分一下区间,如下图所示:
也就是说,我们只需要比较一下事务 ID 的大小,从而就能判断其在哪个区间,知道了在哪个区间之后,我们就能知道当前事务该看到哪些快照。
那可能就会有人问,我们要去比较谁和谁的事务 ID❓❓❓
首先要记住,read view 不是在事务启动后就创建的,而是在开始快照读操作的时候才会创建的!创建完之后填充刚才我们讲的四个字段。接着就拿着这个读视图对象去与版本链中的每个版本进行事务 ID 的比较(一般是从新快照到旧快照),下面我们来看看比较的过程是怎么样的!
(下面的比较过程,一定要先搞清楚每个字段的含义,并且结合上面分区图一起看更好理解!)
- 如果该快照的
DB_TRX_ID==creator_trx_id的话,说明这个快照就是当前事务创建的,那么对于当前事务肯定是可见的! - 如果该快照的
DB_TRX_ID!=creator_trx_id的话,有多种情况:- 如果该快照的
DB_TRX_ID<up_limit_id的话,说明该快照比当前事务列表中最早出现的活跃事务出现还早出现,所以此时该快照对于当前事务来说肯定是可见的! - 如果该快照的
DB_TRX_ID>low_limit_id的话,说明该快照尚未被系统分配过,也就是该快照比当前事务列表中最晚出现的活跃事务还晚出现,那么对于当前事务来说,该快照肯定是不可见的! - 如果该快照的
up_limit_id≤DB_TRX_ID≤low_limit_id的话,此时需要分两种情况讨论:- 如果
DB_TRX_ID不存在m_ids中,说明产生该快照的事务已经提交了,此时对当前事务来说,该快照是可见的! - 如果
DB_TRX_ID存在m_ids中,说明产生该快照的事务还正在执行中,此时对当前事务来说,该快照是不可见的!
- 如果
- 如果该快照的
- 如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件。
在数据库管理系统中,一个事务通常只有一个
read view,它反映了事务开始时数据库的一致状态,并且在整个事务的生命周期内保持不变。 当一个事务开始时,它会创建一个读取视图,并且只能在这个视图中看到数据库中的数据。这意味着如果其他事务在此事务开始后对数据库进行了更改,这些更改对于当前事务是不可见的。因此,每个事务都有自己的独立读取视图,以保障事务之间的隔离性。
另外还需要强调的是,
read view不是在事务启动后就创建的,而是在开始快照读操作的时候才会创建的!
这种做法能成功的关键也在于创建事务 ID 的时候是自增的,根据其大小就能判断出现的先后顺序,天然就给我们提供了一个能判断的区间!这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
我们再来看看 mysql 对应源码的策略:
🎏 模拟整个比较流程
假设当前有条记录:
| name | age | DB_TRX_ID(创建该记录的事务ID) |
DB_ROW_ID(隐藏主键) |
DB_ROLL_PTR(回滚指针) |
|---|---|---|---|---|
| 张三 | 28 | null | 1 | null |
然后此时有四个事务同时启动,其中事务四先进行修改,接着事务二进行快照读,如下表所示:
| 事务一(id=1) | 事务二(id=2) | 事务三(id=3) | 事务四(id=4) |
|---|---|---|---|
| 事务开始 | 事务开始 | 事务开始 | 事务开始 |
| …… | …… | …… | 修改并且提交 |
| 执行中 | 快照读 | 执行中 | |
| …… | …… | …… |
下面根据上表我们来模拟一下整个比较流程!
假设事务四修改的内容是将 name(张三) 改为了 name(李四),此时的版本链如下所示:
当 事务二 对某行数据执行了 快照读 ,数据库为该行数据生成一个 Read View 读视图:
// 事务二创建的Read View
m_ids; // 1,3 表示当前1号到3号是活跃事务
up_limit_id; // 1 表示当前活跃事务中最早出现的是1号
low_limit_id; // 4 + 1 = 5 因为Read View分配的是系统尚未分配的下一个事务ID
creator_trx_id; // 2 表示创建该Read View的是2号 此时 mysql 会将事务二生成的该读视图拿去跟当前所有的快照进行比较:
先从新版本开始,其 DB_TRX_ID 为 4,首先可以看出其不等于 creator_trx_id 即不等于 2,并且它是介于 [up_limit_id, low_limit_id] 也就是 [1, 5] 之间的,那么此时我们需要判断一下 DB_TRX_ID 是否存在 m_ids 中,很明显是不存在的,那么此时我们就能得出这个快照对于事务二来说是可见的。
那么事务二能读到的最新数据记录是事务四所提交的版本,而事务四提交的版本也是全局角度上最新的版本!
4、「读提交」与「可重复读」的本质区别 -- 取决于read view的生成时机
在解决这个问题之前,我们首先得来解决「快照读」和「当前读」的区别,才能明白「读提交」与「可重复读」,因为它们其实关系是很密切的!
这里介绍一个语句:
select * from user lock in share mode; 其表示以加共享锁方式进行读取,简单地说,就是当前读!下面我们会用这个语句做测试!
下面我们以「可重复读」为例,来测试一下「读提交」与「可重复读」的区别,首先将数据库设为「可重复读」,然后插入一张测试表和数据,如下所示:
mysql> select @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set, 1 warning (0.00 sec)
create table if not exists account(
id int primary key,
name varchar(50) not null default '',
blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;
insert into account values(1, 'liren', 13.14);测试一
| 事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
|---|---|---|---|
begin; |
开启事务 | 开启事务 | begin; |
select * from account; |
快照读 | 快照读 | select * from account; |
update account set name=‘tthh’ where id=1; |
更新name | ||
commit |
提交事务 | ||
| 快照读 没有读到修改内容 |
select * from account; |
||
| 当前读 读到修改内容 |
select * from account lock in share mode; |
这种情况合情合理,因为终端2是在终端1更新前进行快照读的,此时终端2就会生成一个 read view,记录的是更改前的那个快照记录!所以当终端1进行更新并且提交之后,终端2再次去快照读的时候,使用的还是原来那个 read view,所以看到的还是没更改前的那个快照!
当我们使用 select * from user lock in share mode 语句之后,表示强制使用当前读的意思,那么自然读到的就是新版本也就是更新后的快照!由此我们可以得出小结论:
在「可重复读」的情况下,执行快照读之后创建的 read view,在其事务的生命周期之内都不会更改,这样子就能保证其它事务提交之后不影响当前事务!
测试二
| 事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
|---|---|---|---|
| begin; | 开启事务 | 开启事务 | begin; |
| select * from account; | 快照读 | ||
| update account set name=‘tthh’ where id=1; | 更新name | ||
| commit | 提交事务 | ||
| 快照读 读到修改内容 |
select * from account; | ||
| 当前读 读到修改内容 |
select * from account lock in share mode; |
测试二和测试一的区别就是事务B的快照读时机,是在事务A进行提交之后才进行的!
仅仅是一个进行快照读的时机不同,就导致了不同的结果,这其实就是我们所学的 read view 的工作原理!
因为终端1进行提交之后,版本链中此时最新版本就是更改后的内容,而终端2是在终端1提交事务之后才进行快照读的,那么此时创建的 read view 中,终端1是属于已经提交的事务,那么对于终端2来说肯定是可见的,并且因为其是最新版本,所以自然快照读就是读到该更改后的内容。
当使用 select * from user lock in share mode 语句的时候,其实和快照读就没区别了,因为它们此时都指向最新版本!
这也印证了一个结论:read view 不是在事务启动的时候创建的,而是在执行快照读之后才会创建的!
那么可能会有人问,「可重复读」不是要避免看到终端1提交的内容吗,这不就违背了吗❓❓❓
问这个问题的同学,说明还不理解「可重复读」的作用!「可重复读」是用来避免不可重复读的问题,即在终端2的事务执行过程中,看到不同的结果,但是当前的情况是,在终端2中,我们确实看到了终端1的更新内容,但是这并不影响「可重复读」的原则。
如果后续其它事务进行更改了内容,那么终端2看到还是当前这个内容,相当于我们可以保证在终端
2一直读取该记录,而不会出现不同结果,这才是「可重复读」的价值!
💥结论
-
事务中首次出现快照读的时机,决定了该事务能看到哪些快照记录!
-
正是
Read View生成时机的不同,从而造成RC、RR级别下快照读的结果的不同:- 在
RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View,将当前系统活跃的其他事务记录起来- 此后在调用快照读的时候,还是使用的是同一个
Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改自然就不可见。 - 即在
RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
- 此后在调用快照读的时候,还是使用的是同一个
- 而在
RC级别下的事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因!
- 在
-
总之在
RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View。 -
正是
RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。
下面是对上面的简化叙述:
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过
Read View来实现的,它们的区别在于创建Read View的时机不同:
- 「读提交」隔离级别是在每个
select都会生成一个新的Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。- 「可重复读」隔离级别是启动事务时生成一个
Read View,然后整个事务期间都在用同一个Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。 这两个隔离级别实现是通过「事务的
Read View里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫MVCC(多版本并发控制)。
推荐阅读
关于这块,有很好的文章,推荐大家阅读:
- https://blog.csdn.net/SnailMann/article/details/94724197
- /notes-img/65355109_9010872.html.png
- https://blog.csdn.net/chenghan_yang/article/details/97630626