你还记得MySQL事务的四大特性中的事务的隔离性吗?如果忘记的话,可以进入《数据库事务 (Transaction)》复习一遍。在多个事务同时运行的情况下,事务之间容易出现脏读、不可重复读和幻读等情况,为了确保数据操作的安全性,引出了事务隔离这个概念。同时,事务隔离是分级别的,绝大多数现代的关系型数据库(RDBMS)都有事务隔离级别的概念。今天我们主要通过具体实例让你彻底理解事务隔离级别。
由于事务之间缺乏足够的隔离性(Isolation),数据库在并发控制时容易发生以下异常现象:
异常现象 | 本质原因 | be like: |
---|---|---|
脏读 (Dirty Read) | 读到了其他事务未提交的数据。 | 老板看工资表: 会计正在修改你的工资(从5000改为8000),但还未最终提交(可能还会改回去)。此时老板刷新页面,看到了8000的这个临时数据。结果会计撤销了修改,你的工资还是5000。但老板却以为你涨薪了。老板读到了一个“脏”数据。 |
不可重复读 (Non-Repeatable Read) | 同一事务内,两次读取同一数据,结果不同(因其他事务提交了修改)。 | 两次点餐结算: 你下单时看了一眼购物车里的商品A价格是10元(第一次读)。 此时管理员将商品A价格改为15元并提交。 你下单前再次确认价格,发现变成了15元(第二次读)。 在同一笔订单中,你对同一商品两次读取得到了不同结果。 |
幻读 (Phantom Read) | 同一事务内,两次执行相同条件的查询,返回的数据行数不同(因其他事务提交了新增或删除)。 | 统计会议人数: 你第一次统计“今天参会人数有10人”。 此时另一事务登记了一位新参会者并提交。 你再次统计,莫名其妙地变成了11人,仿佛产生了幻觉。重点在于“有无”的变化。 |
为了解决以上这些问题,标准 SQL 定义了 四类事务隔离级别,用来指定事务中的哪些数据改变是可见的,哪些数据改变是不可见的,下面通过表格简要理解这四个隔离级别(升序排列):
隔离级别 | 脏读 | 不可重复读 | 幻读 | 核心特点简述 |
---|---|---|---|---|
READ UNCOMMITTED (读未提交) | ❌ | ❌ | ❌ | 无隔离。 性能最高,但可能读到未提交的“脏”数据,一致性最差。 |
READ COMMITTED (读提交) | ✅ | ❌ | ❌ | 防脏读。 只能读到已提交的数据,是性能与一致性的实用平衡点,多为默认级别。 |
REPEATABLE READ (可重复读) | ✅ | ✅ | ❌ | 保一致性。 同一事务中多次读取结果相同,避免了脏读和不可重复读。 |
SERIALIZABLE (串行化) | ✅ | ✅ | ✅ | 完全隔离。 强制事务串行执行,杜绝所有并发问题,但性能开销最大。 |
现在我们通过具体实例由低到高地介绍各个隔离级别:
一、 READ UNCOMMITTED (读未提交)
“读未提交(READ UNCOMMITTED)”是唯一一个允许脏读发生的事务隔离级别。 它非但不能解决脏读现象,反而是脏读发生的根源。
首先先准备一张表格,并在里面存入1、2、3、4、5;这5个数:
准备两个CMD窗口,分别命名为A窗口和B窗口:
打开并在A窗口修改事务隔离级别:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
通过:
SHOW VARIABLES LIKE '%transaction%iso%'\G
查看是否真的设置了:
CMD打开B窗口,此时该窗口已经设置读未提交(READ UNCOMMITTED)。
在A窗口里,开启一个事务并通过UPDATE语句更新tableNum里面第三行的数据,改为0;
BEGIN; UPDATE tableNum SET num=0 WHERE num=3;
此时来到B窗口进行tableNum的查询:
此时发现在B窗口下,第三个数据已将被修改成0了。
回到A窗口,撤回该事务:
ROLLBACK;
返回B窗口查表tableNum:
第三个数据又变了!
A窗口未提交事务,进行数据修改,通过B窗口进行表查询,结果表tableNum根据A窗口操作而变化,导致不准确的现象,引发B窗口的脏读现象。
二、 读提交(READ COMMITTED)
一个事务在执行过程中,只能读取到其他事务已经提交的数据修改。它有效防止了脏读(Dirty Read),即不会看到任何未提交的中间数据。然而,在同一事务内重复执行相同查询,可能会因其他事务的提交而得到不同结果,这种现象称为不可重复读(Non-repeatable Read)。该级别保证了数据的已提交性,是事务隔离的基本要求。Oracle、SQL Server 等数据库系统均以此作为默认隔离级别。
该例子解释的是读提交(READ COMMITTED)解决脏读的问题(还是准备两个CMD窗口A、B,切勿全部一下子全打开)
老规矩,A窗口里先修改事务隔离级别是READ COMMITTED (读提交):
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
检查一下:
SHOW VARIABLES LIKE '%transaction%iso%'\G
看一下表里的数据,发现还是原来那个表哈(1、2、3、4、5),因为之前ROLLBACK了;
开启事务并通过UPDATE语句更新tableNum里面第三行的数据,改为0;(老操作了)
BEGIN; UPDATE tableNum SET num=0 WHERE num=3;
打开B窗口查询tableNum:
A窗口提交事务:
COMMIT;
B窗口查看:
只有A窗口进行事务提交才会修改数据,B窗口才可以读取修改后的数据,否则还是未提交前的状态。这里我们细心地发现,B窗口出现了两种不同的查询结果(提交前是3,提交后是0),这样会破坏了事务的原子性逻辑和确定性。
此时我们可以通过可重复读隔离级别来解决实例中产生的不可重复读问题。
三、 可重复读(REPEATABLE READ)
就像虚拟机上的快照一样, 可重复读(REPEATABLE READ)能够保持最初的状态,从而有效的避免不可重复读。
还是这个tableNum表格(依旧是1、2、3、4、5):
老规矩,A窗口全局设置可重复读(REPEATABLE READ):
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
A窗口内,开启事务并进行修改,还是把第三个int改成0;
BEGIN; UPDATE tableNum SET num=0 WHERE num=3;
B窗口进入mysql且开始事务并查询第三个数据:
select * from tableNum where num=3;
得到:
发现此时数据并没有修改,还是3,难到是因为还没有提交事务吗?
聪明的你决定回到A窗口提交事务看看:
COMMIT;
再去B窗口看看,这下应该变了吧:
查无此0???
你又看了一遍总结,发现:可重复读(REPEATABLE READ)能够保持最初的状态,从而有效的避免不可重复读。放心了,记下了!
四、串行化(SERIALIZABLE)
串行化(SERIALIZABLE) 是最高的事务隔离级别,其核心目标是彻底解决幻读(Phantom Read) 问题。
所谓幻读,是指一个事务在后续查询中发现了之前查询时不存在的新记录(这些记录是其他已提交事务新插入的)。串行化机制通过强制所有事务串行执行(而非并发),并在读取的数据范围上加锁,使得事务仿佛在独享整个数据库,从而完全杜绝了幻读。然而,这种极致的隔离性是以极其低下的并发性能和最高的性能开销为代价的,因此除非有严格的数据一致性要求,否则通常不建议使用。
C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:
一点编程也不会写的:零基础C语言学练课程
解决困扰你多年的C语言疑难杂症特性的C语言进阶课程
从零到写出一个爬虫的Python编程课程
只会语法写不出代码?手把手带你写100个编程真题的编程百练课程
信息学奥赛或C++选手的 必学C++课程
蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程
手把手讲解近五年真题的蓝桥杯辅导课程