做后端开发和数据库运维的同学,应该都经历过这种场景:线上一条慢 SQL 把整张表锁住,或者某个事务里先查后改,结果两条相同条件的 SQL 查出来的数据不一样,最后对账对到怀疑人生。这些破事,十有八九都和事务隔离级别有关。
MySQL 的事务隔离级别,是 InnoDB 引擎里最核心也最容易讲糊的一个概念。说它核心,是因为它直接决定了并发场景下数据的可见性和一致性;说它容易讲糊,是因为网上教程要么只丢出四种隔离级别和三个问题的对照表,要么一上来就是 ReadView、版本链、间隙锁这种劝退术语。这篇内容,我打算用实际踩坑的经验把这些概念串起来,从并发问题到 MVCC 实现,再到生产环境怎么选,尽量讲得能直接上手用。
这篇内容适合谁?刚准备面试的 Java 后端,被线上死锁折磨的运维,以及写了好几年 SQL 但一被问到“RR 和 RC 到底差在哪”就卡壳的开发同学。看完之后,你至少能搞清楚三件事:四种隔离级别分别解决什么问题、MySQL 默认的可重复读为什么不会像教科书里说的那样产生幻读、以及生产环境里隔离级别到底应该怎么配。
1. 先搞清楚:事务隔离级别到底在解决什么问题
1.1 事务的 ACID 与隔离性的位置
要理解隔离级别,得先回到事务本身。事务是数据库执行逻辑的最小单元,ACID 四个特性里,A 是原子性、C 是一致性、I 是隔离性、D 是持久性。原子性靠 undo log 实现,持久性靠 redo log 实现,而隔离性,靠的就是我们今天要聊的锁和 MVCC。
很多人把隔离性理解成“事务之间完全互不干扰”,这个说法不准确。完全互不干扰是最高级别的隔离,但数据库为了性能和并发度,默认允许事务之间存在一定程度的“干扰”。隔离级别就是一套用来权衡“数据一致性”和“并发性能”的规则:级别定得越高,数据越安全,但并发能力越差;级别定得越低,吞吐量越大,但可能读到乱七八糟的中间状态。
MySQL 里 InnoDB 支持四种隔离级别,按严格程度从低到高依次是:
| 隔离级别 | 英文名称 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|---|
| 读未提交 | READ UNCOMMITTED | 可能 | 可能 | 可能 |
| 读已提交 | READ COMMITTED | 不会 | 可能 | 可能 |
| 可重复读 | REPEATABLE READ | 不会 | 不会 | 可能(InnoDB 实际解决了) |
| 串行化 | SERIALIZABLE | 不会 | 不会 | 不会 |
注意这张表里的“可能”和“不会”,它描述的是理论上的隔离标准。但 MySQL 的 InnoDB 引擎在可重复读级别下,通过 MVCC 和间隙锁把幻读也基本解决了,这和教科书里讲的“可重复读会存在幻读”有明显差异。这个点后面单独展开。
1.2 并发事务会闯什么祸:三个经典读异常
在动手调隔离级别之前,先把三个基础问题彻底吃透。这三个问题是理解隔离级别的钥匙。
脏读,指的是一个事务读到了另一个事务还没提交的数据。打个比方,A 事务给商品价格从 100 改成 80,改完之后还没提交,B 事务这时候去读价格,读到了 80。结果 A 事务后悔了,回滚,价格变回 100。B 事务手里的 80 就是个根本不存在过的数据。这种情况只能发生在读未提交级别下,只要把级别提到读已提交,脏读立刻消失。
不可重复读,指的是同一个事务内,同一条 SELECT 语句执行两次,结果不一样。原因是有别的事务在这两次查询之间提交了修改。比如 C 事务第一次查价格是 100,这时候 D 事务把 100 改成了 80 并提交,C 事务第二次查价格变成了 80。同一个事务里两次读对不上,这在账务类系统里是要出大问题的。读已提交级别允许这种情况发生,可重复读级别保证同一个事务里多次读的结果一致。
幻读,指的是同一个事务内,执行同一条范围查询,两次得到的结果集行数不一样。比如 E 事务查订单表里金额大于 100 的订单,第一次查出来 5 条,这时候 F 事务插入了一条金额 200 的订单并提交,E 事务再查就变成 6 条。多出来的那条就是“幻影”数据。注意幻读和不可重复读的区别:不可重复读是同一行数据的内容变了,幻读是结果集的行数变了,重点在“多出来”的记录上。
这三个问题里,脏读是底线,任何靠谱的系统都不能容忍;不可重复读和幻读则是很多业务场景的硬性要求。接下来看四种隔离级别各自的处理方式。
2. 四种隔离级别逐个拆解
2.1 READ UNCOMMITTED:裸奔的读
读未提交是最低级别的隔离,它的规则就一条:一个事务可以读到其他事务还没有提交的数据。这个级别基本不在生产环境出现,存在的意义更多是理论上的完整性。
实际会发生什么?A 事务 UPDATE 了一条记录但不提交,B 事务 SELECT 这条记录,直接就能看到修改后的值。如果 A 事务回滚,B 事务已经基于一个不存在的数据做了后续操作,整个逻辑链路就全错了。这玩意儿在金融、订单这种对数据准确性要求极高的业务里就是灾难。
但读未提交也并不是完全没有可用场景。有些对实时性要求极高、对准确性容忍度也高的场景,比如统计在线人数的缓存表、非关键性的计数展示,可以用它减少锁开销。印象里有段时间我做活动页的实时在线人数,用的就是读未提交,因为那个数据多一百少一百没人看得出来,但延迟必须毫秒级。绝大多数业务系统,不建议碰这个级别。
2.2 READ COMMITTED:行业最常见的默认选择
读已提交解决了脏读,但允许不可重复读和幻读。它的核心规则是:一个事务只能读到其他事务已经提交的数据。这条规则是通过“每次 SELECT 都重新生成一次 ReadView”实现的,这也是它和可重复读最根本的区别。
Oracle 和 PostgreSQL 的默认隔离级别就是读已提交。为什么这么多数据库把它当默认?因为它在数据一致性和并发性能之间找到了一个很好的平衡点,很多业务场景下,我们并不需要在一个事务内多次读取完全一致的结果。
比如电商系统里,一个查询商品列表的操作,第一次读到库存 10 件,另一个事务同时扣了 2 件库存并提交,第二次读到 8 件。这在大多数业务场景下是完全能接受的——库存本来就是实时变动的,你甚至会希望它每次读到的都是最新值。读已提交的价值就在这里:每个 SELECT 都拿到最新已提交数据,避免读到过期的旧数据。
我实际经验里,不少银行核心系统反而用读已提交,需要强一致性的地方用显式锁或串行化兜底。读已提交还有一个好处是锁范围小,InnoDB 在 RC 级别下只加记录锁,不加间隙锁,死锁概率和锁竞争的概率都会低不少。
2.3 REPEATABLE READ:MySQL 的“主场”隔离级别
可重复读是 MySQL 的默认隔离级别。它的核心规则:同一个事务内,第一次 SELECT 之后,后续所有 SELECT 都基于同一个快照,读到的数据完全一致,不受其他事务提交的影响。
这是咋做到的呢?靠 MVCC 多版本并发控制。InnoDB 在事务第一次执行快照读时,生成一个 ReadView,这个 ReadView 相当于给事务拍了一张“数据快照”,记录下当时哪些事务已提交、哪些正在活跃。后续所有的 SELECT 都基于这个 ReadView 判断数据的可见性,所以无论其他事务怎么改,这个事务看到的都是第一次查询时的样子。
这里有个关键点,很多新手会误解:可重复读只是保证“快照读”一致,不保证“当前读”一致。快照读就是普通的 SELECT,当前读是带锁的读操作,比如 SELECT ... FOR UPDATE、INSERT、UPDATE、DELETE。当前读永远读最新版本的数据,不受快照限制。这就是为什么可重复读级别下,一个事务里先快照读再 UPDATE,然后 SELECT 还是能看到自己改完的数据——因为 UPDATE 走的是当前读,改了以后数据的版本链变了,但快照读还是按第一次的 ReadView 来判断可见性,而自己修改过的数据版本,事务 ID 和自己一致,所以照样可见。这个机制看着绕,实际体验下来是很顺滑的。
MySQL 为什么把可重复读当默认?历史原因,从 5.0 之前就定下来了,因为早期 binlog 只有 statement 格式,为了保证主从数据一致,需要可重复读配合间隙锁来避免幻读导致的主从不一致。现在 binlog 已经有了 row 格式,理论上默认可以改成读已提交,但 MySQL 至今没改,原因很简单:兼容性。改默认值会影响所有存量系统的行为,对数据库这种底层软件来说,默认行为的稳定性远比理论上的更优重要。
2.4 SERIALIZABLE:用性能换绝对一致
串行化是最高隔离级别,读和写之间完全互斥。规则:所有事务按顺序执行,事务之间没有任何并发。实现上,InnoDB 在 SERIALIZABLE 级别下,所有普通 SELECT 也会自动加上共享锁,也就是变成当前读;写操作加排他锁。读读不阻塞,读写、写写全部阻塞。
效果是数据绝对安全,但并发能力极低。一个慢查询能堵住后面所有事务,这在互联网高并发场景下基本不可用。什么时候用?数据一致性要求极高、并发量极低的系统,比如记账核心的某些批处理环节、对账任务里的关键事务。我曾经在做一个数据迁移工具时,迁移最后一步的校验阶段把它临时切成串行化,保证迁移过程没有任何并发写入,校验结果完全可信,跑完再切回来。日常线上业务,不建议全局设置串行化。
3. MySQL 凭什么在 RR 下搞定幻读:MVCC 和锁
3.1 快照读与当前读,先分清这两个
学 MySQL 事务隔离级别,最容易被绕进去的就是没搞懂快照读和当前读。
快照读是最常见的普通 SELECT。它不加锁,直接通过 MVCC 读取版本链上的历史版本,理论上性能最高。在可重复读级别下,快照读的数据来自事务第一次 SELECT 时生成的 ReadView,所以多次读取结果一致。在读已提交级别下,每次 SELECT 都重新生成 ReadView,所以每次读到的都是最新已提交数据。
当前读是带锁的读,常见的包括 SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、INSERT、UPDATE、DELETE。这些操作必须读取数据的最新版本,并且会对读取的记录加锁,防止其他事务并发修改。当前读在可重复读级别下还得依赖间隙锁来解决幻读问题。
理解这两者的区别特别重要,因为很多线上问题都出在快照读和当前读混用上。最常见的一个坑:事务里先 SELECT 查了一条记录,然后用 UPDATE 去改这条记录,结果 UPDATE 发现记录被别的事务锁住了,直接锁等待超时。原因是这两个操作一个用的是快照读,一个用的是当前读,锁行为完全不同。
3.2 版本链和 ReadView:MVCC 的工作方式
MVCC 翻译成中文是多版本并发控制,核心思想是:同一行记录,同时存在多个历史版本,不同事务通过判断版本可见性,读取适合自己的数据。
每个数据行上,InnoDB 会隐藏两个关键字段:trx_id(最近修改该行的事务 ID)和 roll_pointer(指向上一个版本的 undo log 地址)。每一次 UPDATE,InnoDB 不会直接覆盖旧数据,而是把旧数据写到 undo log,通过 roll_pointer 串成一条版本链,最新版本在最上面。
ReadView 是事务执行快照读时生成的一个视图,里面保存了四个关键信息:创建这个视图的事务 ID、当前活跃事务 ID 列表、活跃事务列表里最小的 ID、以及下一个将要分配的事务 ID。判断一行数据是否对当前事务可见,规则是这样的:
- 如果数据的 trx_id 等于当前事务 ID,说明是自己改的,可见
- 如果 trx_id 小于活跃事务最小 ID,说明该事务已提交,可见
- 如果 trx_id 大于等于下一个分配的事务 ID,说明是快照之后才开启的事务,不可见
- 如果 trx_id 在活跃事务列表里,说明这个事务还没提交,不可见
- 如果 trx_id 不在活跃事务列表里但小于下一个分配 ID,说明已经提交了,可见
这个判断规则看着复杂,其实就是问一个问题:这个数据版本是不是在我这个事务开启之前就已经提交了的?是,就能看;不是,就不能看。沿着版本链往回找,直到找到一个可见的版本。
在可重复读级别下,ReadView 只在第一次快照读时生成一次,后续复用。在读已提交级别下,每次快照读都重新生成。这就是两种隔离级别在快照读表现上差异的本质原因,理解了这一层,很多面试题都能迎刃而解。
3.3 间隙锁和临键锁:当前读如何防幻读
快照读靠 MVCC 解决了可重复读下的普通查询问题,但当前读呢?比如一个事务里执行 SELECT ... FOR UPDATE 查某个范围的订单,另一个事务往这个范围里插入一条新订单,如果不加额外机制,这个事务第二次执行同样的当前读会多出一条数据,这就是幻读。
InnoDB 的解决方案是间隙锁和临键锁。间隙锁锁的是一个开区间范围,比如 id 在 (10, 20) 之间的“空隙”,防止其他事务在这个空隙里插入数据。临键锁是记录锁加间隙锁的合体,锁的是一个左开右闭区间,比如 (10, 20],既锁住 20 这条记录,又锁住 10 到 20 之间的空隙。在可重复读级别下,InnoDB 对当前读默认使用临键锁,范围查询时,锁范围覆盖到所有可能插入新数据的间隙。
实际效果是什么?一个事务执行 SELECT * FROM orders WHERE amount > 100 FOR UPDATE,另一个事务想 INSERT 一条 amount = 150 的订单,会被阻塞住,直到第一个事务提交。这样第一个事务就算反复执行同样的当前读,结果集也不会增加,幻读被物理上杜绝了。
但间隙锁有个明显的副作用:锁范围可能比实际需要大很多,导致并发插入被卡住。比如一个简单的等值查询定位不到记录,比如查 id = 1000 但表里没有这条记录,InnoDB 会把 (上一个记录, 1000) 的间隙也锁住。这在生产环境里是死锁和锁等待的重灾区,后面专门聊排查方法。
读已提交级别没有间隙锁,只用记录锁,这个问题会好很多,这也是为什么很多高并发系统会主动从可重复读切到读已提交的原因。
4. 实操:怎么查看和设置隔离级别
4.1 查看当前隔离级别
动手验证之前,先学会查当前会话和全局的隔离级别。MySQL 5.7 及以后版本,用这条 SQL:
SELECT @@session.transaction_isolation; SELECT @@global.transaction_isolation;MySQL 5.6 及更老的版本,变量名是 tx_isolation 而不是 transaction_isolation:
SELECT @@session.tx_isolation;如果你不确定你用的版本支持哪个,可以直接用 SHOW VARIABLES:
SHOW VARIABLES LIKE 'transaction_isolation'; SHOW VARIABLES LIKE 'tx_isolation';两条都执行一下,哪条不报错用哪条。执行结果会显示类似 REPEATABLE-READ 这样的值,注意中间是连字符不是空格。MySQL 8.0 和 5.7 在默认安装情况下,会话级和全局级都是 REPEATABLE-READ。
4.2 修改隔离级别
修改有两种维度:全局级别和会话级别。全局级影响之后新建立的连接,不影响当前已存在的连接;会话级只影响当前连接。
-- 全局级,对所有新连接生效 SET GLOBAL transaction_isolation = 'READ-COMMITTED'; -- 会话级,只对当前连接生效 SET SESSION transaction_isolation = 'READ-COMMITTED';MySQL 支持对下一个事务单独设置,也可以不加 SESSION 关键字直接设置:
-- 立即生效,只对下一个事务有效 SET TRANSACTION ISOLATION LEVEL READ COMMITTED;生产环境如果要修改线上数据库的隔离级别,两个思路:一是直接在数据库上执行 SET GLOBAL,但要注意这个操作不会自动持久化到配置文件,重启后失效;二是改 my.cnf 里的 transaction-isolation 参数,在 [mysqld] 段下面加上一行:
[mysqld] transaction-isolation = READ-COMMITTED动态改和配置改的区别类似于热更新和永久修改,建议线上操作先动态改验证效果,确认稳定后再写入配置文件,避免重启丢失配置。
4.3 用两个会话亲手验证四种隔离级别
纸上谈兵一百遍不如亲手跑一遍。下面是我自己验证隔离级别时用的方法,开两个 MySQL 客户端会话,分别叫会话 A 和会话 B,用一张简单的测试表配合执行。
先建一张表,插入测试数据:
CREATE TABLE `test_isolation` ( `id` int(11) NOT NULL AUTO_INCREMENT, `amount` decimal(10,2) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `test_isolation` (`amount`) VALUES (100.00), (200.00), (300.00);验证脏读,把会话 A 设置为读未提交,然后执行更新但不提交:
-- 会话 A SET SESSION transaction_isolation = 'READ-UNCOMMITTED'; START TRANSACTION; UPDATE test_isolation SET amount = 80.00 WHERE id = 1;然后到会话 B,同样设置读未提交,查询:
-- 会话 B SET SESSION transaction_isolation = 'READ-UNCOMMITTED'; START TRANSACTION; SELECT * FROM test_isolation WHERE id = 1;会看到 amount 显示为 80.00,但这条数据其实来自未提交事务。然后回会话 A 执行 ROLLBACK,再在会话 B 重新查询,数据又变回 100.00,脏读验证完毕。
验证不可重复读,把两个会话都切到读已提交。会话 A 开启事务查询一次,会话 B 更新并提交,然后会话 A 再查一次。读已提交级别下,会话 A 两次查到的数据不一样:
-- 会话 A SET SESSION transaction_isolation = 'READ-COMMITTED'; START TRANSACTION; SELECT * FROM test_isolation WHERE id = 1; -- 100.00 -- 会话 B SET SESSION transaction_isolation = 'READ-COMMITTED'; START TRANSACTION; UPDATE test_isolation SET amount = 90.00 WHERE id = 1; COMMIT; -- 会话 A 再次查询 SELECT * FROM test_isolation WHERE id = 1; -- 90.00同一个事务里两次读,结果从 100.00 变成了 90.00,这就是不可重复读。然后把两个会话都切到可重复读,重复同样的操作,会发现会话 A 第二次查询依然是 100.00,说明可重复读生效了。
验证幻读,可重复读级别下,用当前读配合间隙锁才是重点。会话 A 开启事务,执行带锁的范围查询,然后会话 B 尝试插入一条新记录,会看到插入操作被阻塞:
-- 会话 A SET SESSION transaction_isolation = 'REPEATABLE-READ'; START TRANSACTION; SELECT * FROM test_isolation WHERE id > 1 FOR UPDATE; -- 会话 B(会被阻塞,除非超时) SET SESSION transaction_isolation = 'REPEATABLE-READ'; START TRANSACTION; INSERT INTO test_isolation (`amount`) VALUES (400.00);如果会话 A 不提交,会话 B 的 INSERT 会一直等。等会话 A 提交之后,INSERT 才执行成功。这就是间隙锁和临键锁在起作用,也是 InnoDB 在可重复读级别下解决幻读的直接证据。把隔离级别切到读已提交再重复这个操作,会发现 INSERT 不会被阻塞,因为 RC 级别没有间隙锁。
这套验证流程尽量在自己本机的测试环境跑一遍,感受会完全不一样。尤其是二次查询结果不一致、插入被阻塞这两个现象,文字描述再多都不如亲眼看到一次记得牢。
5. 生产环境怎么选隔离级别:几个真实场景的经验
5.1 MySQL 默认 RR 的取舍与历史包袱
先说结论:如果你的系统没有明确的一致性需求,且对并发性能比较敏感,用 MySQL 默认的可重复读完全没问题,官方默认一定是最稳妥的选择。但你要清楚它相对读已提交在性能上的代价。
可重复读的最大代价是间隙锁。间隙锁锁的是范围而不是具体行,在高并发插入场景下,很容易出现锁范围互相覆盖导致死锁。举个我实际碰到过的例子:一个订单流水表,业务上按用户维度记录操作日志,多个用户并发下单插入记录时,如果某个事务先执行了范围查询的当前读,比如查某个时间段的订单,间隙锁会把相邻用户的数据插入也挡住,导致大量插入操作排队。
而且死锁排查起来比想象中难。两个事务各自持有一把间隙锁,又同时等对方释放,MySQL 会自动检测死锁并回滚一个事务,但业务侧收到的是 Deadlock found when trying to get lock 错误,需要重试机制兜底。很多团队处理这类问题的第一反应就是加索引或改 SQL,但真正有效的办法往往是把隔离级别降到读已提交。
读已提交不产生间隙锁,锁冲突概率显著下降。如果你的业务对同一事务内多次查询结果一致性没有硬性要求,切到 RC 通常能肉眼可见地减少锁等待和死锁发生频率。
5.2 什么时候必须用 RR,什么时候可以降级到 RC
我总结了一套自己的判断逻辑,不一定绝对严谨,但实际用下来比较靠谱。
必须保留可重复读的场景:一是业务里有“先查后写”且需要基于查询结果做判断的逻辑,比如读到一个账户余额,判断够不够扣款,然后再执行扣款,整个操作在一个事务里。如果不在同一事务内保持一致读取,可能发生余额被其他事务改掉、判断失效的情况。二是报表统计类场景,一张报表在生成过程中,如果数据源持续变化,统计结果会是拼凑出来的,不同时间点的数据混在一起,这种场景必须保证整个事务内看到一致的数据快照。三是金融交易类,对账、清算、账务处理,凡是涉及金额计算的历史数据读取,一律用可重复读,宁可性能差一点也不能数据出错。
可以降级到读已提交的场景:一是纯写入型业务,事务里基本都是 INSERT、UPDATE,只有很少的查询,不需要跨多次查询一致性。二是查询实时性要求高的场景,比如库存查询、价格查询,希望每次读到的都是最新的已提交数据,业务上也不关心同一事务内两次查询是否一致。三是高并发插入型业务,比如日志入库、消息记录、流水写入,这种场景下间隙锁是纯负担,RC 能省掉大量锁冲突。
我踩过的坑是:本来库存扣减系统用的可重复读,并发一高就频繁死锁,排查了两天才锁定是间隙锁互相覆盖导致。切换到读已提交后,死锁基本消失,因为库存扣减本身用 UPDATE ... WHERE 走的是记录锁,根本不需要间隙锁,快照读的一致性在这个场景也没意义。这个案例典型说明了隔离级别不是越高越好,而是越合适越好。
5.3 常见问题与排查技巧实录
最后整理几个我在实际运维和面试辅导中高频遇到的问题,都有对应的排查方法和处理思路。
第一个是锁等待超时。报错信息通常是 Lock wait timeout exceeded,默认时长是 50 秒。排查三步走:先执行 SHOW ENGINE INNODB STATUS,看 LATEST DETECTED DEADLOCK 和 TRANSACTIONS 部分,找到持锁事务的 SQL;再查 information_schema.innodb_trx 表,找到长时间未提交的事务;最后找到源头,多半是有事务没提交,或者长事务里带着范围查询的当前读。处理手段也分轻重:紧急情况直接 KILL 掉持锁事务,治本需要查代码里有没有事务忘提交、有没有大事务,以及考虑降隔离级别。
第二个是死锁。死锁和锁等待超时的区别是,死锁是因为两个事务互相持有对方需要的锁,MySQL 会自动回滚其中一个,应用层会收到 Deadlock found 错误。排查思路是看错误日志里展示的两个事务的 SQL,分析各自的锁顺序。常见的修复方式:调整 SQL 的执行顺序,让所有事务按相同的顺序访问表和行;缩小事务范围,减少持锁时间;如果死锁集中在范围查询上,优先考虑降隔离级别到读已提交。
第三个是事务里查询结果和预期不符。比如同一个事务里,先 SELECT 查出来的数据是老值,然后执行 UPDATE 把值改掉,再 SELECT 发现是新值,但另一个查询还是老值。这个现象我见过很多次,本质是没分清快照读和当前读。排查时先确认 SQL 里是否用了 FOR UPDATE 或者是否混用了普通 SELECT 和 DML,再看隔离级别设置。如果是可重复读,普通 SELECT 用快照读,数据可能和最新值不一致,需要更新数据时直接走 UPDATE,不要先 SELECT 再凭快照里的值判断。
第四个是 binlog 格式和隔离级别的配合问题。MySQL 5.7 默认的 binlog_format 是 ROW,这种情况下读已提交可以安全使用。如果你还在用老的 binlog_format=STATEMENT,并且想降隔离级别,主从复制可能出问题,因为语句格式下,可重复读是保证主从一致性比较稳妥的前提。要进行改造的话,先把 binlog_format 改成 ROW,再降隔离级别,顺序别搞反。
这套隔离级别的知识,光靠背概念是记不牢的。我自己的体会是:先搭一个本地 MySQL,把四个级别分别跑一遍脏读、不可重复读、幻读的实验,再把生产环境里遇到过的一两个死锁日志翻出来对着分析,效果比看十篇理论文章都强。你在排查线上问题的时候,拿到一个锁等待的报错,先别急着改 SQL,想想事务隔离级别在这个场景里扮演了什么角色,往往答案就出来了。
如果后续你的业务在 RR 和 RC 之间纠结,我的建议是先用 RC 做压测,观察死锁和锁等待的数量变化,同时让业务方确认是否接受同一事务内多次查询结果可能不一致。大多数业务场景接受这个变化,切换后你会明显感觉到锁相关的报错少了很多。