【MySQL事务】事务的隔离级别
目录
一.事务的隔离性
二. 事务的隔离级别
三. 脏读,不可重复读,幻读
3.1.脏读
3.2.不可重复读
3.3.幻读
四.多版本并发控制——MVCC
4.1 实现原理
4.1.1.版本链
4.1.2.ReadView
4.2 MVCC是否可以解决不可重复读与幻读
三. 查看并设置隔离级别
四. READ UNCOMMITTED - 读未提交与脏读
五. READ COMMITTED - 读已提交与不可重复读
六. REPEATABLE READ - 可重复读与幻读
七. SERIALIZABLE - 串行化
八.不同隔离级别的性能与安全
一.事务的隔离性
隔离是为了把隔离对象分隔开,防止被隔离对象之间相互影响。
MySQL服务可以同时被多个客户端访问,每个客户端执行的DML语句以事务为基本单位,那么不同的客户端在对同一张表中的同一条数据进行修改的时候就可能出现相互影响的情况,为了保证不同的事务之间在执行的过程中不受影响,那么事务之间就需要相互隔离,这种特性就是隔离性。
并发操作是一会执行线程A,一会执行线程B.
二. 事务的隔离级别
事务具有隔离性,那么如何实现事务之间的隔离?隔离到什么程度?如何保证数据安全的同时也要兼顾性能?这都是要思考的问题。
如果大家学习过多线程技术,都知道在并发执行的过程中,多个线程对同一个共享变量进行修改时,在不加限制的情况下会出现线程安全问题,我们解决线程安全问题时,一般的做法是通过对修改操作进行加锁;
同理,多个事务在对同一个表中的同一条数据进行修改时,如果要实现事务间的隔离也可以通过锁来完成,在MySQL中常见的锁包括:读锁、写锁、行锁、间隙锁、Next-Key锁等,不同的锁策略联合多版本并发控制可以实现事务间不同程度的隔离,称为事务的隔离级别;
不同的隔离级别在性能和安全方面做了取舍,有的隔离级别注重并发性,有的注重安全性,有的则是并发和安全适中;
在MySQL的InnoDB引擎中事务的隔离级别有四种,分别是:
- READ UNCOMMITTED,读未提交
- READ COMMITTED,读已提交
- REPEATABLE READ,可重复读(默认)
- SERIALIZABLE,串行化
三. 脏读,不可重复读,幻读
3.1.脏读
什么是脏读?
我们来彻底聊聊数据库中的“脏读”,用一个生活化的故事把它讲明白:
想象一下银行的柜台操作:
-
场景: 你(客户A)去银行柜台,想把账户里的 1000 块钱转到朋友(客户B)的账户。柜员(代表数据库的一个事务,我们叫它事务A)开始操作。
-
操作步骤:
-
柜员(事务A)先从你的账户扣除了 1000 块。这时,你的账户余额显示减少了 1000。
-
柜员(事务A)正准备把这 1000 块加到朋友B的账户里。但就在他操作“加钱”这一步之前,发生了一件事...
-
-
另一个查询: 与此同时,银行的另一个系统(比如一个自动生成余额报表的程序,代表另一个事务,叫它事务B)正在读取所有账户的当前余额,用来生成一份即时的账户余额报告。
-
“脏读”发生了: 事务B在读取数据时,恰好读到了事务A修改了一半的状态:
-
它看到你的账户已经被扣了 1000 块(余额变少了)。
-
但它没看到朋友B的账户还没收到这 1000 块(余额还没增加)。
-
-
关键问题: 事务B读取并记录了这个状态:你账户少了1000,朋友B账户没变。然后它基于这个数据生成了报告。
-
意外转折(事务回滚): 就在这时,柜员(事务A)在操作朋友B的账户时,突然发现朋友B的账户状态异常(比如被冻结了),或者系统出了个小故障。柜员决定取消这次转账操作(事务A回滚)。这意味着:
-
他把你账户扣掉的 1000 块又加回去了(恢复原状)。
-
朋友B的账户自然也没有增加钱。
-
-
后果: 现在,问题来了:
-
那份由事务B生成的账户余额报告已经发出去了。
-
报告里显示你的账户余额是扣款之后的状态(少了1000),朋友B的余额没变。
-
但实际上,因为转账被取消了(事务A回滚),你的钱一分没少,朋友B的钱也一分没多!
-
这份报告里关于你账户余额的数据,就是完全错误、不存在、或者说“脏”的数据。它记录了一个从未真正在数据库中持久存在过的中间状态。
-
所以,什么是脏读?
-
脏读就是一个事务(事务B)读到了另一个正在进行中且尚未提交的事务(事务A)所修改的数据。
-
关键在于,那个被读取的数据所在的“源事务”(事务A)后来可能被撤销(回滚)了!
-
因此,事务B读到的数据是无效的、临时的、未被最终确认的——就像银行报表记录了你被扣款但钱其实没转出去一样。这份数据是“脏”的,因为它不代表数据库最终、真实、一致的状态。
-
脏读发生在数据库隔离级别设置得比较低(通常是“读未提交”)的情况下。更高级别的隔离(如“读已提交”)会阻止脏读,确保事务只能读到其他事务已经最终提交的数据,避免了读到这种“半成品”或可能被撤销的数据。
简单来说:脏读就是不小心看到了别人正在写但还没保存(甚至可能丢弃)的草稿,并错误地把它当成了最终稿。 这会导致基于这份“草稿”做出的判断或报告完全错误。
哪些隔离级别最容易发生脏读
READ UNCOMMITTED,读未提交这个隔离级别就非常容易触发脏读,其他隔离级别不会发生脏读
3.2.不可重复读
什么是不可重复读?
想象一下学校公布期末成绩的场景:
-
第一次查看(事务开始): 你(学生A)登录学校成绩系统查询自己的数学成绩。系统显示 85分(代表一次数据库查询)。你很高兴,截图保存了这一刻。
-
幕后修改(另一个事务提交): 与此同时,老师(代表另一个已提交的事务)在审核成绩时,发现你的某道大题批改有误,应该多给你5分。于是老师将你的数学成绩从 85分 修改为 90分,并确认提交了这次修改(事务提交成功)。现在数据库里你的真实成绩就是90分了。
-
第二次查看(同一个事务内): 过了一会儿,你想再确认一下成绩,在同一个登录会话里(同一个事务内),再次刷新页面查询数学成绩。这次系统显示 90分!
-
困惑发生: 你懵了:“我刚才明明看到是85分,怎么现在变成90分了?系统出错了?我眼花了?” 你甚至怀疑第一次看到的85分是不是幻觉。你试图用第一次的截图证明,但系统现在坚持是90分。
这就是“不可重复读”——在同一个事务里面多次读取同一个数据,却发现查询的结果不一样。
-
核心问题: 在 同一个事务(你这次登录会话)中,你 多次读取同一个数据项(你的数学成绩)。
-
结果不一致: 你读到了 两个不同的值(第一次85分,第二次90分)。
-
原因: 在你的事务进行过程中,另一个已经成功提交的事务(老师修改成绩)修改并提交了这个数据!数据库允许你看到其他事务提交后的最新数据(取决于隔离级别)。
-
与脏读的关键区别:
-
脏读 读到的是别人没提交的、可能被撤销的“草稿”数据(如转账中途扣款但未加款的状态)。
-
不可重复读 读到的是别人已经成功提交的、确凿无疑的修改(老师确实把85改成了90并生效了)。问题在于你同一个事务内读到的结果不一致了。
-
为什么这是个问题?
-
逻辑混乱: 就像例子中的学生,无法相信自己的眼睛,导致困惑和不信任。
-
决策错误: 设想你在根据第一次读到的85分计算自己的平均分或奖学金资格,并基于此做了决定(比如申请复查)。结果第二次读变成90分,你的计算和决策基础就错了。
-
数据一致性破坏: 在一个事务内部,对同一数据的多次读取结果应该是一致的。不可重复读破坏了这个一致性预期。
数据库如何避免它?
-
这发生在 “读已提交” 隔离级别(避免了脏读,但允许不可重复读)。
-
要避免不可重复读,需要将隔离级别提升到 “可重复读” 。
-
在 “可重复读” 级别下,数据库会在你事务第一次读取某个数据时,就“记住”它当时的值(通过多版本并发控制MVCC等机制)。
-
之后在同一个事务内,无论其他事务是否修改并提交了该数据,你再读取它时,看到的仍然是第一次读取时的那个旧值(85分)。就像系统给你拍了一张第一次查询时的成绩快照,之后只给你看这张快照。直到你的事务结束并重新开始一个新事务,你才会看到新的90分。
-
一句话总结不可重复读:
在同一个事务里,你满怀信心地回头想确认一下刚才看到的数据,却发现它在你眼皮底下“变”了——因为别人已经合法地修改了它并成功提交。数据库的“可重复读”隔离级别就是给你一个“事务内快照”,让你在同一个事务里每次回头看,数据都保持你第一次看到的样子。
哪些隔离级别最容易发生不可重复读?
最容易发生不可重复读的隔离级别是:
-
READ UNCOMMITTED (读未提交)
-
READ COMMITTED (读已提交)
原因分析:
-
READ UNCOMMITTED:
-
这是最低的隔离级别,没有任何措施来保证数据读取的一致性。
-
它不仅允许脏读,也必然允许不可重复读。因为事务A可以随时读取到事务B尚未提交的修改。如果事务B在A的两次读取之间修改了数据(即使未提交),A的两次读取结果就可能不同。
-
-
READ COMMITTED:
-
这个级别解决了脏读问题(只能读取已提交的数据)。
-
但它没有解决不可重复读。原因在于,它只保证单次读取看到的是已提交的数据,不保证在整个事务期间多次读取同一行时看到的是相同的数据。
-
机制:在事务A执行过程中,如果事务B修改了某行数据并成功提交,那么事务A后续再次读取该行时,就会看到事务B提交后的新值,导致两次读取结果不一致(不可重复读)。
-
至于剩下的REPEATABLE READ (可重复读)和SERIALIZABLE (串行化),根本不可能发生不可重复读
3.3.幻读
什么是幻读?
想象一下这个场景:
你是一个图书馆管理员。今天你要做两件相关的事:
-
上午任务: 你接到通知,要求清点所有“科幻小说”类别的书,看看一共有多少本。你仔细地一排排书架找过去,最后数出来是 50 本。你把这个数字记录在你的小本本上。
-
下午任务: 领导说,根据上午的清点,给所有“科幻小说”贴上新的标签。你拿着标签,再次走向科幻小说的书架,准备开始贴。
“幻读”就发生在你下午贴标签的时候:
-
当你走到书架前,惊讶地发现,科幻小说的区域里多出来好几本新书!这些书是上午你清点之后,另一个图书管理员(或者系统)新采购上架的,它们也属于“科幻小说”。
-
你上午明明数的是50本,也确实是当时货真价实的50本。但现在下午一看,变成了55本!这多出来的5本,就像是凭空“变”出来的一样,让你感觉自己上午是不是眼花了,出现了“幻觉”——明明数过是50,怎么现在变成55了?
这就是幻读的核心体验:
-
你做了第一次查询(读操作): 上午清点科幻小说数量(50本)。
-
在两次操作之间,系统里“偷偷”插入了新东西: 另一个管理员在你清点完后,又上架了5本科幻小说(插入新行)。
-
你做了第二次操作(通常是基于第一次结果的修改): 下午你根据上午记录的50本去贴标签。
-
你看到了“幻觉”: 在贴标签的过程中(或者如果你下午再数一次),你发现了一些上午根本不存在的新记录(那5本书)。这些新记录就像幽灵一样突然出现了,让你感觉上午的查询结果“不准了”或者自己“看错了”,仿佛出现了幻觉。
关键点总结:
-
焦点在“新冒出来的行”: 幻读特指你第一次查询时不存在,但在你后续操作时却突然出现的新数据行。这和你两次读同一条数据,发现它被修改了(不可重复读)是不同的。
-
通常影响范围操作: 它常常在你执行一个基于之前查询结果的范围操作(比如贴标签、批量修改) 时被发现。你本以为操作的对象是固定的(上午那50本),结果操作过程中发现对象变多了(55本)。
-
感觉像“幻觉”: 这个名字很形象,因为你之前确认过的状态(50本),后面操作时发现“凭空”多出了一些东西,让你怀疑自己之前的认知。
-
并发问题: 根本原因是多个操作(你的清点、另一个管理员的上架、你的贴标签)在同时进行,系统没有很好地隔离它们,导致你看到了中间插入的、本不该影响你当前任务的新数据。
数据库怎么解决?
数据库有一种叫“可串行化”的最高隔离级别(就像给图书馆加了个神奇规则:在你上午开始清点到下午贴完标签整个期间,禁止任何其他管理员往科幻小说书架放新书)。或者用更高级的锁机制(比如范围锁),来保证在你第一次查询后,别人不能在你查询的范围内插入新数据,从而防止这些“幽灵”新书的出现,消除你的“幻觉”。
所以,简单记住:幻读就是你查询时没看到某些数据,过一会儿(在同一个任务里)做相关操作时,却“见鬼了”似的发现了一些第一次查询时根本不存在的、新插入的数据行,让你感觉像出现了幻觉。 它的核心在于“新插入的行”带来的不一致。
哪些隔离级别容易发生幻读?
在一般数据库中,以下隔离级别容易发生幻读:
-
READ UNCOMMITTED (读未提交)
-
READ COMMITTED (读已提交)
-
REPEATABLE READ (可重复读) - MySQL 默认级别
SERIALIZABLE (串行化) 隔离级别则基本不会发生幻读。
需要注意的关键点是: 虽然标准 SQL 理论中 REPEATABLE READ
级别可能发生幻读,但 MySQL 的 InnoDB 存储引擎对其进行了优化。InnoDB 通过使用 next-key 锁 机制(锁定目标行及其周围的间隙),在 REPEATABLE READ
级别下解决了大部分幻读问题。
四.多版本并发控制——MVCC
上一个小节介绍了实现事务隔离性的锁机制,但是频繁加锁与释放锁会对性能产生比较大的影响,为了提高性能,InnoDB与锁配合,同时采用另一种事务隔离性的实现机制 MVCC,即 Multi-Versioned Concurrency Control 多版本并发控制,用来解决肤读、不可重复读等事务之间读写问题,MVCC在某些场景中替代了低效的锁,在保证了隔离性的基础上,提升了读取效率和并发性。
事务的隔离性是通过锁和MVCC共同实现的。
4.1 实现原理
MVCC 基于 Undo Log 版本链和 ReadView 实现。
4.1.1.版本链
版本链
Undo Log做为回滚的基础,在执行Update或Delete操作时,会将每次操作的上一个版本记录在Undo Log中,每条Undo Log中都记录一个叫做 roll_pointer 的引用信息,通过 roll_pointer 就可以将某条数据对应的Undo Log组织成一个Undo链,在数据行的头部通过数据行中的 roll_pointer 与Undo Log中的第一条日志进行关联,这样就构成一条完整的数据版本链
如下图所示,Undo Log的具体结构和行结构请参考:【InnoDB磁盘结构3】撤销表空间,Undo日志-CSDN博客
每⼀条被修改的记录都会有⼀条版本链,体现了这条记录的所有变更,当有事务对这条数据进⾏修 改时,将修改后的数据链接到版本链接的头部,如下图中 UNDO3
版本链是以数据行为单位的,版本链在所有事务中共享,也就是说所有事务访问的都是同一条版本链。
4.1.2.ReadView
什么是ReadView?
在MVCC(多版本并发控制)机制中,为每条数据记录维护了由多个版本组成的版本链(Undo Log Chain)。当一个事务发起查询(如 SELECT
)时,核心问题在于:面对同一条数据的多个历史版本,当前事务究竟应该看到哪个版本?这就是 ReadView 所要解决的关键问题。
ReadView 的定义与作用
-
定义:ReadView 是事务在执行查询语句时(例如
SELECT
)动态创建的一个内存数据结构。 -
作用:它本质上是一个快照视图(Snapshot View),记录了该查询发起时刻数据库系统中所有活跃事务的全局状态信息。其核心价值在于:通过维护一组精炼的统计值,使得在遍历数据版本链进行可见性判断时,无需检查所有事务的完整状态或遍历整个链,从而极大提高了效率。
ReadView 包含的关键字段
一个 ReadView 结构通常包含以下关键信息:
-
m_ids
:当前所有活跃事务(Active Transactions) 的事务 ID 集合。-
活跃事务:指在创建 ReadView 的这一刻,已经开始(
BEGIN
)但尚未提交(COMMIT
)或回滚(ROLLBACK
)的事务。 -
重要补充:该集合不包含创建此 ReadView 的事务自身(即使它也是活跃的)。
-
-
m_up_limit_id
(低水位线 - Low Watermark):活跃事务集合m_ids
中最小的事务 ID。-
其意义在于:标识了在 ReadView 创建时,最早开始且尚未结束的事务。
-
可见性含义:任何事务 ID 小于
m_up_limit_id
的事务,在 ReadView 创建时必定已经提交完成(因为它们比最小的活跃事务 ID 还小,且不在活跃列表中)。因此,这些事务所做的修改对当前 ReadView 是可见的。
-
-
m_low_limit_id
(高水位线 - High Watermark):下一个将被分配的事务 ID(通常等于当前系统已分配的最大事务 ID + 1)。-
其意义在于:标识了 ReadView 创建后,将要开始的(尚未创建的)事务的起始 ID。
-
可见性含义:任何事务 ID 大于等于
m_low_limit_id
的事务,都是在 ReadView 创建之后才开始的。因此,这些事务所做的任何修改,对于创建此 ReadView 的事务来说绝对不可见。
-
-
m_creator_trx_id
:创建此 ReadView 的事务自身的事务 ID。用于识别“自己修改的数据”。
版本选择流程:四步可见性判断规则
确定了 ReadView 后,查询过程如下:
-
定位起点:找到目标数据行的最新版本(即版本链的链头)。
-
遍历与判断:从链头开始,依次遍历版本链中的每个历史版本。对每个遍历到的版本,应用以下规则判断其是否对当前事务可见:
-
规则 1 (自修改可见):
-
条件:该版本的事务 ID 等于
m_creator_trx_id
。 -
结果:说明这是当前事务自己修改的版本。可见,直接访问此版本。
-
结束:找到可见版本,停止遍历。
-
-
规则 2 (早于快照提交可见):
-
条件:该版本的事务 ID 小于
m_up_limit_id
(低水位线)。 -
结果:说明该版本对应的事务在 ReadView 创建之前就已经提交。可见,直接访问此版本。
-
结束:找到可见版本,停止遍历。
-
-
规则 3 (晚于快照开始不可见):
-
条件:该版本的事务 ID 大于等于
m_low_limit_id
(高水位线)。 -
结果:说明该版本对应的事务是在 ReadView 创建之后才开始的。绝对不可见。
-
动作:跳过此版本,继续遍历检查更旧的下一个版本(链尾方向)。
-
-
规则 4 (快照时刻活跃性检查):
-
条件:该版本的事务 ID 落在区间
[m_up_limit_id, m_low_limit_id)
内(即大于等于低水位线但小于高水位线)。 -
检查:判断该事务 ID 是否在活跃事务列表
m_ids
中。-
若不在
m_ids
中:说明该事务虽然在 ReadView 创建时已开始(ID在区间内),但在创建 ReadView 的那一刻它已经提交了。可见,直接访问此版本。 -
若在
m_ids
中:说明该事务在 ReadView 创建时仍处于活跃状态(未提交)。不可见。
-
-
动作:
-
若可见,则访问此版本并停止遍历。
-
若不可见,则跳过此版本,继续遍历检查更旧的下一个版本(链尾方向)。
-
-
-
-
终止条件:按照上述规则遍历版本链,一旦找到第一个满足“可见”条件的版本,即停止遍历并使用该版本。如果遍历完所有版本都未找到可见版本(例如该行数据的所有版本都是由未提交的活跃事务修改的),则意味着该行数据对当前事务不可见(可能返回空或根据隔离级别处理)。
一个小例子
构造好 ReadView 之后需要根据一定的查询规则找到唯一的可用版本,这个查找规则比较简单,以下图的版本链为例,在 m_creator_trx_id=201 的事务执行 select 时,会构造一个 ReadView 同时对相应的变量赋值
-
m_ids:活跃事务集合为 [90, 100, 200]
-
m_up_limit_id:活跃事务最小事务Id = 90
-
m_low_limit_id:预分配事务ID = 202,最大事务Id = 预分配事务ID-1 = 201
-
m_creator_trx_id:当前创建 ReadView 的事务Id = 201
接下来找到版本链头,从链头开始遍历所有版本,根据四步查找规则,判断每个版本:
-
第一步:判断该版本是否为当前事务创建,若 m_creator_trx_id 等于该版本事务id,意味着读取自己修改的数据,可以直接访问,如果不等则到第二步
-
第二步:若该版本事务id < m_up_limit_id (最小事务id),意味着该版本在ReadView生成之前已经提交,可以直接访问,如果不是则到第三步
-
第三步:或该版本事务id >= m_low_limit_id (最大事务id),意味着该版本在ReadView生成之后才创建,所以肯定不能被当前事务访问,所以无需第四步判断,直接遍历下一个版本,如果不是则到第四步
-
第四步:若该版本事务id在 m_up_limit_id (最小事务id)和 m_low_limit_id (最大事务id)之间,同时该版本不在活跃事务列表中,意味着创建ReadView时该版本已经提交,可以直接访问,如果不是则遍历并判断下一个版本
这样从版本链头遍历判断到版本链尾,找到⾸个符合要求的版本即可,就可以实现查询到的结果都 是已经提交事务的数据,解决了脏读问题。
4.2 MVCC是否可以解决不可重复读与幻读
MVCC (多版本并发控制) 机制在解决数据库隔离性问题中扮演着核心角色,但其对不可重复读 (Non-repeatable Read) 和幻读 (Phantom Read) 的解决能力有所不同。
-
MVCC 有效解决不可重复读 (在 REPEATABLE READ 级别):
-
在 REPEATABLE READ (可重复读) 隔离级别下,数据库利用 MVCC 提供了一个关键保证:事务在其整个生命周期内看到的数据快照是一致的。
-
实现方式是:当事务执行其第一个查询时,数据库会为该事务创建一个 ReadView。这个 ReadView 决定了该事务能看到哪些已提交的数据版本。
-
关键点在于:后续在该事务中执行的所有查询,都会复用这同一个初始 ReadView 来进行可见性判断。
-
效果: 因为事务始终基于同一个“数据快照”(由初始 ReadView 定义)进行读取,即使其他并发事务在此期间修改并提交了数据,当前事务也不会看到这些新提交的变更。因此,在同一个事务内重复执行相同的查询,保证会得到相同的结果集,从而彻底解决了不可重复读问题。
-
-
MVCC 无法单独解决幻读 (即使在 REPEATABLE READ 级别):
-
虽然 REPEATABLE READ 通过复用 ReadView 解决了不可重复读(即已存在行的数据变更),但它无法完全阻止幻读的发生。
-
原因: MVCC 的 ReadView 机制主要控制的是已存在记录行的版本可见性。它无法锁定一个范围或防止新记录的插入。
-
假设事务A基于其初始 ReadView 执行了一个范围查询(如
SELECT * FROM t WHERE id > 100
)。在此期间,如果另一个事务B插入了一条新的、满足id > 100
条件的记录并提交,那么当事务A再次执行完全相同的范围查询时:-
MVCC 会确保事务A看不到其他事务对原有记录的修改(不可重复读被阻止)。
-
但是,MVCC 无法阻止事务A看到这个新插入的、在初始 ReadView 创建时还不存在的记录。因为这个新记录没有“旧版本”需要被 ReadView 过滤掉。
-
-
结果: 事务A的两次相同范围查询返回了不同的行数(第二次多出了新插入的行),即发生了幻读。
-
结论: 仅靠 MVCC 的快照隔离机制,无法完全防止幻读。 在标准的 SQL REPEATABLE READ 隔离级别定义下,数据库通常需要结合间隙锁 (Gap Locks) 或Next-Key Locks 等锁机制来阻塞其他事务在范围内的插入操作,才能彻底解决幻读问题。
-
-
对比:READ COMMITTED 隔离级别下的 MVCC 行为:
-
在 READ COMMITTED (读已提交) 隔离级别下,MVCC 的行为有所不同。
-
此时,每次执行查询语句(甚至可能是一个查询内的不同行扫描)都可能创建一个新的 ReadView。也就是说,在读已提交的隔离级别中,整个事务可能会创建多个Readview.
-
效果: 每次查询都能看到最新已提交的数据版本。这解决了脏读问题,但导致了不可重复读:在同一事务内,两次相同的查询可能返回不同的结果,因为中间有其他事务提交了修改。同时,幻读也可能发生,因为新的 ReadView 能看到其他事务新提交的插入。
-
总结:
-
MVCC 的核心机制是通过 ReadView 控制事务能看到的数据版本。
-
REPEATABLE READ 级别通过事务开始时(首次查询)创建并固定一个 ReadView,完美解决了不可重复读。
-
仅靠 MVCC 的快照(固定 ReadView)无法阻止幻读,因为它无法过滤事务开始后新插入的数据。解决幻读通常需要 REPEATABLE READ 级别配合额外的锁机制(如间隙锁)。
-
READ COMMITTED 级别通过每次查询创建新 ReadView,解决了脏读,但允许不可重复读和幻读发生。
以上就是关于MVCC的相关介绍,加上锁就可以实现完整的ACID中的隔离性。
三. 查看并设置隔离级别
事务的隔离级别分为全局作用域和会话作用域,查看不同作用域事务的隔离级别,可以使用以下的方式:
--全局作用域
SELECT @@GLOBAL.transaction_isolation;
--会话作用域
SELECT @@SESSION.transaction_isolation;
可以看到默认的事务隔离级别是REPEATABLE-READ(可重复读)
设置事务的隔离级别和访问模式,可以使用以下语法:
通过GLOBAL|SESSION分别指定不同作用域的事务隔离级别
-- 通过作用域限定符设置事务隔离级别
SET [GLOBAL | SESSION] TRANSACTIONISOLATION LEVEL <level> -- 设置隔离级别| <access_mode>; -- 设置访问模式
隔离级别选项(level)
用于控制事务间的数据可见性与并发行为,包含以下四种级别:
-
REPEATABLE READ(可重复读)
确保同一事务中多次读取相同数据时结果一致,防止不可重复读现象 -
READ COMMITTED(读已提交)
仅允许读取其他事务已提交的数据,避免脏读问题 -
READ UNCOMMITTED(读未提交)
允许读取其他事务未提交的数据,存在脏读风险 -
SERIALIZABLE(串行化)
最高隔离级别,强制事务串行执行,彻底避免脏读、不可重复读和幻读
访问模式选项(access_mode)
用于定义事务的数据操作权限,包含两种模式:
-
READ WRITE(读写模式)
允许事务执行数据读写操作(包括插入、更新、删除) -
READ ONLY(只读模式)
限制事务仅能进行数据查询,禁止任何修改操作
示例
-- 设置全局事务隔离级别为串行化
-- 在下一个会话中生效,不影响当前会话
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;-- 设置会话事务隔离级别为串行化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;-- 如果不指定任何作用域,设置将在下一个事务开始生效,只针对下一个事务,不影响会话和全局的隔离级别
SET TRANSACTION ISOLATION LEVEL SERTALIZABLE;
通过选项文件指定事务的隔离级别,以便MySQL启动的时候读取并设置
[mysqld]
transaction-isolation = REPEATABLE-READ -- 隔离级别为可重复读
transaction-read-only = OFF -- 关闭只读意味着访问模式为读写
TIPS:
官网MySQL8.0更新描述: The tx_isolation and tx_read_only system variables have been removed. Use transaction_isolation and transaction_read_only instead.
所以在MySQL5.7及以前的版本中使用 tx_isolation 和 tx_read_only 来设置事务的隔离级别和访问模式。
通过SET语法设置系统变量的方式设置事务的隔离级别
-- 方式一
SET GLOBAL transaction_isolation = 'SERTALIZABLE';
-- 注意使用SET语法时有空格要用"-"代替
SET SESSION transaction_isolation = 'REPEATABLE-READ';-- 方式二
SET @GLOBAL.transaction_isolation='SERTALIZABLE';
-- 注意使用SET语法时有空格要用"-"代替
SET @@SESSION.transaction_isolation='REPEATABLE-READ';
设置事务隔离级别的语句不能在已开启的事务中执行,否则将会报错:
开启事务后,再去修改事务的隔离级别,将会报错
-
接下来介绍不同事务隔离级别的实现方式,以及可能出现的问题
四. READ UNCOMMITTED - 读未提交与脏读
实现方式
-
读取时:不加任何锁,直接读取版本链中的最新版本,也就是当前读,可能会出现脏读,不可重复读、幻读问题;
-
更新时:加共享行锁(S锁),事务结束时释放,在数据修改完成之前,其他事务不能修改当前数据,但可以被其他事务读取。
存在问题
事务的 READ UNCOMMITTED 隔离级别不使用独占锁,所以并发性能很高,但是会出现大量的数据安全问题,比如在事务A中执行了一条 INSERT 语句,在没有执行 COMMIT 的情况下,会在事务B中被读取到,此时如果事务A执行回滚操作,那么事务B中读取到事务A写入的数据将没有意义,我们把这个现象叫做 "脏读" 。
问题重现
-
在一个客户端A中先设置全局事务隔离级别为 READ UNCOMMITTED 读未提交:
设置隔离级别为READ UNCOMMITTED读未提交
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
查看设置是否生效
SELECT @@GLOBAL_transaction_isolation;
-
打开另一个客户端并确认隔离级别
查看设置是否生效
SELECT @@GLOBAL_transaction_isolation;
在不同的客⼾端中执⾏事务
会话A开启事务A | 会话B开启事务B |
# 选择数据库 # 写入一条新数据 # 查询结果 #新记录已写⼊,但是此时事务 A 并没有 提交 | |
# 选择数据库 # 查询结果 # 发现查到了事务A没有提交的数据 | |
# 回滚 # 查询结果,数据正常回滚 select * from account; | id | name | balance | |--- |--- |---- | | 1 | 张三 | 800.00 | | 2 | 李四 | 1100.00 | 2 rows in set (0.00 sec) | |
# 查询结果,刚才“王五”这条记录不见了 select * from account; | id | name | balance | |--- |-- - |---- | | 1 | 张三 | 800.00 | | 2 | 李四 | 1100.00 | 2 rows in set (0.00 sec) |
由于 READ UNCOMMITTED 读未提交会出现“脏读”现象,在正常的业务中出现这种问题会产生非常危重后果,所以正常情况下应该避免使用 READ UNCOMMITTED 读未提交这样的隔离级别。
五. READ COMMITTED - 读已提交与不可重复读
实现方式
- 读取时:不加锁,但使用快照读,即按照 MVCC 机制读取符合 ReadView 要求的最大数据,每次查询都会构造一个新的 ReadView,可以解决脏读,但无法解决不可重复读和幻读问题;
- 更新时:加独占行锁(X),事务结束时释放,数据在修改完毕之前,其他事务不能修改也不能读取这行数据。
存在问题
为了解决脏读问题,可以把事务的隔离级别设置为 READ COMMITTED,这时事务只能读到了其他事务提交之后的数据,但会出现不可重复读的问题。
读已提交的隔离级别不开启间隙锁,所以也没有使用next-key锁,只使用了索引记录锁。
比如事务A先对某条数据进行了查询,之后事务B对这条数据进行了修改,并且提交(COMMIT)事务,事务A再对这条数据进行查询时,得到了事务B修改之后的结果,这导致了事务A在同一个事务中以相同的条件查询得到不同的值,这个现象要“不可重复读”。
问题重现
在一个客户端A中先设置全局事务隔离级别为 READ COMMITTED 读未提交:
设置隔离级别为READ COMMITTED读未提交
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
查看设置是否生效
SELECT @GLOBAL.transaction_isolation;
- 打开另一个客户端B并确认隔离级别
查看设置是否生效
SELECT @@GLOBAL.transaction_isolation;
会话A开启事务A | 会话B开启事务B |
#选择数据库 #写入一条新测试数据 #开启事务 #查询王五的记录,余额是2000 | |
#选择数据库 #开启事务 #修改王五的余额为1000 #提交事务 | |
#此时事务并没有提交或回滚 #再次查询王五的记录发现余额变成了1000 #与上一个查询结果不一致 select * from account where name='王五'; +----+------+---------+ | id | name | balance | +----+------+---------+ | | 王五 | 1000.00 |#出现问题 +----+------+---------+ row in set (0.00 sec) |
六. REPEATABLE READ - 可重复读与幻读
实现方式
- 读取时: 不加锁,也使用快照读,按照MVCC机制读取符合ReadView要求的版本数据,但无论事务中有几次查询,只会在首次查询时生成一个ReadView,可以解决脏读、不可重复读、配合Next-Key行锁可以解决一部分幻读问题;
- 更新时: 加Next-Key行锁,事务结束时释放,在一个范围内的数据修改完成之前,其他事务不能对这个范围内的数据进行修改、插入和删除操作,同时也不能被查询。
存在问题
事务的REPEATABLE READ隔离级别是会出现幻读问题的,在InnoDB中使用了Next-Key行锁来解决大部分场景下的幻读问题,那么在不加Next-Key行锁的情况下会出现什么问题吗?
我们知道Next-Key锁,锁住的是当前索引记录以及索引记录前面的间隙,那么在不加Next-Key锁的情况下,也就是只对当前修改行加了独占行锁(X),这时记录前的间隙没有被锁定,其他的事务就可以向这个间隙中插入记录,就会导致一个问题:事务A查询了一个区间的记录得到结果集A,事务B向这个区间的间隙中写入了一条记录,事务A再查询这个区间的结果集时会查到事务B新写入的记录得到结果集B,两次查询的结果集不一致,这个现象就是“幻读”。
问题重现
由于REPEATABLE READ隔离级别默认使用了Next-Key锁,为了重现幻读问题,我们把隔离级回退到更新时只加了排他锁的READ COMMITTED。
设置隔离级别为READ COMMITTED读未提交
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
查看设置是否生效
SELECT @GGLOBAL.transaction_isolation;
会话A开启事务A | 会话B开启事务B |
#选择数据库 #更新王五的余额,使该记录加排他锁 #查询结果集,更新成功 | |
#选择数据库 #在李"四与"和"王五"之间的间隙写入一条数据"赵六" #查询结果集,写入成功 #提交事务 | |
#查询结果集, select * from account; commit; |
七. SERIALIZABLE - 串行化
实现方式
- 读取时: 加共享表锁,读取版本链中的最新版本,事务结束时释放;
- 更新时: 加独占表锁,事务结束时释放,完全串行操作,可以解决所有事务问题。
存在问题
- 所有的更新都是串行操作,效率极低。
八.不同隔离级别的性能与安全
指标 | READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE |
---|---|---|---|---|
并发性能 | 高 | 中高 | 中低 | 低 |
脏读 | 存在 | 解决 | 解决 | 解决 |
不可重复读 | 存在 | 存在 | 解决 | 解决 |
幻读 | 存在 | 存在 | 存在 | 解决 |
隔离力度(安全性) | 低 | 中 | 中高 | 高 |