MySQL事务介绍
- 为什么要有事务
- 什么是事务
- 事务的本质
- 事务的版本支持
- 事务的提交方式
- 事务的基本操作
-
- 开始事务
- 手动提交事务
- 设置保存点
- 回滚事务
- 创建测试表
- [演示事务的基本操作 --正常情况](#演示事务的基本操作 --正常情况)
- 非正常退出--未提交
- 非正常退出--已提交事务
- 单条sql与事务的关系
- 事务的隔离性理论1--使用介绍
-
- 什么是隔离性
- 为什么需要隔离性
- 隔离等级介绍
- 事务隔离级别的查看与设置
- 验证四种隔离等级
-
- [读未提交--READ UNCOMMITTED](#读未提交--READ UNCOMMITTED)
- [读提交--READ COMMITTED](#读提交--READ COMMITTED)
- [可重复读--Repeatable Read](#可重复读--Repeatable Read)
- 串行化--Serializable
- 事务的隔离性理论2--原理剖析
-
- 常见的并发场景
- 多版本并发控制(MVCC)
-
- MySQL表中的三个隐藏字段
- undo日志
- [Read View](#Read View)
- 综合案例
- [RR 与RC隔离级别的本质区别](#RR 与RC隔离级别的本质区别)
为什么要有事务
我们在数据库中创建的表,是可能同时有不同的客户端来访问的,因为客户端不仅仅有命令行式的 ,还有编程语言运行起来的作为客户端 、图像化界面的。
这就意味着,未来我们都表创建了,是极有可能在某一个时间段内,同时被多个客户端访问的,而数据库中的表就成了共享的数据,如果不加锁同时进行CURD操作就会导致数据不一致的情况,所以,事务的目的是为了保证数据的完整性和一致性,数据库将原本由语言来控制的事情,代替它做了。(高并发场景)
什么是事务
-
事务是MySQL中的多个(或一个)sql语句 。如果这些语句都被成功执行,并且成功提交后,将会永久更改数据;否则将会
Rollback(回滚到之前的版本)。 -
事务的四大属性:
- 原子性 (Atomicity):原子性确保事务是一个不可分割的工作单位;事务中的操作要么全部完成,要么全部不执行,不存在部分完成的情况。如果事务过程中遇到错误,系统会自动回滚到事务开始前的状态。
- 一致性 (Consistency):一致性保证了事务必须将数据库从一种一致状态转换为另一种一致状态。也就是说,在事务开始之前和结束之后,数据的状态是可预测的并且数据是完整的,符合预期。
- 隔离性 (Isolation):隔离性要求一个事务的影响在其完全执行完毕之前对其他事务是不可见的。这通过锁机制实现,防止并发事务之间的相互干扰。
- 持久性 (Durability):持久性意味着一旦事务被提交,它所做的修改就会永久保存在数据库中,即使系统发生故障也不会丢失。
目前我们无法get到这四大属性是什么意思。
但有几点可以明确的:
- 上面的四大属性是
MySQL帮我们维护的。 - 原子性 、持久性 、隔离性
MySQL设计了一些策略来实现,但是对于一致性 并没有直接的策略,前三个策略是一致性的保证,另外一致性还需要上层程序员的正确业务实现。
所以最终我们得到一个结论:在ACID属性加持下的一批SQL语句就叫做事务,这是我们目前对于事务的简单理解。
事务的本质
在同一段时间内,是否会可能有多个客户端在同一个数据库中开启了事务呢?
这是肯定有可能的 ,所以MySQL就必须对事务做管理,先描述后组织,所以事务的另外一种理解,就可以将其理解为我们语言级别的对象或者结构体。
事务的版本支持
使用下面的sql语句,可以查看MySQL中哪些存储引擎支持事务:
sql
show engines \G;

事务的提交方式
事务有两种提交方式:自动提交 和手动提交。
但是两者针对的不是一个东西,我们后面谈事务的操作时会详谈。
使用下面的sql查看自动提交是否开启:
sql
SHOW VARIABLES LIKE 'autocommit';

事务的基本操作
开始事务
开启事务有以下两种方法:
sql
-- 方法1
START TRANSACTION;
-- 方法2
begin;
手动提交事务
sql
commit;
设置保存点
设置保存点就类似于打了一个标记,后续回滚操作可以回滚到指定的保存点处。
sql
-- savepoint_name是当前状态,保存点的名称
SAVEPOINT savepoint_name;
回滚事务
回滚操作的意思是返回之前的某个状态,如果没有指定保存点,就默认回滚到事务刚开始前的状态。
sql
ROLLBACK TO SAVEPOINT [保存点];
-- 或者直接回滚
ROLLBACK;
创建测试表
我们创建一个测试表,用于等一会的演示显示开始事务的操作:
sql
create table test24(
id int primary key auto_increment,
name varchar(20)
);
演示事务的基本操作 --正常情况
验证回滚机制:
-
我们先将事务的隔离级别全局设置为最低,为了便于演示:
sqlset global transaction isolation level READ UNCOMMITTED; -
重启后,查看当前会话的隔离级别:
sqlSELECT @@transaction_isolation;
- 默认是可重复读。
-
更改后变成读为提交:

-
创建两个
mysql客户端,登录后,进入u1数据库,手动新起一个事务:
-
插入一些数据,由于是读未提交隔离级别,所以双方即使在事务中也是可以看到对方新插入的数据:

-
再次插入数据,并设置一些保存点:

-
删除之后表变成空了,我们想回滚到断点
b到情况:
- 回滚之后数据变成保存点
b时的状态了。
- 回滚之后数据变成保存点
-
默认回滚不加保存点,就是回滚到事务开始前的状态,表应该是空表:

-
回滚到开始状态,再想回滚到后面的保存点是不被允许的,回滚后当前状态就是最新:

非正常退出--未提交
上述是正常情况事务的回滚机制,但是大部分情况我们会遇见网络直接中断,客户端退出,这个时候
MySQL会回滚吗,我们来验证下。
-
两个客户端都开启事务:

-
插入一些数据后,没有提交,左边客户端直接退出:

-
此时退出后,会回滚吗,我们查看右端客户端:

- 发现确实自动回滚了。
所以我们得出一个结论,MySQL中的事务如果没有提交,客户端直接崩溃了,为了保证数据的完整性和一致性,自动会回滚到事务开始到状态。
非正常退出--已提交事务
如果非正常退出前已经提交事务,那么数据就已经持久化,不会回退。

单条sql与事务的关系
我们前面谈到的两种提交方式:手动提交 与自动提交。
- 其中自动提交是影响单条
sql的。 - 只要我们手动开了一个事务,默认就是手动提交,不受自动提交是否开启的影响。
-
关闭自动提交:现在默认是开启的。
sqlshow variables like 'autocommit';
sqlset autocommit=0;
-
左边终端插入一条数据后,右边终端是可以看见的,但是直接退出后,右边终端再次查表发现无法看到刚刚插入的数据了,因为没有提交的事务如果客户端崩溃,会自动回滚:

-
如果我们插入数据后,手动提交或者已经打开自动提交后再次客户端退出,刚刚的数据就不会回滚了:

-
这说明
MySQL中的单条sql也会被封装成事务。 -
autocommit只会影响单条sql是否被提交,而不会影响我们通过begin自己手动创建的事务,也就是说自己手动创建的事务,必须手动提交。 -
通过上述实验我们验证了
MySQL事务的原子性 与持久性。原子性 :一条事务从开启到提交是不可中断的(要么全部完成,要不都不执行,不存在部分完成的情况),如果被中断
MySQL为了保证数据的一致性和完整性会回滚到这个事务之前的状态。持久性:一旦事务被提交就不可回滚了,因为修改的数据已经被持久化到磁盘。
事务的隔离性理论1--使用介绍
什么是隔离性
在数据库管理系统(DBMS)中,隔离性 (Isolation) 是事务ACID特性之一。它确保了并发执行的多个事务之间相互隔离,即一个事务的中间状态对其他并发事务是不可见的,直到该事务被提交。这样可以防止数据不一致的问题,并保证数据的正确性和完整性。
其实上面的概念摆出来,我们对于隔离性这个东西还是很模糊的,我们可以简单的感性的理解一下:
- 首先对于一个新起事务而言,虽然它是原子性的,但是它的执行肯定是有一个过程的,也就是需要时间,有的事务执行的快,很快就结束了,有的事务执行的慢,很久才结束。
- 事务是为了解决高并发场景下的数据不一致问题,隔离性作为事务的四大特性之一,肯定也是为了解决这个问题,我们可以暂时这样理解,所谓隔离性就是要让不同的事务通过到来的时间的不同,看到同一张表中的不同版本的数据。
因为并非所有的事务都需要看到所有版本的数据,就拿我们日常生活中发生的例子来看,可能有点不准确,但是很形象:
- 假设你正在某番看小说,你每次刷新小说的章节都只能看见作者已经发布的内容,这里的发布就可以理解为提交,刷新就可以理解为进行事务时的
select查询。 - 对于那些作者已经写完,但是还没有提交的,对于你来说是无法看到的(假设你们操作的区域是同一片区域)。
为什么需要隔离性
如果没有隔离性会导致什么问题呢?
事务没有隔离性会导致数据发生不一致的情况,具体的情况有:脏读 、幻读 、不可重复读 、丢失更新 、读写冲突。我们后续将会一一介绍。
隔离等级介绍
在事务的中,有了隔离性保证了不同的事务在运行期间不会相互干扰导致一系列数据不一致的问题,根据干扰程度的不同,将隔离性分成不同的等级。
- 读未提交(Read Uncommitted):这是最低的隔离级别,两个事务在运行时,可以看到互相未提交的数据。
- 读提交(Read committed):一个事务只能读取已经提交的数据,解决了"脏读"的问题。
- 可重复读(Repeatable Read) :在这个级别上,事务在整个生命周期内看到的数据是一致的,即使其他事务在此期间进行了更新或删除操作。解决了不可重复读的问题 ,但是
insert插入可能在低版本中出现幻读的问题,MySQL5.7版本已经解决。 - 串行读(Serializable):这是最高的隔离级别,完全避免了上述所有问题,事务按照顺序执行,如同只有一个事务在运行,性能开销大。
事务隔离级别的查看与设置
下面的指令,可能
MySQL不同版本有所差异。
我们当前的MySQL版本是最新的:

!caution
注意:从
MySQL 8.0开始,使用@@transaction_isolation替代了旧版本中的@@tx_isolation。如果您使用的是MySQL 5.7或更早版本,请使用@@tx_isolation。
-
查看隔离级别:
sql-- 查看全局隔离级别 SELECT @@GLOBAL.transaction_isolation; -- 查看局部隔离级别(只生效于当前会话) SELECT @@transaction_isolation;- 如果局部隔离级别和全局隔离级别不同,
MySQL会优先采用局部的隔离级别 ,采用就近原则。
- 如果局部隔离级别和全局隔离级别不同,
-
设置隔离级别:
sql-- 设置全局隔离级别 SET GLOBAL TRANSACTION ISOLATION LEVEL [隔离级别]; -- 设置当前会话的隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL [隔离级别];其中 隔离级别 可以是以下之一:
READ UNCOMMITTED:读未提交READ COMMITTED:读提交REPEATABLE READ(默认):可重复读SERIALIZABLE:串行化。
验证四种隔离等级
读未提交--READ UNCOMMITTED
上述在演示事务的确存在回滚这个操作时,我们将事务的隔离级别设置为了最低级别
READ UNCOMMITTED。
-
优点:提供了最高的并发度,因为不需要等待其他事务完成。
-
缺点:
- 脏读 (Dirty Reads):可以读取到其他事务未提交的数据,如果这些数据最终被回滚,则会导致不一致。
- 不可重复读 (Non-repeatable Reads):同一事务内两次读取相同的数据可能得到不同的结果,因为其他事务在这期间更新并提交了数据。
- 幻读 (Phantom Reads):在同一事务中执行相同的查询可能返回不同的行集,因为其他事务插入或删除了满足查询条件的新行。
-
适用场景:
- 实时分析和统计 :当进行非关键性的数据分析或者统计时,如果允许一定的不准确性,并且追求极高的性能,可以选择
READ UNCOMMITTED隔离级别。 - 临时数据处理 :对于一些临时数据处理任务,比如测试环境中的查询操作,可以接受较低的数据一致性要求,这时可以考虑使用
READ UNCOMMITTED来减少锁定开销。 - 快速查询需求:在某些情况下,为了获得最快的查询响应时间,即使牺牲一定的数据一致性也是可接受的,例如某些非关键业务的快速检索。
- 实时分析和统计 :当进行非关键性的数据分析或者统计时,如果允许一定的不准确性,并且追求极高的性能,可以选择
演示脏读
脏读其实我们之前已经演示过了。
同时打开两个客户端,隔离级别设置为读未提交,左边的客户端叫做A、右边的客户端叫做B。
-
设置当前会话的隔离等级为
READ UNCOMMITTED:
-
A、B手动开启事务:
-
A、B进入u1数据库,查看当前表中的数据:
-
A插入数据后,B读取A数据,发现读取到A未提交的数据:
-
A发现插入错误,回退到了事务开始前:
- 脏读是问题吗? :当然是问题,
B客户端读到了A未提交的数据,这个数据最终被回滚了,那么B就读到一个无效的数据,如果B与具体的业务相关联,那么问题可能就大了,**因为B**此时可能是上层的程序。
读提交--READ COMMITTED
读提交解决了读未提交脏读的问题,但是不可重复的问题仍然存在。
- 优点:防止了"脏读",即不会读取到未提交的数据。
- 缺点 :
- 不可重复读:同一事务内两次读取相同的数据仍可能得到不同的结果,因为其他事务在这期间更新并提交了数据。
- 幻读:在同一事务中执行相同的查询可能返回不同的行集,因为其他事务插入或删除了满足查询条件的新行。
- 适用场景 :
- 高并发环境 :
READ COMMITTED提供了较好的并发性能,适用于需要频繁读写操作的场景。 - 普通业务系统 :大多数常规业务系统(如订单管理、库存管理等)都可以使用
READ COMMITTED,因为它能有效防止脏读,同时允许适度的并发操作。 - 不需要严格一致性的分析任务:对于一些数据分析或报表生成任务,只要求读取已提交的数据即可,无需完全一致的视图。
- 实时性要求较高的应用:如在线交易、实时监控等场景,要求能够快速读取最新的已提交数据。
- 高并发环境 :
演示不可重复读
-
首先将
A、B的当前会话的隔离级别设置为READ COMMITTED:sql-- 设置当前会话隔离级别 set session transaction isolation level READ COMMITTED -- 查看 select @@transaction_isolation;
-
A、B开启一个事务,查看当前的表中的数据,A插入一行数据,B查看,发现看不到A未提交的数据:sql-- 对于终端A begin; select * from test24; insert test24 (name)values('张飞'); -- 对于终端B begin; select * from test24; -- 等A执行插入操作后 select * from test24;
-
因此
READ COMMITTED隔离级别是解决了脏读问题的,下面我们将A事务提交,B继续查看表中数据:sql-- 终端A commit; -- 终端B select * from test24;
B事务在同一事务中,执行同一查询sql,却查到不同的结果,这就是不可重复读问题。
不可重复读是问题吗 :A事务提交了,我不应该看到它的内容吗?这会导致什么问题呢?
- 在有些业务逻辑下,不应该看到。
- 例如:当前事务
B正在统计满足条件的学生的姓名,这个提交是学生的某科成绩,要将不同范围内的学生名单统计出来,假设此时事务A修改了某个学生C的成绩,使C到了不同的范围,而C在之前已经被统计到了,由于不可重复读的问题,最终C会出现两次,这显然不合理。
可重复读--Repeatable Read
为了解决上述不可重复读的问题,可重复读隔离级别就出现了。
-
优点 :解决了脏读 、不可重复读 的问题,在这个隔离级别内,整个事务生命周期内看到的数据时一致的,每条
SQL满足幂等性(不管执行多少次,只要在事务生命周期内,查询结果一定相同)。 -
缺点:幻读:虽然解决了不可重复读的问题,但在某些情况下,仍然可能出现"幻读",即其他事务插入了新的记录影响当前事务的查询结果。
-
应用场景:
-
需要保证在整个事务过程中读取的数据保持一致的应用程序。
-
对于那些需要避免由于并发事务导致的数据不一致问题的环境特别有用,例如财务处理、库存管理和报表生成等。
-
!tip
MySQL 的 InnoDB 存储引擎通过多版本并发控制(MVCC)技术,在这个级别上实际上也解决了"幻读"的问题,使得其实现比标准定义更加严格。
演示可重复读--解决不可重复问题
-
A、B将当前会话隔离级别设置为Repeatable Read:sql-- 设置 set session transaction isolation level Repeatable Read; -- 查看 select @@transaction_isolation;
-
A、B新起一个事务,并查看当前数据:
-
A删除一行数据、插入一行后提交,B查询到的结果和之前的一致:
- 的确解决了不可重复读 的问题,
MySQL事务的默认级别就是Repeatable Read。
串行化--Serializable
串行化,和它的名字一样,在这个隔离级别下,所以事务都是顺序执行的,不存在并发问题。
-
优点 :不存在任何并发导致的问题,如幻读 、脏读 、不可重复读。
-
缺点:性能不行,不适合高并发访问的场景。
-
应用场景:适合低并发访问的场景。
事务的隔离性理论2--原理剖析
上述主要从使用上介绍事务的隔离性理论,可我们对于事务的隔离性及其各种隔离级别的实现原理其实并不了解,下面着重来从原理上谈事务。
常见的并发场景
- 读读并发:多个客户端同时访问同一张表的数据(只读),没有并发问题,不需要控制。
- 读写并发 :多个客户端同时写或者读同一张表的数据,用并发问题,需要处理脏读 、不可重复读 、幻读等问题。
- 写写并发:多个客户端同时修改一张表的数据,有并发问题。
最常见的是读写并发,我们下面也主要谈读写并发。
多版本并发控制(MVCC)
多版本并发控制(MVCC, Multi-Version Concurrency Control)是一种用于数据库管理系统中提高并发性能和解决事务隔离问题的技术。MVCC允许数据的多个版本同时存在,使得读操作不会阻塞写操作,写操作也不会阻塞读操作,从而提高了系统的并发处理能力。
也就是说它是专门用来解决读写并发问题的,而且读写之间是不需要加锁的。
想完全理解MVCC前需要先了解下面三个点。
MySQL表中的三个隐藏字段
其实不止三个,最重要的是三个。
-
DB_ROW_ID(6Byre) :如果表中没有定义主键,
InnoDB会自动生成一个隐藏的主键列,这个列被称为DB_ROW_ID,它是一个自增的整数值,用来标记每一行都数据。 -
DB_TRX_ID(6Byte) :记录最后一个对该行数据进行修改的事务
ID,也就是说MySQL会给每个事务都分配一个事务ID,这个字段在多版本控制中用于判断某一行数据是否对当前事务可见。 -
DB_ROLL_PTR(7Byte):一个指针,指向该行数据的回滚段(Undo Log)记录,指向最近一次该行的旧版本。
除了上述三个,其实隐藏字段还有一些,如
flag标志位。
-
flag(1Byte) :用来标记某一行是否已经被删除。当执行
DELETE操作时,InnoDB并不会立即将该行从磁盘上删除,而是简单地设置这条记录的删除标志位。实际的空间回收会在后续的清理过程中完成(例如通过purge线程或手动执行OPTIMIZE TABLE命令),这样的做法是典型的空间换时间。purge是MySQL中的清除线程,负责从数据库中永久删除那些被标记为已删除且不再需要的旧版本数据,该过程不需要我们手动干预。
undo日志
简单理解,
undo日志本质就是一个缓冲区,用于保存日志数据。
当事务中涉及到对数据做修改时,就会先将原先的数据做一个备份,放入undo的缓冲区中,便于后续的一系列操作(如回滚)。
下面我们简单模拟一下MVCC:
-
现在有一个简单的学生表,表中初始时的数据时这样的:

-
此时来了一个事务6,它将
name修改了张飞,在修改前,需要将原始的数据备份到undo缓冲区中,当然并发访问的话还需要行级加锁因为涉及到修改数据:
-
事务6提交后,释放锁,此时又来了一个事务7,它需要将年龄修改为20岁:

-
这样我们不就形成了一个个基于链表记录的版本链。
-
后续如果想回滚,直接通过
DB_ROLL_PTR字段访问undo log中一条条的记录即可,所谓的回滚操作,不就是把undo log某个符合要求的版本替换为最新的版本吗?不是直接覆盖值,MySQL中其实是保存的相反操作的sql语句。 -
我们将每一个版本称为一个快照。
!caution
不用担心,
undo log空间不够的问题,当该行没有事务访问时,意味着它们都提交了,提交了的数据是持久化到磁盘中的,不能回滚,所以直接将历史版本释放就行了。
Read View
读视图也是多版本并发控制的重要一环,它决定了某个正在跑的事务,能够看到
undo log的哪些版本记录。不能让其看到所有的版本,这就是隔离性的体现。
快照读与当前读
-
快照读:读取的是历史版本。
-
当前读:总是读取最新的版本。
sql-- 第一种方式 SELECT column_list FROM table_name WHERE condition FOR UPDATE; -- 第二种方式 SELECT column_list FROM table_name WHERE condition LOCK IN SHARE MODE;
Read View的组成
Read View是事务中的一个数据结构,但并不是所有的事务都会有这个对象,只有第一次进行快照读时,该事务中才会创建这个对象,它有以下几个重要的字段:
- 高水位(m_low_limit_id ):没有写错,
MySQL这个字段确实是高水位,它等于当前能看到的事务ID+1,所以凡是大于等于m_low_limit_id的都是不可见的。 - 低水位(m_up_limit_id ):也没有写错,这是当前能看到的事务(并行运行的)
id的最小值,凡是小于这个事务id的都是历史已经提交的,可见。 - 活跃事务列表(m_ids) :包含创建
Read View对象时,正在运行的所有事务id。 - 当前读视图的事务ID**(reator_trx_id**) :创建该ReadView的事务ID。
!important
将
Read View类比进程地址空间,所谓的进程地址空间不就是标识某一个进程对于内存的可见性吗?所以
Read View就是标识某一个事务对于undo日志中历史版本的可见性。
简单总结
此时我们已经知道了以下几个点:
undo日志:用来记录某一行的历史版本。我们前面谈到过每一个行中都有一些隐藏列,其中有一个列叫做DB_TRX_ID,它是用来标记这个版本是由哪个事务修改的,它存储的是该事务的事务ID,这不就是未来我们进行快照读时判断能否读到该版本的重要依据吗???- Read View :读视图,它是在进行快照读的时候才会生成,它的三个主要字段,标识了哪些版本我们可以读,哪些不能读。
- 对于历史版本可见。
- 对于并发正在运行的事务的记录我们不可见。
- 事务是在我进行快照读后才到来的,我们也不可见。
下图以时间线的方式来阐述当前的快照读应该看到哪些版本:

!caution
误区 :正在活跃的事务
id不一定是连续的,因为有些事务虽然开始的晚,但是结束的早,已经提交的事务,不能添加到活跃事务列表中。
mysql 可见性分析算法 版本源码剖析:
综合案例
下面我们举个例子,来演示下多版本并发控制的流程。
假设现在有条记录(对于表中的一行数据,我们又称之为记录):
| age | name | DB_ROW_ID | DB_TRX_ID | DB_ROLL_PTR |
|---|---|---|---|---|
| 18 | 张三 | 1 | 1 | nullptr |
当前记录的事务列表,列表示时间线:
| 事务1(id = 1) | 事务2(id = 2) | 事务3(id = 3) | 事务4(id = 4) | 事务5(id = 5) | 事务6(id = 6) | 事务7(id = 7) |
|---|---|---|---|---|---|---|
| 已经提交 | 已经提交 | |||||
| 事务开始 | 事务开始 | 事务开始 | 事务开始 | |||
| ... | ... | ... | 事务修改数据并提交 | |||
| 进行中 | 进行中 | 快照读 | ||||
| ... | ... | ... | 事务开始 | |||
| 修改数据 |
-
事务6将记录中的
age从28修改为了28。 -
事务5进行快照读,数据库为它创建了
Read View对象:sql-- 事务5 Read View reator_trx_id = 5; m_low_limit_id = 6+1 = 7; -- 此时事务5是不知道事务7的存在的,因为它还没开始 m_up_limit_id = 3; m_ids = {3,4,5} -- 6不在里面因为6已经提交了,不是活跃事务 -
当事务6修改了行记录,版本链为:

-
当事务5进行快照读时,它会拿着
Read View的3个关键字段,与当前记录修改的事务id做比较,如何可见性为true,直接返回这条记录,就不去遍历更老的记录了:sql-- 此时对于事务6的记录 6 < m_low_limit_id(7) -- 继续判断 6 >= m_up_limit_id(3) -- 继续判断 6 不在集合 {3,4,5}中 -- 直接返回true -- 因此事务6修改的记录对于当前事务5来说是可见的,它刚好是最新记录。 -
如果快照读比较慢,刚好看见了事务7修改该记录提交的版本,那么此时按照可见性算法,该记录对于事务5就是不可见的。
RR 与RC隔离级别的本质区别
经过前面的演示,我们已经知道了
RR级别下,查询一条记录永远都只可能出现一种结果,这是如何实现的呢?上面我们谈MVCC并没有区分RR和RC,因为它们都使用到了它。
证明一条记录确实存在多个版本
-
终端
A、B,将隔离级别设置为RR级别:sql-- 设置当前会话的隔离级别为可重复读 SET SESSION TRANSACTION ISOLATION LEVEL repeatable read; -- 查看当前会话的隔离级别 SELECT @@transaction_isolation;
分别开启一个事务,查看当前表
test24中的内容:
-
终端
A删除id = 14的列,正常情况下,A提交后,终端B也是无法看到其内容的(如果之前已经进行过快照读):
-
上述
select默认是快照读,是会创建Read View走可见性判断算法的,我们使用当前读,就可以看到最新的记录:sqlSELECT * FROM test24 LOCK IN SHARE MODE; -- 或者 SELECT * FROM test24 FOR UPDATE;
上述实验验证了多版本是确实存在的,所谓的隔离不就是让不同的事务按照时间看到不同的记录的版本吗?
RR级别下的神奇现象
观察这个现象,将让你理解
RR、RC的底层原理的区别。
RR级别又叫不可重复读,也就是在同一个事务中,执行同一个sql具有幂等性。
-
上述实验我们已经将隔离级别设置成为
RR级别,直接重新开始事务即可:
-
终端
A修改id = 17的name为张飞,然后提交,按照预期终端B是无法看见的:
但是按照我们上面的MVCC,此时如果进行快照读,事务A已经结束,如果是进行快照读,B应该可以看到修改的内容才对呀,这是什么情况呢?别着急,我们继续进行下面的实验。
-
还是一样,终端A与B同时开启事务,此时隔离级别都是可重复读:

-
此时
B先不进行快照读,等A先修改一行数据提交后,再进行快照读:
-
此时
B第一次开始快照读:
-
这也符合我们的预期呀,
A已经提交了,它提交的版本,我们本来就应该能看到,但是此时如果事务A又重新启动,再次修改数据并提交,B下一次快照读,就无法读到这个记录了,因为它要保证可重复读:
结论
- 每次进行快照读都会创建
Read View对象,RR级别下,Read View至多被创建一次,在第一次快照读的时刻,自此之后Read View对象就不变了,因此该事务对于undo log中该行记录的可见性也就固定了,自然每次读到的结果都是一样的。 - 但是RC(读提交)级别则不同,虽然它也使用
MVCC机制来实现读写并发,但是它每一次进行快照读时都会重新创建Read View对象,所以它的可见性一直在变化,自然每次读到的结果就可能不同。 - 多版本并发控制,由于读写读到的是不同版本,不会出现并发问题,所以不用加锁,但是写写可以认为是当前读,要加锁。