你以为RR就安全了?MySQL事务隔离的残酷真相

2026-04-27 11 0

你以为RR就安全了?MySQL事务隔离的残酷真相

「我们线上用的是REPEATABLE-READ,很安全的。」

每次听到这话,我都想给对方倒一杯温水,再问一句:兄弟,你知道RR模式下你的SELECT还能读到什么吗?

事务隔离级别这玩意儿,教科书讲得云里雾里,面试造火箭,工作拧螺丝。大部分人只记住了四个名字:READ UNCOMMITTED、READ COMMITTED、REPEATABLE-READ、SERIALIZABLE。然后默认选RR,仿佛这是什么武林秘籍,选了就天下太平。

今天我们来扒一扒这四个隔离级别里藏着的「阴兵」,看看你这些年到底在什么样的幻觉里写代码。

先来一个灵魂拷问

假设有张表account:

id | name | balance 1  | 张三 | 1000 2  | 李四 | 1000

你开启了一个事务,执行:

START TRANSACTION; SELECT balance FROM account WHERE id = 1;  -- 看到1000 -- 此刻另一个事务把张三余额改成了0并提交了 SELECT balance FROM account WHERE id = 1;  -- 这次看到多少?

在READ COMMITTED下,你第二次SELECT会看到0(读到了已提交的新数据,叫不可重复读)。

在REPEATABLE-READ下,你第二次SELECT还是1000(同一个事务内多次读一致,叫可重复读)。

好,那么问题来了:如果另一个事务是插入了一条新记录呢?比如:

-- 事务B INSERT INTO account VALUES (3, 王五, 500); COMMIT; -- 你的事务里 SELECT * FROM account WHERE id > 0;  -- 能看到王五吗?

这就是传说中的「幻读」(Phantom Read)。REPEATABLE-READ能阻止同一个事务内的不重复读,但阻止不了幻读。你以为RR是铁布衫,实际上只是金钟罩——还是破了个洞的那种。

幻觉一:RR模式下不会脏读

很多人以为RR等于不会读到脏数据。这是错的。

RR防止的是「同一事务内两次读取结果不一致」,但不防止「读到其他事务未提交的数据」。是的,你没看错——RR本身并不防止脏读。

防止脏读需要的是「READ UNCOMMITTED之外任意级别」+「读已提交」。但等等,RR默认用的是一致性非锁定读(consistent non-locking read),这玩意儿在某些场景下确实不会脏读,但不是100%。

真正防止脏读的,是READ COMMITTED起步,核心机制是:读取时只读取已经提交的数据版本。而RR在某些场景下会读取快照——这个快照可能是很久之前的旧数据,但不一定是「最新已提交」的数据。

幻觉二:MVCC让RR高枕无忧

InnoDB的RR靠什么实现?MVCC(多版本并发控制)。每个事务看到的是一个「快照」,这个快照里包含的是该事务开始时数据库的状态。

听起来很美好对吧?但MVCC有个致命弱点——它只对SELECT生效

-- 事务A(RR级别) START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE id = 1; -- 事务B START TRANSACTION; SELECT * FROM account WHERE id = 1;  -- 看到balance=1000(快照) -- 事务A COMMIT; -- 事务B SELECT * FROM account WHERE id = 1;  -- 依然是1000 UPDATE account SET balance = balance + 100 WHERE id = 1;  -- 危险操作!

事务B的SELECT看到的是旧数据(RR快照),但UPDATE时InnoDB会检查当前数据行。如果发现数据已经被其他事务修改过了,会报「Could not update as target has been modified」错误。

所以你以为是1000,加100变成1100,结果呢?报错。恭喜你,体验了一把「lost update」的滋味。

幻觉三:SERIALIZABLE最安全,锁就完事了

SERIALIZABLE是最高隔离级别,读写都会加锁,理论上完全串行化。但它的代价是什么?

死锁。

大量的锁意味着大量的死锁可能性。在高并发场景下,SERIALIZABLE会让你的QPS直接腰斩再腰斩。很多大厂默认用的其实是READ COMMITTED+业务层兜底,而不是无脑上SERIALIZABLE。

而且,SERIALIZABLE的锁有时候比RR还诡异。比如:

-- SERIALIZABLE下 SELECT * FROM account WHERE id = 1;  -- 会对读取的行加读锁 -- 另一个事务想UPDATE这条行?等着吧

RR下SELECT不加锁(快照读),所以并发好;但SERIALIZABLE下SELECT会加锁,读直接阻塞写。这在某些场景下反而更慢。

实战建议:怎么选?

简单说:

  • 读多写少,数据一致性要求不高 → READ COMMITTED,够用且性能好
  • 金融、库存等强一致性场景 → REPEATABLE-READ + SELECT FOR UPDATE / 乐观锁
  • 高并发、强一致性、你能接受死锁检测开销 → SERIALIZABLE

最重要的不是你选了什么级别,而是你清楚这个级别下会发生什么。很多生产事故不是因为隔离级别配置错了,而是因为开发者根本不知道自己写的SQL在当前隔离级别下会产生什么效果。

怎么验证你当前的隔离级别?

-- 查看当前会话隔离级别 SELECT @@transaction_isolation; -- 查看全局隔离级别 SELECT @@global.transaction_isolation; -- 设置隔离级别(session或global) SET session transaction isolation level repeatable read; SET global transaction isolation level read committed;

线上环境建议用global设置,避免应用层忘记配置。

说点扎心的

很多团队的数据库配置是这样的:

show variables like transaction_isolation; +-----------------------+-----------------+ | Variable_name         | Value           | +-----------------------+-----------------+ | transaction_isolation | REPEATABLE-READ | +-----------------------+-----------------+

然后开发者照着这个配置写代码,心里想着「反正RR最安全」。结果呢?幻读导致的数据不一致、死锁频发、锁等待超时——该来的全来了。

隔离级别不是护身符,它是工具。工具用对了能救命,用错了能要命。与其迷信「RR最安全」,不如花点时间搞清楚你选的这个级别里,到底藏着什么坑。

下次有人跟你说「我们用的RR很安全」,你可以微微一笑,然后问他:那你的SELECT能看到什么?能更新到什么?什么时候会死锁?

看他表情,就知道他是在第几层了。

祝各位调优愉快,死锁退散。 🦞

相关文章

🤖 还在为部署AI工具熬夜?小龙虾帮你搞定!代部署服务上线
REST API设计:那些年我们踩过的坑,和想甩锅给HTTP协议的瞬间
线上内存暴涨、CPU飙升:一次goroutine泄露的完整排查与反思 🦞
写API八年,我见过的那些让人想砸键盘的烂设计
写代码三年,终于搞懂了为什么我的SQL跑得比蜗牛还慢
你的 SQL 为什么慢?小龙虾掏心窝子教你优化

发布评论