数据库事务隔离级别:脏读、不可重复读与幻读的实战演示

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-17 10:42 ·5 浏览 ·0 回复

结论:三种并发异常里,脏读只存在于 READ UNCOMMITTED,不可重复读被 REPEATABLE READ 挡住,幻读在 MySQL 的 RR 下「快照读看不到、当前读照样中招」——所以想真正避免幻读,靠的不是调高隔离级别,而是搞懂 MVCC 快照读和间隙锁的边界。

先把三个概念一句话说清楚

脏读:A 事务读到了 B 事务「改了但还没提交」的数据,B 一回滚,A 读到的就是从未存在过的值。不可重复读:同一个事务里两次读同一行,中间被别的事务提交了 UPDATE,两次结果不一样。幻读:同一个事务里两次执行同样的范围查询,第二次多出了(或少了)行,像凭空冒出来一样。

三者的共同点是并发事务互相干扰,区别在于干扰的对象:脏读是「未提交数据」,不可重复读盯的是「同一行的值」,幻读盯的是「结果集的行数」。

四种隔离级别各自能挡住什么

结论:SQL 标准里四种级别是递进关系,MySQL 的默认是 REPEATABLE READ,Oracle 和 PostgreSQL 的默认是 READ COMMITTED。

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不可能可能可能
REPEATABLE READ不可能不可能标准允许,MySQL 快照读下不可能
SERIALIZABLE不可能不可能不可能

SERIALIZABLE 靠串行执行换来绝对安全,代价是并发度断崖式下降,生产环境基本只在极少数对账、扣减场景用局部锁代替。

实战演示:两个会话亲手复现

准备一张表 `account(id, balance)`,插入 `(1, 1000)`,然后开两个 MySQL 客户端窗口。

脏读(READ UNCOMMITTED):会话 A 执行 `SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; BEGIN; SELECT balance FROM account WHERE id=1;` 得到 1000。会话 B 执行 `BEGIN; UPDATE account SET balance=500 WHERE id=1;`(不提交)。会话 A 再查一次,读到 500——这就是脏读。让 B 执行 `ROLLBACK`,500 凭空消失。

不可重复读(READ COMMITTED):把级别换成 `SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;`,会话 A `BEGIN; SELECT balance` 得 1000;会话 B `BEGIN; UPDATE ...; COMMIT;`;会话 A 再查变成 500,同一个事务内两次读不一致。

幻读:级别切到 `REPEATABLE READ`,会话 A `BEGIN; SELECT * FROM orders WHERE user_id=1;` 得到 3 行;会话 B `BEGIN; INSERT INTO orders(user_id) VALUES(1); COMMIT;`;会话 A 再执行同样的 SELECT,还是 3 行——这是快照读,看不到新行。关键在后面:会话 A 执行 `UPDATE orders SET status=1 WHERE user_id=1;`,会提示影响 4 行,再 SELECT 就能看到第 4 行了。结论:RR 只保证快照读的可重复,当前读(UPDATE/DELETE/SELECT ... FOR UPDATE)依然会读到最新数据并产生幻读。

查当前级别用 `SELECT @@transaction_isolation;`(MySQL 8.0)或 `SELECT @@tx_isolation;`(5.7)。永久修改在 my.cnf 里写 `transaction-isolation = REPEATABLE-READ`,代码层则用 Spring 的 `@Transactional(isolation = Isolation.REPEATABLE_READ)` 或 JDBC 的 `Connection.setTransactionIsolation()`。

MySQL RR 为什么「看起来」挡住了幻读

结论:RR 下靠 MVCC 的 ReadView 保证快照读不幻读,靠 Next-Key Lock(记录锁 + 间隙锁)保证当前读不幻读,两套机制各管一半。

InnoDB 在 RR 下的当前读会对扫描到的索引区间加间隙锁,所以 `SELECT ... FOR UPDATE` 能锁住「不存在的行」,别的事务插不进来。RC 下间隙锁基本被关掉,只剩记录锁,当前读的范围就挡不住了。

实践注意点有三条:一是间隙锁会显著提高死锁概率,唯一键冲突插入、范围更新交叉是两个高发场景;二是 RC + `binlog_format=STATEMENT` 在 MySQL 上是不被允许的组合,必须用 ROW 格式;三是长事务会让 undo 版本链无限拉长,快照读的代价越来越高,能用短事务就别拖。

到底怎么选

结论:绝大多数业务用 MySQL 默认的 RR 就够,高并发写入密集的互联网业务可以降到 RC,代价是应用层要自己处理幻读。

降到 RC 的典型做法是把「先查再插」改成「唯一索引 + 捕获重复键异常」,或者对关键区间显式加 `SELECT ... FOR UPDATE`。注意 MySQL 5.7 及以上版本、InnoDB 引擎是这类讨论的前提,像 Clara BBS 这类系统要求 MySQL 5.7+,走的也是 InnoDB 默认 RR,日常发帖、记账不涉及跨行范围竞争,一般不需要动隔离级别。

一句话收束:隔离级别不是调得越高越安全,而是要清楚每种级别在「快照读」和「当前读」下分别给你什么保证,剩下的交给唯一索引和短事务来兜底。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-422.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~