当前位置: 首页 > news >正文

MySQL基础 [八] - 事务

目录

前言

什么是事务

事务的版本支持

事务的提交方式

事务的相关演示

并行事务引发的问题

脏读 dirty read

不可重复读 non-repeatable read

幻读 phantom read

事务的隔离级别

查看与设置隔离级别

读未提交(Read Uncommitted)

读提交(Read Committed)

可重复读(Repeatable Read)

串行化(Serializable)

​编辑 隔离级别总结

关于一致性

MVCC多版本并发控制机制

数据库并发的场景

实现MVCC的三个前提

记录中的隐藏字段

undo日志

Read View

RR与RC的本质区别


前言

假设我买彩票中了五百万元,现在李四手里缺钱,想找我借一百万,然后我准备往家人账号进行转账的时候,此时这个转账动作分为下面几步:

  1. 从数据库中读取我的余额
  2. 将我的余额减去转账的金额
  3. 将我修改后的余额更新到数据库
  4. 从数据库读取对方的余额
  5. 将对方的余额加上转账的金额
  6. 将对方修改后的余额更新到数据库

可以看到这个转账的过程涉及到了两次修改数据库的操作。

假设在执行第三步骤之后,服务器突然掉电了,就会发生一个蛋疼的事情,我的账户扣了一百万,但是钱并没有到家人的账户上,也就是说这一百万消失了!

所以我们在日常生活中必须解决这个问题,不然秩序就乱了!要解决这个问题,就要保证转账业务里的所有数据库的操作完整的、不可分割的,要么全部执行成功 ,要么全部失败,不允许出现中间状态的数据!

数据库中的 事务(Transaction) 就能达到这样的效果。

​我们在转账操作前先开启事务,等所有数据库操作执行完成后,才提交事务,对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,如果中途发生发生中断或错误,那么该事务期间对数据库所做的修改将会被 回滚 到没执行该事务之前的状态。

什么是事务

事务是 由一组 DML 语句组成 的,这些语句在逻辑上存在相关性,这一组 DML 语句要么全部成功,要么全部失败,是一个整体。 MySQL 提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。

​事务要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。假设一种场景:你毕业了,学校的教务系统后台 MySQL 中,不再需要你的数据,要删除你的所有信息(一般不会),那么要删除你的基本信息(姓名,电话,籍贯等)的同时,也删除和你有关的其他信息,比如:你的各科成绩,你在校表现,甚至你在论坛发过的文章等。这样,就 需要多条 MySQL 语句构成,那么所有这些操作合起来,就构成了一个事务。

正如我们上面所说,一个 MySQL 数据库,可不止你一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL ,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题,就是数据不安全问题了。甚至,因为事务由多条 SQL 构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?

所以,一个完整的事务,绝对不是简单的 sql 集合还需要满足如下四个属性:

  • 原子性(Atomicity) 一个事务中的所有操作,要么全部完成,要么全部不完成不会结束在中间某个环节。事务在执行过程中如果发生错误,则会自动回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整型没有被破坏,这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联型以及后续数据库可以自发性地完成预定的工作。
  • 隔离性(Isolation): 数据库允许多个事务同时访问同一份数据,隔离性可以保证多个事务在并发执行时,不会因为由于交叉执行而导致数据的不一致。
  • 持久性(Durability): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

上面的四个属性简称ACID

为什么会出现事务?

事务被MySQL编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要用户自己去考虑各种各样的潜在错误和并发问题。
如果MySQL只是单纯的提供数据存储服务,那么用户在访问数据库时就需要自行考虑各种潜在问题,包括网络异常、服务器宕机等。因此事务本质是为了应用服务的,而不是伴随着数据库系统天生就有的。

事务的版本支持

通过show engines命令可以查看数据库引擎。如下:

  • Engine: 表示存储引擎的名称。
  • Support: 表示服务器对存储引擎的支持级别,YES表示支持,NO表示不支持,DEFAULT表示数据库默认使用的存储引擎,DISABLED表示支持引擎但已将其禁用。
  • Comment: 表示存储引擎的简要说明。
  • Transactions: 表示存储引擎是否支持事务,可以看到InnoDB存储引擎支持事务,而MyISAM存储引擎不支持事务。
  • XA: 表示存储引擎是否支持XA事务。
  • Savepoints: 表示存储引擎是否支持保存点。

事务由 MySQL 的引擎来实现的,我们常见的 InnoDB 引擎它是支持事务的。不过并不是所有的引擎都能支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,也正是这样,所以大多数 MySQL 的引擎都是用 InnoDB。

​那 InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

持久性 是通过 redo log(重做日志)来保证的
原子性 是通过 undo log(回滚日志)来保证的
隔离性 是通过 MVCC(多版本并发控制)或锁机制来保证的
而 一致性,是通过持久性 + 原子性 + 隔离性共同来保证

事务的提交方式

事务常见的提交方式有两种,分别是自动提交手动提交

  • 自动提交:默认情况下,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 命令,就会 马上启动事务。

查看事务的提交方式 

通过show命令查看autocommit全局变量,可以查看事务的自动提交是否被打开。如下:

autocommit的值为ON表示自动提交被打开,值为OFF表示自动提交被关闭,即事务的提交方式为手动提交。

设置事务的提交方式

通过set命令设置autocommit全局变量的值,可以打开或关闭事务的自动提交。如下:

将autocommit的值设置为1表示打开自动提交,设置为0表示关闭自动提交,相当于将事务提交方式设置为手动提交。

事务的相关演示

准备测试表

为了便于演示,我们将MySQL的隔离级别设置成读未提交,也就是把隔离级别设置的比较低,方便看到实验现象。如下:

需要注意的是,设置全局隔离级别后当前会话的隔离级别不会改变,只会影响后续与MySQL新建立的连接,因此需要重启终端才能看到会话的隔离级别被成功设置。如下:

已经设置了,但是没重启

注意:tx_isolation变量在MySQL 5.7.20中被废弃,并在MySQL 8.0中被完全移除。它被transaction_isolation所替代。

重启之后,再查看

创建一个银行用户表,表中包含用户的id、姓名和账户余额。如下:

演示一:事务的常规操作

启动两个终端,左终端使用begin或start transaction命令启动一个事务,右终端查看银行用户表中的信息。如下: 

左终端中的事务向表中插入一条记录,由于我们将隔离级别设置成了读未提交因此在左终端中的事务使用commit提交之前,在右终端中就能查看到事务向表中插入的记录。如下:

左终端中的事务使用savepoint命令创建一个保存点,然后继续向表中插入一条记录,这时在右终端中也能看到新插入的这条记录。如下:

左终端中的事务使用rollback命令回滚到保存点,这时右终端在查看表中数据时就看不到刚才插入的第二条记录了。如下:

  • 使用begin或start transaction命令,可以启动一个事务。
  • 使用savepoint 保存点命令,可以在事务中创建指定名称的保存点。
  • 使用rollback to 保存点命令,可以让事务回滚到指定保存点。
  • 使用rollback命令,可以直接让事务回滚到最开始。
  • 使用commit命令,可以提交事务,提交事务后就不能回滚了。

演示二:原子性

在左终端中启动一个事务,在右终端查看银行用户表中的信息。如下:

左终端中的事务向表中插入一条记录,由于隔离级别是读未提交,因此在右终端中能够查询到插入的这条记录。如下: 

如果左终端中的事务在提交之前因为某些原因与MySQL断开连接,那么MySQL会自动让事务回滚到最开始,这时右终端中就看不到之前插入的记录了。如下:

演示三:持久性

 在左终端中启动一个事务,在右终端查看银行用户表中的信息。如下:

左终端中的事务向表中插入一条记录,由于隔离级别是读未提交,因此在右终端中能够查询到插入的这条记录。如下:

左终端中的事务在提交后与MySQL断开连接这时右终端中仍然可以看到之前插入的记录,因为事务提交后数据就被持久化了。如下:

演示四:begin会自动更改提交方式

通过show命令查看autocommit的值为ON,表示事务的提交方式是自动提交此时银行用户表中有一条记录。如下:

在左终端中启动一个事务并向表中新插入一条记录,由于隔离级别是读未提交,因此在右终端中能够查询到新插入的这条记录。如下:

如果左终端中的事务在提交之前与MySQL断开连接,那么MySQL依旧会自动让事务回滚到最开始,这时右终端中就看不到之前新插入的记录了。如下:

也就是说,使用begin或start transaction命令启动的事务,都必须要使用commit命令手动提交,数据才会被持久化,与是否设置autocommit无关。

演示五:单条SQL与事务的关系

  • 实际全局变量autocommit是否被设置影响的是单条SQL语句,InnoDB中的每一条SQL都会默认被封装成事务。
  • autocommit为ON,则单条SQL语句执行后会自动被提交,如果为OFF,则SQL语句执行后需要使用commit进行手动提交。

比如通过show命令查看autocommit的值为ON,表示事务的提交方式是自动提交,此时银行用户表中有一条记录。如下:

在左终端中直接向表中新插入一条记录,由于隔离级别是读未提交,因此在右终端中肯定能够查询到新插入的这条记录。如下:

但就算左终端在执行单条SQL后不使用commit进行提交,而直接与MySQL断开连接,这时右终端仍然可以看到之前新插入的记录了,因为单条SQL在执行后被自动提交持久化了。如下:

相反,如果将autocommit设置为OFF,表示事务执行后需要手动提交,此时银行用户表中有两条记录。如下:

在左终端中直接向表中新插入一条记录,由于隔离级别是读未提交,因此在右终端中肯定能够查询到新插入的这条记录。如下:

但如果此时左终端在执行单条SQL后不使用commit进行提交而直接与MySQL断开连接,那么这时右终端中就看不到之前新插入的记录了,因为这时单条SQL执行后需要使用commit手动提交后才会持久化,在commit之前与MySQL断开连接则会自动进行回滚操作。如下:

也就是说,实际我们之前一直都在使用单SQL事务,只不过autocommit默认是打开的,因此单SQL事务执行后自动就被提交了。

并行事务引发的问题

​ MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况。

​ 那么在 并发处理多个事务的时候,就可能出现 脏读不可重复读幻读 的问题。

​ 接下来,通过举例子说明这些问题是如何发生的。

脏读 dirty read

脏读是指 一个事务读取了另一个事务尚未提交的数据。当一个事务读取了另一个事务的未提交数据时,如果另一个事务最终回滚,则读取到的数据实际上是无效的。脏读可能导致不一致的结果。

​假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库中读取利刃的余额数据,然后再执行更新操作,如果此时事务 A 还没有提交事务,而此时正好事务 B 也从数据库中读取利刃的余额数据,那么事务 B 读取到的余额数据是刚才事务 A 更新后的数据,即使没有提交事务。

因为事务 A 是还没提交事务的,也就是它随时可能发生回滚操作,如果事务 A 发生了回滚,那么事务 B 刚才得到的数据就是过期的数据,这种现象就被称为脏读。

不可重复读 non-repeatable read

不可重复读是指 在一个事务内,多次读取同一数据时,得到的结果不一致。这是因为在事务执行期间,其他事务可能 修改 了被读取的数据。不可重复读可能导致数据的不一致性。

​ 假设有 A 和 B 这两个事务同时在处理,事务 A 先开始从数据库中读取利刃的余额数据,然后继续执行代码逻辑处理,在这过程中如果事务 B 更新了这条数据,并提交了事务,那么当事务 A 再次读取该数据时,就会发现前后两次读到的数据是不一致的,这种现象就被称为不可重复读。

幻读 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 条,发现和前一次读到的记录数量不一样了,就感觉发生了幻觉一样,这种现象就被称为幻读。 

这三个现象的严重性排序如下:

简单地说,脏读读取了未提交的数据不可重复读读取同一数据时结果不一致幻读查询结果集不一致这些问题都是由于并发事务引起的,解决方法通常是通过 锁机制隔离级别 来保证数据的一致性。

事务的隔离级别

  • MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务的方式进行。
  • 一个事务可能由多条SQL语句构成,也就意味着任何一个事务,都有执行前、执行中和执行后三个阶段,所谓的原子性就是让用户层要么看到执行前,要么看到执行后,执行中如果出现问题,可以随时进行回滚,所以单个事务对用户表现出来的特性就是原子性。
  • 但毕竟每个事务都有一个执行的过程,在多个事务各自执行自己的多条SQL时,仍然可能会出现互相影响的情况,比如多个事务同时访问同一张表,甚至是表中的同一条记录。
  • 数据库为了保证事务执行过程中尽量不受干扰,于是出现了隔离性的概念,数据库为了允许事务在执行过程中受到不同程度的干扰,于是出现了隔离级别的概念。

就如同一些导师说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你导师不关心。此时你的学习对你导师来讲,就是原子的。而在你学习的过程中,是很容易受别人干扰的,此时,就需要将你的学习环境与其它环境隔离开,保证你的学习环境是健康的。

数据库事务的隔离级别有以下四种:

  • 读未提交(Read Uncommitted): 在该隔离级别下,所有的事务都可以看到其他事务没有提交的执行结果,实际生产中不可能使用这种隔离级别,因为这种隔离级别相当于没有任何隔离性,会存在很多并发问题,如脏读、幻读、不可重复读等。
  • 读提交(Read Committed): 该隔离级别是大多数数据库的默认隔离级别,但它不是MySQL默认的隔离级别,它满足了隔离的简单定义:一个事务只能看到其他已经提交的事务所做的改变,但这种隔离级别存在不可重复读和幻读的问题。
  • 可重复读(Repeatable Read): 这是MySQL默认的隔离级别,该隔离级别确保同一个事务在执行过程中,多次读取操作数据时会看到同样的数据,即解决了不可重复读的问题,但这种隔离级别下仍然存在幻读的问题。
  • 串行化(Serializable): 这是事务的最高隔离级别,该隔离级别通过强制事务排序,使之不可能相互冲突,从而解决了幻读问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争问题,这种隔离级别太极端,实际生成中基本不使用。

虽然数据库事务的隔离级别有以上四种,但一个稳态的数据库只会选择这其中的一种,作为自己的默认隔离级别。但数据库默认的隔离级别有时可能并不满足上层的业务需求,因此数据库提供了这四种隔离级别,可以让我们自行设置。

 那隔离级别是如何实现的呢?

是通过加锁的方式实现的,不同的隔离级别对锁的使用是不同的,常见的有表锁、行锁、写锁、间隙锁(GAP)、Next-Key锁(GAP+行锁)等。

查看与设置隔离级别

查看全局隔离级别

通过select @@global.transaction_isolation命令,可以查看全局隔离级别。如下:

查看会话隔离级别 

通过select @@session.transaction_isolation命令,可以查看当前会话的隔离级别。如下:

此外,还可以通过select @@transaction_isolation命令,也可以查看当前会话的隔离级别。如下:

设置会话隔离级别

通过 set session transaction isolation level 隔离级别 命令,可以设置当前会话的隔离级别。如下: 

设置会话的隔离级别只会影响当前会话,新起的会话依旧采用全局隔离级。

设置全局隔离级别

通过 set global transaction isolation level 隔离级别 命令

设置全局隔离级别会影响后续的新会话,但当前会话的隔离级别没有发生变化,如果要让当前会话的隔离级别也改变,则需要重启会话。

读未提交(Read Uncommitted)

启动两个终端,将隔离级别都设置为读未提交,并查看此时银行用户表中的数据。如下:

在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务就已经能够看到了。如下:

  • 读未提交是事务的最低隔离级别,几乎没有加锁,虽然效率高,但是问题比较多,所以严重不建议使用。
  • 一个事务在执行过程中,读取到另一个执行中的事务所做的修改,但是该事务还没有进行提交,这种现象叫做脏读。

读提交(Read Committed)

在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。如下:

只有当左终端中的事务提交后,右终端中的事务才能看到修改后的数据。如下:

上面的情况虽然避免了脏读问题,但是此时还在当前事务中,并未 commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中),读取到了不同的值这种现象叫做 不可重复读

不可重复读这种问题,有啥错误吗?

其实是有的。举个例子,

比如一个公司在发年终奖品的时候,可能按照当年的工资来划分区间进行不同奖品的发放;

此时员工小王本来工资是三千块钱,只能领个水杯,但是小王兢兢业业在背后默默的修代码查BUG,小王就想在发年终奖品前去找了老板升工资,老板也同意了升工资,将小王工资升到了四千五,这个时候小王的奖品应该对应的是一部手机,然后老板就吩咐小李去更新一下小王的工资数据。而我们知道,此时负责划分区间来发奖品的小瓦需要对公司的数据做划分,那么也肯定是通过事务来执行多条筛选语句划分区间。此时问题来了,当小瓦在事务中执行 select 语句筛选工资为 [3000, 4000] 的员工的时候,此时因为小李还没更新小王的数据,那么小王还是 3000 块钱,理所应当的分发一个水杯,于是就被记录下来了发放一个水杯;此时刚好小李更新了小王的数据,那么小瓦在执行下一条 select 语句筛选 [4000, 5000] 工资的员工的时候,发现小王又出现在了名单里面,于是小王就被统计发送了两个奖品,这是非常不合理的,也就是因为不可重复读的问题,才会导致小瓦在执行事务的时候做不到数据的隔离性而导致这种问题的发生!

可重复读(Repeatable Read)

启动两个终端,将隔离级别都设置为可重复读,并查看此时银行用户表中的数据。如下:

在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。如下:

并且当左终端中的事务提交后,右终端中的事务仍然看不到修改后的数据。如下:

​但 其实一般的数据库(不是指MySQL)在「可重复读」情况的时候,无法屏蔽其他事务 insert 的数据,这是为什么?

因为隔离性实现是对数据加锁完成的,而 insert 待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题。这会造成虽然大部分内容是可重复读的,但是 insert 的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象叫做 幻读(phantom read)。

而 MySQL 的「可重复读」隔离级别,引入了 MVCC 和 next-key lock在很大程度上避免了幻读问题

重新在这两个终端各自启动一个事务,左终端中的事务向表中插入数据的在没有提交之前,右终端中的事务无法看到。如下: 

并且当左终端中的事务提交后,右终端中的事务仍然看不到新插入的数据。如下:

 只有当右终端中的事务提交后再查看表中的数据,这时才能看到新插入的数据。如下:

但是却 无法完全解决幻读问题,要想解决幻读问题,还得靠下面要讲的「串行化」隔离级别。(但是一般我们不会使用这种极端隔离级别,效率太低)

串行化(Serializable)

启动两个终端,将隔离级别都设置为串行化,并查看此时银行用户表中的数据。如下:

在两个终端各自启动一个事务,如果这两个事务都对表进行的是读操作,那么这两个事务可以并发执行,不会被阻塞。如下:

但如果这两个事务中有一个事务要对表进行写操作,那么这个事务就会立即被阻塞。如下:

直到访问这张表的其他事务都提交后,这个被阻塞的事务才会被唤醒,然后才能对表进行修改操作。如下:

这里再用图片讲解一下其大概过程:

首先事务 B 在执行将余额 100 万修改为 200 万时,由于此前事务 A 执行了读操作,这样就发生了读写冲突,于是就会被锁住,直到事务 A 提交后,事务 B 才可以继续执行,所以从 A 的角度看,余额 V1V2 的值是 100 万,余额 V3 的值是 200 万。

 隔离级别总结

√:会发生该问题
X:不会发生该问题

  • 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,在选择隔离级别时往往需要在两者之间找一个平衡点。
  • 表中只写出了各种隔离级别下进行读操作时是否需要加锁,因为无论哪种隔离级别,只要需要进行写操作就一定需要加锁。

关于一致性

 事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。

其实一致性和用户的业务逻辑强相关,一般 MySQL 提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是说,一致性是由程序员决定的

​而在技术上,一致性是通过「原子性+隔离性+持久性」来保证的

MVCC多版本并发控制机制

数据库并发的场景

  • 读-读 :不存在任何问题,也不需要并发控制
  • 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读的情况
  • 写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失问题
  • 写-写并发场景下的第一类更新丢失又叫做回滚丢失,即一个事务的回滚把另一个已经提交的事务更新的数据覆盖了
  • 第二类更新丢失又叫做覆盖丢失,即一个事务的提交把另一个已经提交的事务更新的数据覆盖了。

读-读并发不需要进行并发控制,写-写并发实际也就是对数据进行加锁,这里最值得讨论的是读-写并发,读-写并发是数据库当中最高频的场景,在解决读-写并发时不仅需要考虑线程安全问题,还需要考虑并发的性能问题。

​ 在数据库中,读写模式更为常见,所以下面我们会以 读-写 模式进行切入讲解!

​ 在此之前,我们先谈一些共识:

  • 每个事务都要有自己的事务 ID ,这样子的话 mysql 可以根据事务 ID 的大小,来决定事务到来的先后顺序,一般都是 ID 越小,说明事务越早产生!
  • mysqld 可能会面临处理多个事务的情况,所以事务也需要有自己的生命周期,而 mysqld 要对这些事务进行管理,就得先描述,再组织,也就是说事务也有自己的一套结构体!

实现MVCC的三个前提

首先,MVCCMulti-Version Concurrency Control是一种并发控制机制,用于在数据库系统中处理并发访问和修改数据的问题,其允许多个事务同时读取和修改数据库,而不会相互干扰或产生冲突。

在 MVCC 中,每个事务在开始时会获得一个唯一的事务 ID并且每个数据行都会有一个版本号或时间戳。当事务对数据进行修改时,会创建一个新的数据版本,并将事务 ID 和版本号关联起来。这样,其他事务仍然可以读取旧版本的数据,而不会受到正在进行的修改的影响。

​ 所以 MVCC 可以为数据库解决以下问题:

  1. 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。
  2. 同时还可以解决脏读、不可重复读、幻读等事务隔离问题,但不能解决更新丢失问题。

而要实现上述内容,其实我们就要引入三个前提知识:

  • 记录中的隐藏字段
  • 回滚日志 undo log
  • 读视图 Read View

记录中的隐藏字段

首先先强调一点,我们把MySQL中的一行信息,称为一行记录!那么一行记录中的隐藏字段,其实就是我们平时查表的时候看不到的字段!

数据库表中的每条记录都会有如下3个隐藏字段,前两个很重要

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字段被设置为删除,只有当数据库进行持久化刷新之后,数据才会真的从磁盘文件中删除!

当向表中插入一条记录后,该记录不仅包含name和age字段,还包含三个隐藏字段。如下: 

  • 假设插入该记录的事务的事务ID为9,那么该记录的DB_TRX_ID字段填的就是9。
  • 因为这是插入的第一条记录,所以隐式主键DB_ROW_ID字段填的就是1。
  • 由于这条记录是新插入的,没有历史版本,所以回滚指针DB_ROLL_PTR的值设置为null。
  • MVCC重点需要的就是这三个隐藏字段,实际还有其他隐藏字段,只不过没有画出。

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中,此时undo log中就有了一行副本数据。
  • 然后再将原始记录中的学生姓名改为“李四”,并将该记录的DB_TRX_ID改为10,回滚指针DB_ROLL_PTR设置成undo log中副本数据的地址,从而指向该记录的上一个版本。
  • 最后当事务10提交后释放锁,这时最新的记录就是学生姓名为“李四”的那条记录。

 更新如下图所示:

现在又有一个事务ID为11的事务,要将刚才学生表中的那条记录的学生年龄改为38:

  • 因为是要进行写操作,所以需要先给该记录(最新的记录)加行锁。
  • 修改前,先将该行记录拷贝到undo log中,此时undo log中就又有了一行副本数据。
  • 然后再将原始记录中的学生年龄改为38,并将该记录的DB_TRX_ID改为11,回滚指针DB_ROLL_PTR设置成刚才拷贝到undo log中的副本数据的地址,从而指向该记录的上一个版本。
  • 最后当事务11提交后释放锁,这时最新的记录就是学生年龄为38的那条记录。

修改后的示意图如下: 

这样一来,我们就有了一个 基于链表记录的历史版本链,而所谓的回滚,无非就是用历史数据,覆盖当前数据罢了!而所谓的创建保存点就可以理解成是给某些版本做了标记,让我们可以直接用这些版本数据来覆盖当前数据。

​ 而 undo log 中的每一个版本,我们可以称之为一个一个的 快照(就好像原记录被拍照后保存下来的样子!)

insert和delete的记录如何维护版本链?

删除记录并不是真的把数据删除了,而是先将该记录拷贝一份放入undo log中,然后将该记录的删除flag隐藏字段设置为1,这样回滚后该记录的删除flag隐藏字段就又变回0了,相当于删除的数据又恢复了。
新插入的记录是没有历史版本的,但是一般为了回滚操作,新插入的记录也需要拷贝一份放入undo log中,只不过被拷贝到undo log中的记录的删除flag隐藏字段被设置为1,这样回滚后就相当于新插入的数据就被删除了。

也就是说,增加、删除和修改数据都是可以形成版本链的。

快照读 && 当前读

快照读

  • 就是 读取一个记录的历史版本。读取历史版本的话,是不受加锁限制的,也就是多个事务可以并行读取历史版本!换言之,快照读提高了并发效率,这也是 MVCC 的意义所在。
  • 针对快照读(普通 select 语句),通过 MVCC 方式解决幻读问题,因为在可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
     

当前读

  • 就是 读取最新的记录 的,都叫做当前读,而 select 也有可能是当前读。在多个事务同时删改查的时候,都是当前读,这是要加锁的。如果同时有 select 语句事务过来的话,并且读取的是最新版本记录(当前读),那么也就需要加锁,这就是串行化。
  • 针对当前读(select ... for update 等语句),通过 next-key lock(记录锁+间隙锁)方式解决幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

​ 那到底是什么决定了事务是执行快照读还是当前读呢?

答案是 由隔离级别来决定!这也就是为什么我们使用不同的隔离级别后,不同的事务因为看到不同版本的快照而互相隔离的原因。
比如说使用读不提交级别,那么只要我们执行了任何操作,其它事务看到的其实是当前读,因为此时那些被修改的记录都会被其它事务看到;

而如果使用可重复读级别的话,那么其它是位于看到的其实是一个历史版本也就是 undo log 中的旧版本,此时最先执行的事务执行的操作,是在新版本上执行的,这和其它看到的历史版本也就是快照是独立的,互不影响的,这就产生了隔离的效果,如下图所示:

undo log中的版本链何时才会被清除?

  • 在undo log中形成的版本链不仅仅是为了进行回滚操作,其他事务在执行过程中也可能读取版本链中的某个版本,也就是快照读。
  • 因此,只有当某条记录的最新版本已经修改并提交,并且此时没有其他事务与该记录的历史版本有关了,这时该记录在undo log中的版本链才可以被清除。

对于新插入的记录来说,没有其他事务会访问它的历史版本,因此新插入的记录在提交后就可以将undo log中的版本链清除了。因此版本链在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 的最小值。

此时有了上面的四个字段之后,再配合之前我们学习的两个前提知识:记录中的隐藏字段 DB_TRX_ID 和 DB_ROLL_PTR,以及 undo 日志,我们就能搞清楚 MVCC 到底是怎么运作的了!

其中最重要的点,就是 拿 Read View 中的事务 ID 与记录中隐藏字段的 DB_TRX_ID 进行对比,判断到底哪些快照该看哪些不该看

Read View在MVCC里如何工作的?

  • 事务ID小于m_up_limit_id的事务,一定是生成Read View时已经提交的事务,因为m_up_limit_id是生成Read View时刻系统中活跃事务ID中的最小ID,因此事务ID比它小的事务在生成Read View时一定已经提交了。
  • 事务ID大于等于m_low_limit_id的事务,一定是生成Read View时还没有启动的事务,因为m_low_limit_id是生成Read View时刻,系统尚未分配的下一个事务ID。
  • 事务ID位于m_up_limit_id和m_low_limit_id之间的事务,在生成Read View时可能正处于活跃状态,也可能已经提交了,这时需要通过判断事务ID是否存在于m_ids中来判断该事务是否已经提交。

我们根据上面的知识,可以先将 Read View 按照时间线划分一下区间,如下图所示:

​ 那可能就会有人问,我们要去比较谁和谁的事务 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(多版本并发控制)。

模拟整个比较流程

 假设当前有条记录:

nameageDB_TRX_ID(创建该记录的事务ID)DB_ROW_ID(隐藏主键)DB_ROLL_PTR(回滚指针)
张三28null1null

​ 然后此时有四个事务同时启动,其中事务四先进行修改,接着事务二进行快照读,如下表所示: 

事务一(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号

先从新版本开始,其 DB_TRX_ID 为 4,首先可以看出其不等于 creator_trx_id 即不等于 2,并且它是介于 [up_limit_id, low_limit_id] 也就是 [1, 5] 之间的,那么此时我们需要判断一下 DB_TRX_ID 是否存在 m_ids 中,很明显是不存在的,那么此时我们就能得出这个快照对于事务二来说是 可见 的。

那么事务二能读到的最新数据记录是事务四所提交的版本,而事务四提交的版本也是全局角度上最新的版本!

RR与RC的本质区别

在解决这个问题之前,我们首先得来解决「快照读」和「当前读」的区别,才能明白「读提交」与「可重复读」,因为它们其实关系是很密切的!

这里介绍一个语句:

select * from user lock in share mode;

其表示以加共享锁方式进行读取,简单地说,就是当前读!下面我们会用这个语句做测试!

下面我们 以「可重复读」为例,来测试一下「读提交」与「可重复读」的区别,首先将数据库设为「可重复读」,然后插入一张测试表和数据,如下所示:

mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_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);

测试一

这种情况合情合理,因为终端2是在终端1更新前进行快照读的,此时终端2就会生成一个 read view,记录的是更改前的那个快照记录!所以当终端1进行更新并且提交之后,终端2再次去快照读的时候,使用的还是原来那个 read view,所以看到的还是没更改前的那个快照!

而当我们使用 select * from user lock in share mode 语句之后,表示强制使用当前读的意思,那么自然读到的就是新版本也就是更新后的快照!由此我们可以得出小结论:

在「可重复读」的情况下,执行快照读之后创建的read view在其事务的生命周期之内都不会更改,这样子就能保证其它事务提交之后不影响当前事务!

测试二

测试二和测试一的区别就是事务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 才会有不可重复读问题。

相关文章:

MySQL基础 [八] - 事务

目录 前言 什么是事务 事务的版本支持 事务的提交方式 事务的相关演示 并行事务引发的问题 脏读 dirty read 不可重复读 non-repeatable read 幻读 phantom read 事务的隔离级别 查看与设置隔离级别 读未提交&#xff08;Read Uncommitted&#xff09; 读提交&…...

深入理解Java反射

反射(Reflection)是Java语言的一个强大特性&#xff0c;它允许程序在运行时动态地获取类的信息并操作类或对象的属性、方法和构造器。就是在获取运行时的java字节码文件&#xff0c;通过各种方法去创建对象&#xff0c;反射是Java被视为动态语言的关键特性之一。 反射其实就是…...

【UE】渐变框材质

效果 步骤 新建一个材质&#xff0c;这里命名为“M_GlowingBorder”&#xff0c;打开“M_GlowingBorder”后&#xff0c;设置材质域为“用户界面”&#xff0c;混合模式为“半透明” 添加如下节点&#xff1a; 代码&#xff1a; Begin Object Class/Script/UnrealEd.Materia…...

2025年第十八届“认证杯”数学中国数学建模网络挑战赛【ABCD题】思路分析

首先&#xff0c;需要理解用户的需求。问题1需要数学模型来确定小行星的相对距离&#xff0c;而问题2需要预测短期轨道并计算特定时间的观测角度。这两个问题都需要结合天文学和数学建模的知识&#xff0c;涉及到轨道力学和几何定位的方法。 接下来&#xff0c;查阅提供的搜索…...

JavaScript 性能优化:突破瓶颈的实战指南

一、引言​ 在现代 Web 应用和 Node.js 服务端开发中&#xff0c;JavaScript 已成为核心编程语言。随着应用复杂度提升&#xff0c;性能问题愈发凸显。高延迟、卡顿甚至崩溃等现象&#xff0c;不仅影响用户体验&#xff0c;还可能导致业务流失。深入理解 JavaScript 性能瓶颈并…...

HarmonyOS:组件布局保存至相册

一&#xff0c;需求背景 有这样一个需求&#xff0c;将页面上的某个自定义组件以图片的形式保存至相册。 二&#xff0c;需求拆解 根据需求分析&#xff0c;可将需求拆解成两步&#xff1a; 1&#xff0c;将组件转换成图片资源&#xff1b; 2&#xff0c;将图片保存到相册…...

【langchain库名解析】

目录 一、from langchain_openai import ChatOpenAI 1. 核心功能 2. 典型使用场景 场景 1&#xff1a;直接生成对话回复 场景 3&#xff1a;流式输出&#xff08;逐词显示结果&#xff09; 3. 与其他 LangChain 组件的协同 结合提示模板&#xff08;PromptTemplate&#…...

629SJBH图书管理系统设计与实现

一、 绪论 &#xff08;一&#xff09;课题的提出、现状及研究意义 图书馆是文献情报中心&#xff0c;是为教学和科研服务的学术性机构。它履行搜集、加工、存贮和传播知识信息的职能&#xff0c;与各系资料室互为补充&#xff0c;共同承担为教学和科研提供文献情报资料保障的…...

2025 年“认证杯”数学中国数学建模网络挑战赛 A题 小行星轨迹预测

近地小行星&#xff08; Near Earth Asteroids, NEAs &#xff09;是轨道相对接近地球的小行 星&#xff0c;它的正式定义为椭圆轨道的近日距不大于 1.3 天文单位&#xff08; AU &#xff09;的小行星。 其中轨道与地球轨道最近距离小于 0.05A 且直径大于 140 米的小行星被…...

PhotoShop学习09

1.弯曲钢笔工具 PhotoShop提供了弯曲钢笔工具可以直观地创建路径&#xff0c;只需要对分段推拉就能够进行修改。弯曲港币工具位于工具面板中的钢笔工具里&#xff0c;它的快捷键为P。 在使用前&#xff0c;可以把填充和描边选为空颜色&#xff0c;并打开路径选项&#xff0c;勾…...

远程管理命令:关机和重启

关机/重启 序号命令对应英文作用01shutdown 选项 时间shutdown关机 / 重新启动 一、shutdown shutdown 命令可以安全关闭 或者 重新启动系统。 选项含义-r重新启动 提示&#xff1a; 不指定选项和参数&#xff0c;默认表示 1 分钟之后 关闭电脑远程维护服务器时&#xff0…...

用Perl和HTTP::Tiny库的爬虫

HTTP::Tiny是Perl的一个轻量级HTTP客户端&#xff0c;适合简单的请求&#xff0c;但不像LWP那样功能全面&#xff0c;不过对于基本需求应该足够了。 首先&#xff0c;我需要熟悉HTTP::Tiny的基本用法。比如如何发起GET请求&#xff0c;设置user-agent&#xff0c;处理响应。用…...

MPP 架构解析:原理、核心优势与对比指南

一、引言&#xff1a;大数据时代的数据处理挑战 全球数据量正以指数级增长。据 Statista 统计&#xff0c;2010 年全球数据量仅 2ZB&#xff0c;2025 年预计达 175ZB。企业面临的核心挑战已从“如何存储数据”转向“如何快速分析数据”。传统架构在处理海量数据时暴露明显瓶颈…...

2025.04.10-拼多多春招笔试第三题

📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 03. 数字重排最大化问题 问题描述 LYA是一位专业的数字设计师。她手中有两个数字序列 s 1 s_1...

前端-vue2核心

官网网址Vue2 安装 — Vue.js 搭建环境 第一种方式&#xff08;刚开是接触Vue&#xff09; 我们看官网&#xff0c;可以直接在script引入vue版本。这里有两个版本&#xff0c;开发版和生产版本。我们两个都下载。 然后创建一个项目&#xff0c;将下载的生产版本和开发版本粘…...

基于springboot的“协同过滤算法的高考择校推荐系统”的设计与实现(源码+数据库+文档+PPT)

基于springboot的“协同过滤算法的高考择校推荐系统”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;springboot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统功能结构图 局部E-R图 系统…...

制作前的关键筹备:考试考核系统之核心要点

明确系统使用目的​ 制作考试考核系统前&#xff0c;企业需明确系统使用目的&#xff0c;这是开发基石&#xff0c;不同目的决定系统功能特性。用于员工培训考核时&#xff0c;系统要与培训内容结合&#xff0c;能生成相应考题&#xff0c;检验员工知识掌握程度&#xff0c;具备…...

【动手学深度学习】现代卷积神经网络:ALexNet

【动手学深度学习】现代卷积神经网络&#xff1a;ALexNet 1&#xff0c;ALexNet简介2&#xff0c;AlexNet和LeNet的对比3&#xff0c; AlexNet模型详细设计4&#xff0c;AlexNet采用ReLU激活函数4.1&#xff0c;ReLU激活函数4.2&#xff0c;sigmoid激活函数4.3&#xff0c;为什…...

Linux自启动脚本 systemctl

1.编写好脚本 #!/bin/bash /home/china/Linux/code/a.out2. 创建 Systemd 服务文件 sudo gedit /etc/systemd/system/my_script.service3.编写服务配置 将以下内容写入文件&#xff08;根据需求修改字段&#xff09;&#xff1a; [Unit] DescriptionMy Custom Shell Script…...

2024年KBS SCI1区TOP:信息增益比子特征分组赋能粒子群算法ISPSO,深度解析+性能实测

目录 1.摘要2.信息度量3.改进策略4.结果展示5.参考文献6.代码获取 1.摘要 特征选择是机器学习中的关键预处理步骤&#xff0c;广泛应用于实际问题。尽管粒子群算法&#xff08;PSO&#xff09;因其强大的全局搜索能力被广泛用于特征选择&#xff0c;但要开发一种高效的PSO方法…...

餐饮厨房开源监控安全系统的智能革命

面对日益严格的合规要求和消费者对卫生的信任危机&#xff0c;传统人工监督已力不从心&#xff1a;卫生死角难发现、违规操作难追溯、安全隐患防不胜防。如何让后厨更透明、更安全、更可信&#xff1f;餐饮厨房视频安全系统横空出世&#xff01;这套系统融合实时监控与AI技术&a…...

Ansys Electronics 变压器 ACT

你好&#xff0c; 在本博客中&#xff0c;我将讨论如何使用 Ansys 电子变压器 ACT 自动快速地设计电力电子电感器或变压器。我将逐步介绍设计和创建电力电子变压器示例的步骤&#xff0c;该变压器为同心组件&#xff0c;双绕组&#xff0c;采用正弦电压激励&#xff0c;并应用…...

Redis与Lua原子操作深度解析及案例分析

一、Redis原子操作概述 Redis作为高性能的键值存储系统&#xff0c;其原子性操作是保证数据一致性的核心机制。在Redis中&#xff0c;原子性指的是一个操作要么完全执行&#xff0c;要么完全不执行&#xff0c;不会出现部分执行的情况。 Redis原子性的实现原理 单线程模型&a…...

Shell 脚本开发从入门到实战

第1章&#xff1a;什么是 Shell 与 Shell 脚本&#xff1f; 一、Shell 是什么&#xff1f; Shell 是一个命令解释器&#xff0c;是你在 Linux 里敲命令的地方。你平时用的命令如 cd、ls、echo&#xff0c;其实都由 Shell 来解析执行。最常见的 Shell 是 Bash&#xff0c;绝大…...

宇视设备视频平台EasyCVR打造智慧酒店安防体系,筑牢安全防线

一、需求背景 酒店作为人员流动频繁的场所&#xff0c;对安全保障与隐私保护有着极高的要求。为切实维护酒店内部公共区域的安全秩序&#xff0c;24小时不间断视频监控成为必要举措。通常情况下&#xff0c;酒店需在本地部署视频监控系统以供查看&#xff0c;部分连锁酒店还希…...

深度解读分销小程序商城源码系统:从搭建到运营的关键指南​​​​

在移动互联网浪潮的席卷下&#xff0c;电商领域持续变革与创新。分销小程序商城凭借其独特优势&#xff0c;如依托社交平台流量、便捷的购物体验、高效的分销推广模式等&#xff0c;成为众多企业和创业者开展线上业务的热门选择。深入了解分销小程序商城源码系统&#xff0c;从…...

BeeWorks:打造安全可控的企业内网即时通讯平台

在数字化办公时代&#xff0c;企业对即时通讯工具的需求日益增长&#xff0c;尤其是对数据安全和隐私保护有严格要求的行业&#xff0c;如金融、政府、医疗等。BeeWorks 作为一款专注于内网部署的即时通讯软件&#xff0c;凭借其卓越的安全性、稳定性、丰富的功能以及全面的信创…...

微信小程序开发:废品回收小程序-功能清单

用户端&#xff1a;便捷体验&#xff0c;触手可及 废品百科与估价指南&#xff1a;平台以直观的方式展示各类废品的分类标准与实时市场价格&#xff0c;让用户轻松掌握废品价值&#xff0c;决策更从容。 一键预约&#xff0c;轻松回收&#xff1a;用户只需轻触屏幕&#xff0c…...

【Grok 大模型深度解析】第一期:技术溯源与核心突破

一、Grok的技术基因:从Transformer到混合架构的演进 1.1 Transformer架构的局限性 2017年Google提出的Transformer架构彻底改变了自然语言处理领域,其自注意力机制(Self-Attention)在长序列建模上表现优异。然而,随着模型规模的增大,传统Transformer暴露出以下问题: 计…...

性能比拼: Redis vs Memcached

本内容是对知名性能评测博主 Anton Putra Redis vs Memcached Performance Benchmark 内容的翻译与整理, 有适当删减, 相关指标和结论以原作为准 在本视频中&#xff0c;我们将对比 Redis 和 Memcached。我会介绍一些功能上的不同&#xff0c;但主要关注 性能。 首先&#xf…...

Mujoco xml actuator

actuator general&#xff08;通用执行器&#xff09;motor&#xff08;电机执行器&#xff09;position&#xff08;位置伺服&#xff09;velocity&#xff08;速度伺服&#xff09;intvelocity&#xff08;积分速度伺服&#xff09;damper&#xff08;主动阻尼器&#xff09;…...

Mybatis Plus分页查询返回total为0问题

概述 最近开发公司新项目&#xff0c;使用 Mybatis Plus 分页&#xff0c;发现总数和总页数为0&#xff0c;在此记录问题和解决方案。 添加 MybatisPlusConfig /*** author: lanys* version: 1.0* 创建时间&#xff1a;2025年4月9日 14:24:40* Description: MybatisPlus分页…...

多卡分布式训练:torchrun --nproc_per_node=5

多卡分布式训练:torchrun --nproc_per_node=5 1. torchrun 实现规则 torchrun 是 PyTorch 提供的用于启动分布式训练作业的实用工具,它基于 torch.distributed 包,核心目标是简化多进程分布式训练的启动和管理。以下是其主要实现规则: 进程启动 多进程创建:torchrun 会…...

网络层-IP地址计算

例1&#xff1a;IP地址二进制与十进制互转 题目&#xff1a; 将二进制IP 11000000.10101000.00000001.00001010 转换为点分十进制。将IP地址 172.16.254.1 转换为二进制格式。 答案与解析&#xff1a; 转换步骤&#xff1a; 每个8位二进制转为十进制&#xff1a; 11000000 →…...

BeagleBone Black笔记

目录 参考资料开机led控制GPIO输入输出插网线联网安装gcc编译工具镜像备份验证备份完整性将内存卡插入目标BBBboot启动开关 参考资料 链接: BeagleBone Black使用&#xff08;一&#xff09;&#xff1a;狗板简介 链接: 使用Beaglebone Black的IO口 开机 直接用usb连接到电脑…...

【25软考网工笔记】第一章 计算机网络概述

目录 一、计算机网络发展与分类 1. 计算机网络形成和发展 1&#xff09;ICT 2&#xff09;计算机网络的发展 3&#xff09;我国互联网发展 2. 计算机网络分类 1&#xff09;通信子网和资源子网 2&#xff09;PAN、LAN、MAN、WAN 3&#xff09;其他分类方式 3. 计算机…...

Soybean Admin 配置vite兼容低版本浏览器、安卓电视浏览器(飞视浏览器)

环境 window10 pnpm 8.15.4 node 8.15.4 vite 5.1.4 soybean admin: 1.0.0 native-ui: 2.38.0 小米电视 MIUI TV版本&#xff1a;MiTV OS 2.7.1886(稳定版) 飞视浏览器&#xff1a;https://www.fenxm.com/1220.html在小米电视安装飞视浏览器可以去小红书查安装教程&#xff1a…...

MicroPython 开发ESP32应用教程 之 I2S、INMP441音频录制、MAX98357A音频播放、SD卡读写

本课程我们讲解Micropython for ESP32 的i2s及其应用&#xff0c;比如INMP441音频录制、MAX98357A音频播放等&#xff0c;还有SD卡的读写。 一、硬件准备 1、支持micropython的ESP32S3开发板 2、INMP441数字全向麦克风模块 3、MAX98357A音频播放模块 4、SD卡模块 5、面包板及…...

从零到一:基于DeepSeek-R1的智能贪吃蛇开发实战

《基于DeepSeek-R1的AI驱动高性能贪吃蛇游戏开发全流程解析》 一、技术选型与环境搭建 开发工具链 • 编辑器:VSCode/Sublime(支持代码生成插件) • 运行环境:Node.js v16+(用于API调用及后端服务) • 图形库:HTML5 Canvas(网页端)或OLED驱动(单片机场景) • AI引擎…...

数据结构与算法-动态规划-区间dp,状态机dp,树形dp

3-区间 DP 介绍 通常用 (dp[i][j]) 表示区间 ([i, j]) 上的某种最优值&#xff0c;比如 (dp[i][j]) 可以表示从下标 (i) 到 (j) 的元素进行某种操作所得到的最大收益、最小花费等。 状态转移方程&#xff1a;这是区间 DP 的关键。它描述了如何从较小的区间的最优解得到较大区…...

文件内容课堂总结

Spark-Core编程 Key-Value类型&#xff1a; partitionBy函数根据指定Partitioner重新进行分区&#xff0c;默认使用HashPartitioner groupByKey函数根据key对value进行分组&#xff0c;有三种函数签名 reduceByKey函数将数据按相同Key对Value进行聚合&#xff0c;与groupByKey相…...

【树莓派Pico FreeRTOS】-任务通知

任务通知 文章目录 任务通知1、硬件准备2、软件准备3、FreeRTOS的任务通知介绍4、任务通知数据传输实例RP2040 由 Raspberry Pi 设计,具有双核 Arm Cortex-M0+ 处理器和 264KB 内部 RAM,并支持高达 16MB 的片外闪存。 广泛的灵活 I/O 选项包括 I2C、SPI 和独特的可编程 I/O (…...

c++11新内容补充

1.列表初始化 1.1传统{ }初始化 c98的{ }初始化主要是用于数组&#xff0c;以及结构体 1.2c11{ }初始化 1.让内置类型和自定义类型都可以用{ }实现多个数据初始化&#xff0c;而自定义类型的实现原理是类型转换&#xff08;没优化的版本是先构造临时对象&#xff0c;然后拷贝构…...

动态规划基础

动态规划 动态规划概论楼梯最短路最长上升子序列&#xff08;LIS)最长公共子序列&#xff08;LCS)最长回文子串 概率动态规划区间动态规划石子合并括号序列石子合并&#xff08;环形&#xff09; 树形动态规划统计人数没有上司的舞会 背包01背包完全背包多重背包分组背包 动态规…...

导入 Excel 批量替换文件名称及扩展名

重命名的需求是多种多样的&#xff0c;我们一个方法或一个工具很难说完全满足 100% 的文件重命名的需求。如果我们的文件重命名的需求非常的复杂的时候&#xff0c;我们能否有一个万全的方法来帮我们实现呢&#xff1f;那今天就给大家介绍一下导入 excel 的方式批量修改文件名称…...

降低AIGC检测率的AI润色提示词模板

以下是针对降低AIGC检测率的 AI润色提示词模板&#xff0c;涵盖语言风格优化、逻辑重构、学术规范强化等维度&#xff0c;结合反检测策略设计&#xff0c;可直接用于DeepSeek等工具&#xff1a; 一、标题与摘要优化 1. 标题去AI化 提示词&#xff1a; 请将以下标题改写成更学…...

系统思考—提升解决动态性复杂问题能力

感谢合作伙伴的信任推荐&#xff01; 客户今年的人才发展重点之一&#xff0c;是提升管理者应对动态性、复杂性问题的能力。 在深入交流后&#xff0c;系统思考作为关键能力模块&#xff0c;最终被纳入轮训项目——这不仅是一次培训合作&#xff0c;更是一场共同认知的跃迁&am…...

spring--整合Mybatis详解

整合Mybatis 步骤&#xff1a; 1.导入相关Maven依赖 junit mybatis mysql数据库连接 spring相关的 aop织入 mybatis-spring 2.编写配置文件 3.测试 回忆mybatis 还需连接数据库 导入依赖&#xff1a; <dependencies><dependency><groupId>juni…...

深入理解 HTML5 Audio:网页音频播放的新时代

在网页开发领域,音频的嵌入和播放一直是一个重要且不断演进的话题。HTML5 的出现,为网页音频播放带来了标准化的解决方案,极大地改善了开发者和用户的体验。 一、HTML5 之前的音频播放状况 在 HTML5 诞生之前,互联网上缺乏统一的网页音频播放标准。当时,大多数音频播放依…...

Cloudflare 缓存工作原理

Cloudflare 缓存是 Cloudflare 内容分发网络&#xff08;CDN&#xff09;的一个关键组成部分&#xff0c;通过在靠近用户的全球网络边缘服务器上存储和交付内容&#xff0c;显著提升网站性能。以下是关于 Cloudflare 缓存的相关内容&#xff1a; 工作原理 内容请求&#xff1a…...