MySQL - 数据库的事务隔离级别
总结
四大隔离级别理解
值得一提的是:
Sql Server , Oracle(大多数db都是)的隔离级别是Read committed。
Mysql的默认隔离级别是Repeatable read。
四大隔离级别形象讲述
其中“读提交”和“可重复读”比较难理解,所以我用一个例子说明这几种隔离级别。假设数据表 T 中只有一列,其中一行的值为 1,下面是按照时间顺序执行两个事务的行为。
我们来看看在不同的隔离级别下,事务 A 会有哪些不同的返回结果,也就是图里面 V1、V2、V3 的返回值分别是什么。
- 若隔离级别是“读未提交”, 则 V1 的值就是 2。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 2。
- 若隔离级别是“读提交”,则 V1 是 1,V2 的值是 2。事务 B 的更新在提交后才能被 A 看到。所以, V3 的值也是 2。
- 若隔离级别是“可重复读”,则 V1、V2 是 1,V3 是 2。之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
- 若隔离级别是“串行化”,则在事务 B 执行“将 1 改成 2”的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
背景:为实现隔离性,现代数据库提供的三种锁
隔离性保证了每个事务各自读、写的数据互相独立,不会彼此影响。只从定义上就能嗅出隔离性肯定与并发密切相关,因为如果没有并发,所有事务全都是串行的,那就不需要任何隔离,或者说这样的访问具备了天然的隔离性。但现实情况不可能没有并发,要在并发下实现串行的数据访问该怎样做?几乎所有程序员都会回答:加锁同步呀!正确,现代数据库均提供了以下三种锁。
-
写锁(Write Lock,也叫作排他锁,eXclusive Lock,简写为 X-Lock):如果数据有加写锁,就只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加读锁。
-
读锁(Read Lock,也叫作共享锁,Shared Lock,简写为 S-Lock):多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍然可以读取。对于持有读锁的事务,如果该数据只有它自己一个事务加了读锁,允许直接将其升级为写锁,然后写入数据。
-
范围锁(Range Lock):对于某个范围直接加排他锁,在这个范围内的数据不能被写入。如下语句是典型的加范围锁的例子:
SELECT * FROM books WHERE price < 100 FOR UPDATE;
请注意“范围不能被写入”与“一批数据不能被写入”的差别,即不要把范围锁理解成一组排他锁的集合。加了范围锁后,不仅无法修改该范围内已有的数据,也不能在该范围内新增或删除任何数据,后者是一组排他锁的集合无法做到的。
一、隔离级别(从高到低)
1.1 可串行化 Serializable
定义
如果不考虑性能优化的话,
可串行化 对 事务所有读、写的数据,全都加上读锁、写锁和范围锁即可做到
可串行化
(“即可”是简化理解,实际还是很复杂的,要分成 Expanding 和 Shrinking 两阶段去处理读锁、写锁与数据间的关系,称为Two-Phase Lock,2PL)。
缺点:性能问题
但数据库不考虑性能肯定是不行的,并发控制理论(Concurrency Control)决定了隔离程度与并发能力是相互抵触的,隔离程度越高,并发访问时的吞吐量就越低。现代数据库一定会提供除可串行化
以外的其他隔离级别供用户使用,让用户调节隔离级别的选项,根本目的是让用户可以调节数据库的加锁方式,取得隔离性与吞吐量之间的平衡。
1.2 可重复读 Repeatable Read
定义
可串行化
的下一个隔离级别是可重复读
(Repeatable Read)—— (cuixunxu注 : 之所以叫可重复读,本人觉得是相对于下一个隔离级别不可重复读而言的,可重复读指“可以重复读某一条数据”,这一条数据不会在同一个事务中被修改;而在读已提交级别,是存在不可重复读的问题的。)
可重复读
对事务所涉及的数据加读锁和写锁,且一直持有至事务结束,但不再加范围锁。
缺点:幻读问题
可重复读
比可串行化
弱化的地方在于幻读问题(Phantom Reads),它是指在事务执行过程中,两个完全相同的范围查询得到了不同的结果集。譬如现在准备统计一下 Fenix's Bookstore 中售价小于 100 元的书有多少本,会执行以下第一条 SQL 语句:
SELECT count(1) FROM books WHERE price < 100 /* 时间顺序:1,事务: T1 */ INSERT INTO books(name,price) VALUES ('深入理解Java虚拟机',90) /* 时间顺序:2,事务: T2 */ SELECT count(1) FROM books WHERE price < 100 /* 时间顺序:3,事务: T1 */
根据前面对范围锁、读锁和写锁的定义可知,假如这条 SQL 语句在同一个事务中重复执行了两次,且这两次执行之间恰好有另外一个事务在数据库插入了一本小于 100 元的书籍,这是会被允许的,那这两次相同的查询就会得到不一样的结果,原因是可重复读
没有范围锁来禁止在该范围内插入新的数据,这是一个事务受到其他事务影响,隔离性被破坏的表现。
1.3 读已提交 Read Committed
定义
可重复读
的下一个隔离级别是读已提交
(Read Committed),
读已提交
对事务涉及的数据加的写锁会一直持续到事务结束,但加的读锁在查询操作完成后就马上会释放。
缺点:不可重复读问题
读已提交
比可重复读
弱化的地方在于不可重复读问题(Non-Repeatable Reads),它是指在事务执行过程中,对同一行数据的两次查询得到了不同的结果。譬如笔者想要获取 Fenix's Bookstore 中《深入理解 Java 虚拟机》这本书的售价,同样执行了两条 SQL 语句,在此两条语句执行之间,恰好另外一个事务修改了这本书的价格,将书的价格从 90 元调整到了 110 元,如下 SQL 所示:
SELECT * FROM books WHERE id = 1; /* 时间顺序:1,事务: T1 */ UPDATE books SET price = 110 WHERE id = 1; COMMIT; /* 时间顺序:2,事务: T2 */ SELECT * FROM books WHERE id = 1; COMMIT; /* 时间顺序:3,事务: T1 */
如果隔离级别是读已提交
,这两次重复执行的查询结果就会不一样,原因是读已提交
的隔离级别缺乏贯穿整个事务周期的读锁,无法禁止读取过的数据发生变化,此时事务 T2 中的更新语句可以马上提交成功,这也是一个事务受到其他事务影响,隔离性被破坏的表现。假如隔离级别是可重复读
的话,由于数据已被事务 T1 施加了读锁且读取后不会马上释放,所以事务 T2 无法获取到写锁,更新就会被阻塞,直至事务 T1 被提交或回滚后才能提交。
1.4 读未提交 Read Uncommitted
定义
读已提交
的下一个级别是读未提交
(Read Uncommitted),
读未提交
对事务涉及的数据只加写锁,会一直持续到事务结束,但完全不加读锁。
缺点:脏读问题
读未提交
比读已提交
弱化的地方在于脏读问题(Dirty Reads),它是指在事务执行过程中,一个事务读取到了另一个事务未提交的数据。譬如笔者觉得《深入理解 Java 虚拟机》从 90 元涨价到 110 元是损害消费者利益的行为,又执行了一条更新语句把价格改回了 90 元,在提交事务之前,同事说这并不是随便涨价,而是印刷成本上升导致的,按 90 元卖要亏本,于是笔者随即回滚了事务,场景如下 SQL 所示:
SELECT * FROM books WHERE id = 1; /* 时间顺序:1,事务: T1 */ /* 注意没有COMMIT */ UPDATE books SET price = 90 WHERE id = 1; /* 时间顺序:2,事务: T2 */ /* 这条SELECT模拟购书的操作的逻辑 */ SELECT * FROM books WHERE id = 1; /* 时间顺序:3,事务: T1 */ ROLLBACK; /* 时间顺序:4,事务: T2 */
不过,在之前修改价格后,事务 T1 已经按 90 元的价格卖出了几本。原因是读未提交
在数据上完全不加读锁,这反而令它能读到其他事务加了写锁的数据,即上述事务 T1 中两条查询语句得到的结果并不相同。如果你不能理解这句话中的“反而”二字,请再重读一次写锁的定义:写锁禁止其他事务施加读锁,而不是禁止事务读取数据,如果事务 T1 读取数据并不需要去加读锁的话,就会导致事务 T2 未提交的数据也马上就能被事务 T1 所读到。这同样是一个事务受到其他事务影响,隔离性被破坏的表现。假如隔离级别是读已提交
的话,由于事务 T2 持有数据的写锁,所以事务 T1 的第二次查询就无法获得读锁,而读已提交
级别是要求先加读锁后读数据的,因此 T1 中的查询就会被阻塞,直至事务 T2 被提交或者回滚后才能得到结果。
二、四个隔离级别的共性问题:“一个事务读+另一个事务写”的隔离问题
除了都以锁来实现外,以上四种隔离级别还有另一个共同特点,就是幻读、不可重复读、脏读等问题都是由于一个事务在读数据过程中,受另外一个写数据的事务影响而破坏了隔离性
三、“一个事务读+另一个事务写”隔离问题的解决方案:MVCC 多版本并发控制
针对这种“一个事务读+另一个事务写”的隔离问题,近年来有一种名为“多版本并发控制”(Multi-Version Concurrency Control,MVCC)的无锁优化方案被主流的商业数据库广泛采用。
MVCC 是一种读取优化策略,它的“无锁”是特指读取时不需要加锁。MVCC 的基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本共存,以此达到读取时可以完全不加锁的目的。在这句话中,“版本”是个关键词,你不妨将版本理解为数据库中每一行记录都存在两个看不见的字段:CREATE_VERSION 和 DELETE_VERSION,这两个字段记录的值都是事务 ID,事务 ID 是一个全局严格递增的数值,然后根据以下规则写入数据。
- 插入数据时:CREATE_VERSION 记录插入数据的事务 ID,DELETE_VERSION 为空。
- 删除数据时:DELETE_VERSION 记录删除数据的事务 ID,CREATE_VERSION 为空。
- 修改数据时:将修改数据视为“删除旧数据,插入新数据”的组合,即先将原有数据复制一份,原有数据的 DELETE_VERSION 记录修改数据的事务 ID,CREATE_VERSION 为空。复制出来的新数据的 CREATE_VERSION 记录修改数据的事务 ID,DELETE_VERSION 为空。
此时,如有另外一个事务要读取这些发生了变化的数据,将根据隔离级别来决定到底应该读取哪个版本的数据。
- 隔离级别是
可重复读
:总是读取 CREATE_VERSION 小于或等于当前事务 ID 的记录,在这个前提下,如果数据仍有多个版本,则取最新(事务 ID 最大)的。 - 隔离级别是
读已提交
:总是取最新的版本即可,即最近被 Commit 的那个版本的数据记录。
另外两个隔离级别都没有必要用到 MVCC,因为读未提交
直接修改原始数据即可,其他事务查看数据的时候立刻可以看到,根本无须版本字段。可串行化
本来的语义就是要阻塞其他事务的读取操作,而 MVCC 是做读取时无锁优化的,自然就不会放到一起用。
MVCC 是只针对“读+写”场景的优化,如果是两个事务同时修改数据,即“写+写”的情况,那就没有多少优化的空间了,此时加锁几乎是唯一可行的解决方案,稍微有点讨论余地的是加锁的策略是“乐观加锁”(Optimistic Locking)还是“悲观加锁”(Pessimistic Locking)。前面笔者介绍的加锁都属于悲观加锁策略,即认为如果不先做加锁再访问数据,就肯定会出现问题。相对地,乐观加锁策略认为事务之间数据存在竞争是偶然情况,没有竞争才是普遍情况,这样就不应该在一开始就加锁,而是应当在出现竞争时再找补救措施。这种思路被称为“乐观并发控制”(Optimistic Concurrency Control,OCC),囿于篇幅与主题的原因,就不再展开了,不过笔者提醒一句,没有必要迷信什么乐观锁要比悲观锁更快的说法,这纯粹看竞争的剧烈程度,如果竞争剧烈的话,乐观锁反而更慢。
参考
1.吴小凯 https://www.cnblogs.com/ubuntu1/p/8999403.html
2.维亚贝 https://blog.csdn.net/u011861874/article/details/81539306
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?