游乐游手机版
首页/AI热点日报/热点详情

事务隔离级别选错导致数据被吞 脏读幻读一次讲透

类型:热点整理2026-07-14
你在开发过程中,有没有遇到过这样的情况:一个事务里查了两次同样的数据,结果居然不一样;或者查了两次同样的范围条件,结果集凭空多了一行?很多人第一反应是“是不是缓存出问题了”,但其实,更大的概率是事务隔离级别没选对。 事务隔离级别,简单来说就是决定多个事务同时运行时,彼此能看到多少对方的数据。选得太低

你在开发过程中,有没有遇到过这样的情况:一个事务里查了两次同样的数据,结果居然不一样;或者查了两次同样的范围条件,结果集凭空多了一行?很多人第一反应是“是不是缓存出问题了”,但其实,更大的概率是事务隔离级别没选对。

事务隔离级别,简单来说就是决定多个事务同时运行时,彼此能看到多少对方的数据。选得太低,数据可能被“污染”;选得太高,并发性能又会急剧下降。今天这篇文章,我们就从四个隔离级别出发,把脏读、不可重复读、幻读这三个概念,掰开揉碎讲清楚。

先搞懂四个隔离级别

SQL标准定义了四种隔离级别,从低到高依次是:

1. READ UNCOMMITTED(读未提交) ——级别最低。事务可以读取其他事务尚未提交的数据。几乎不提供任何并发控制,实际生产环境中极少见到它的身影。

2. READ COMMITTED(读已提交,简称RC) ——只能读取其他事务已经提交的数据。它成功避免了脏读,但无法避免不可重复读。

3. REPEATABLE READ(可重复读,简称RR) ——保证同一事务内多次读取同一数据,结果完全一致。这也是MySQL InnoDB的默认隔离级别。

4. SERIALIZABLE(可串行化) ——最高级别,事务完全串行执行,从根源上杜绝所有并发问题,但性能开销也是最大的。

不同隔离级别能解决的问题,可以浓缩成下面这张表:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 避免 可能 可能
REPEATABLE READ 避免 避免 InnoDB基本避免
SERIALIZABLE 避免 避免 避免

三种并发问题,用真实SQL讲清楚

脏读:读到别人“还没想好”的数据

脏读,指的是一个事务读取了另一个事务尚未提交的修改。如果那个事务最终回滚了,你读到的就是“脏数据”——从未真正存在过的数据。

用电商库存场景来理解:事务A开始扣减库存,把商品X的库存从100改为99,但还没提交;此时事务B查询库存,读到了99。如果事务A因支付失败回滚了,库存恢复为100,但事务B已经基于99做了后续操作——超卖了。

-- 事务A(未提交)
START TRANSACTION;
UPDATE products SET stock = stock - 1 WHERE id = 1;  -- stock从100变99
-- 此时事务B读取到stock=99(未提交的数据)
-- 事务A回滚
ROLLBACK;  -- stock恢复为100,但事务B已经用了99的数据


想要解决脏读,至少要把隔离级别设置为READ COMMITTED。

不可重复读:同一事务内,数据“变了”

不可重复读指的是在同一个事务内,多次读取同一数据时结果不一致。它与脏读的关键区别在于:脏读读的是未提交的数据,而不可重复读读的是已提交的数据。

用财务场景来理解:财务人员在事务A中查询员工张三的工资,第一次看到5000元;此时HR在事务B中给张三加薪至6000元并提交;事务A再次查询时,工资变成了6000元。同一个事务里两次查询结果不一样——这就是不可重复读。

-- 事务A(隔离级别READ COMMITTED)
START TRANSACTION;
SELECT salary FROM employees WHERE id = 1;  -- 返回5000

-- 事务B(同时执行)
START TRANSACTION;
UPDATE employees SET salary = 6000 WHERE id = 1;
COMMIT;  -- 提交修改

-- 事务A再次查询
SELECT salary FROM employees WHERE id = 1;  -- 返回6000,和第一次不一样
COMMIT;


READ COMMITTED级别无法避免这个问题。你需要升级到REPEATABLE READ。

幻读:结果集“凭空多了一行”

幻读指的是在同一个事务内,多次查询某个范围的数据时,结果集的数量发生了变化——第一次查有5行,第二次查变成了6行。

打个比方:你在图书馆按分类找书,第一次看到书架上有5本书;等你转了一圈回来再数,变成了6本——多出来那本就是你不在的时候别人放上去的。

-- 事务A(隔离级别REPEATABLE READ)
START TRANSACTION;
SELECT * FROM orders WHERE status = 'pending';  -- 返回5行

-- 事务B(同时执行)
START TRANSACTION;
INSERT INTO orders (id, status) VALUES (100, 'pending');
COMMIT;

-- 事务A再次查询
SELECT * FROM orders WHERE status = 'pending';  -- 返回6行,多了一行!
COMMIT;


MySQL的RR为什么能“基本避免”幻读?

按照SQL标准,REPEATABLE READ级别实际上是允许幻读的。但MySQL InnoDB通过MVCC(多版本并发控制)+ Gap Lock(间隙锁) 的组合拳,在绝大多数场景下实实在在地避免了幻读的发生。

  • MVCC​:事务开始时拍一次快照,后续的普通SELECT都基于这个快照来读,别人提交了新数据你也看不到。
  • Gap Lock(间隙锁):锁定索引记录之间的空隙,阻止其他事务在范围内插入新行。

但有一点需要特别留意:​DML语句(UPDATE/DELETE)不遵循快照读​,它们可能会看到其他事务刚提交的行。所以在某些边缘场景下,RR级别仍然有可能出现幻读。

MySQL vs PostgreSQL:默认隔离级别为什么不同?

这是一个容易被忽略,但非常重要的差异。

  • MySQL InnoDB默认使用REPEATABLE READ(RR)
  • PostgreSQL默认使用READ COMMITTED(RC)

为什么?答案在于两者MVCC的实现机制不同:

对比维度 MySQL InnoDB(RR) PostgreSQL(RC)
快照时机 事务开始时拍一次快照 每条SELECT重新生成快照
不可重复读 避免 RC级别会出现
幻读 基本避免(MVCC+间隙锁) RC级别会出现
适用场景 高并发CRUD 复杂查询、数据分析

MySQL选择RR作为默认级别,是为了在并发性能和数据一致性之间取得一个较好的平衡,适合绝大多数互联网业务场景。PostgreSQL选择RC作为默认,则是因为它在RR级别下对幻读的处理更加严格,反而可能导致更多的死锁问题。

业务场景中怎么选隔离级别?

业务场景 推荐级别 理由
金融账务、库存扣减 SERIALIZABLE 或 RR 数据一致性要求极高,不容有失
电商订单、用户中心 RR(MySQL默认) 平衡性能与一致性,典型适用场景
报表查询、数据分析 RC 以查询为主,可接受不可重复读
高并发日志写入 RC 或 READ UNCOMMITTED 写入为主,对一致性要求低

一条核心的选型原则是:不是隔离级别越高就越好。隔离级别越高,并发性能越低。你需要根据业务对一致性和性能的实际要求来做权衡。

实战避坑:两个常见的隔离级别陷阱

陷阱1:在RC级别下做“先查后改”

很多业务逻辑都是“先查询状态,再根据状态做更新”。在RC级别下,两次查询之间数据可能被其他事务修改,导致“查的时候是A,改的时候已经变成B了”——数据就这样被“吞”掉了。解决方案:要么使用RR级别,要么在查询时加上FOR UPDATE把数据锁住。

陷阱2:RR级别下的间隙锁导致死锁

RR级别通过间隙锁成功避免了幻读,但间隙锁本身也是死锁的常见来源。在RR级别下做范围删除或范围更新,很可能锁住大量不存在的“间隙”,导致多个事务互相等待,最终形成死锁。如果你的业务场景不需要防止幻读,不妨考虑降级到RC级别,牺牲一点一致性来换取更高的并发性能。

总结

事务隔离级别是数据库并发控制的核心机制,直接影响数据的正确性和系统的吞吐量。透彻理解脏读、不可重复读、幻读的本质差异,搞清楚MySQL为什么选择RR、PostgreSQL为什么选择RC作为默认值,你就能在实际业务场景中做出更合理的选择,写出更可靠、更高效的事务代码。

来源:https://developer.aliyun.com/article/1743306

相关热点

继续查看同栏目近期热点。

延伸阅读

补充最近整理过的热点入口。