mysql默认repeatable read能防止不可重复读,但两次查询结果不一致通常因事务未真正启动(begin后首次select才生成read view)、客户端复用连接、误用当前读(如for update)、表引擎非innodb或混用快照读与当前读导致快照失效。

MySQL 默认的 REPEATABLE READ 隔离级别本身就能防止不可重复读,但业务逻辑出问题,90% 不是因为隔离级别设错了,而是快照没生效、读写混用或连接复用导致“以为在事务里,其实没真启动”。
为什么设了 REPEATABLE READ 还看到数据变了
这不是 MySQL 实现有 bug,而是常见实操断点让 MVCC 快照失效:
-
BEGIN后没立刻执行SELECT:InnoDB 在第一次普通SELECT时才真正生成 Read View,之前其他事务已提交的修改会被“看到” - 客户端(如 Navicat、DBeaver)复用连接:执行过
SET SESSION TRANSACTION ISOLATION LEVEL ...后未断开重连,会话仍沿用旧隔离级别,SELECT @@transaction_isolation可验证 - 表引擎不是
InnoDB:MyISAM等引擎不支持 MVCC,REPEATABLE READ形同虚设,用SHOW CREATE TABLE table_name确认 - 用了加锁读:
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE是当前读,跳过快照,直接读最新已提交版本
哪些操作会悄悄绕过可重复读语义
不是所有 SQL 都走快照。以下行为会让事务“意外看到新值”,看似违反隔离性,实则是设计如此:
-
UPDATE/DELETE/SELECT ... FOR UPDATE:这些是当前读,必须基于最新已提交状态做判断和加锁 - 事务内先
UPDATE某行,再SELECT同一行:InnoDB 对本事务修改过的行返回“自己刚写的值”,而非初始快照 - 查系统表:
INFORMATION_SCHEMA.INNODB_TRX或performance_schema表不走 MVCC,始终读物理最新状态 - 显式指定
START TRANSACTION WITH CONSISTENT SNAPSHOT:它强制立即生成 Read View,但如果此时有长事务未提交,该快照可能包含部分未提交变更(取决于活跃事务列表)
如何写出真正受保护的业务逻辑
靠隔离级别兜底不如靠代码明确表达意图。关键原则是:避免“先读再判再改”的三段式,把判断和更新合并为原子操作:
- 用带条件的
UPDATE替代SELECT+UPDATE:UPDATE orders SET status = 'shipped' WHERE order_id = 123 AND status = 'pending',返回影响行数为 0 就说明已被并发修改 - 如果必须分步(比如要记录旧值),确保两次
SELECT都是普通快照读,且中间不穿插任何当前读语句;更稳妥的是在同一个事务中用SELECT ... FOR UPDATE一次性锁定并读取 - 避免在
REPEATABLE READ下混用快照读和加锁读:例如先SELECT status判断,再SELECT ... FOR UPDATE修改——中间窗口期可能被篡改 - 监控真实瓶颈:
Innodb_row_lock_waits和慢日志中带FOR UPDATE的语句,往往是不可重复读问题的放大器,不是根源
最易被忽略的一点:事务是否真正启动,不看 BEGIN,而看第一次普通 SELECT 执行时刻。很多业务脚本在 BEGIN 后先做网络调用、日志打印或条件分支,等真正查数据库时,快照已经晚了一拍。











