不可重复读是指同一事务中多次SELECT相同语句却读到其他事务已提交的更新,导致结果不一致;MySQL默认REPEATABLE READ通过MVCC快照读避免该问题,但当前读(如SELECT ... FOR UPDATE)仍可能感知更新。

什么是不可重复读,它在 MySQL 中具体表现为什么
不可重复读是指:同一个事务中,多次执行相同的 SELECT 语句,却读到了其他事务已提交的修改结果,导致前后两次查询结果不一致。这不是幻读(幻读是读到了新插入的行),而是对**已有行的更新被感知到了**。
典型场景:SELECT → 其他事务 UPDATE ... COMMIT → 再次 SELECT,发现同一行数据变了。这在 READ COMMITTED 隔离级别下合法且常见。
MySQL 默认隔离级别能否避免不可重复读
MySQL 默认隔离级别是 REPEATABLE READ,它通过 MVCC + Next-Key Lock(在可重复读下对范围查询加锁)来防止不可重复读——但仅限于**快照读(普通 SELECT)**。
注意以下关键点:
-
REPEATABLE READ下,普通SELECT基于事务开启时的 Read View,不会看到其他事务后续的提交,因此能保证不可重复读不发生 - 但
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE是当前读,会绕过 MVCC 快照,直接读最新已提交版本,并加锁,此时可能“感知”到其他事务的更新(取决于加锁时机和范围) - 如果业务混用快照读与当前读,或依赖
SELECT结果做后续判断再UPDATE(即“读-改-写”逻辑),仍可能因条件竞态导致逻辑错误,这不是隔离级别失效,而是应用层未正确使用锁或未用SELECT ... FOR UPDATE
真正解决并发下的不可重复读问题,该怎么做
靠隔离级别本身只能缓解,不能根治所有业务场景。必须结合具体操作方式:
- 对需要“读取后立即修改”的逻辑,务必用
SELECT ... FOR UPDATE(在主键/索引列上)发起当前读并加行锁,确保从读到写之间该行不被其他事务修改 - 避免在
REPEATABLE READ下先普通SELECT拿值,再拼UPDATE条件——这存在窗口期,应改用原子语句如UPDATE ... WHERE id = ? AND status = 'pending' - 若涉及多行一致性(如转账需查两账户余额),单靠行锁不够,需用
SELECT ... FOR UPDATE锁住所有相关行,且按固定顺序(如 ID 升序)加锁,防死锁 - 高并发下过度依赖
SELECT ... FOR UPDATE可能导致锁等待甚至超时,此时应评估是否可用乐观锁(如version字段 +UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?)替代
不同隔离级别对不可重复读的实际影响对比
MySQL 支持的四种隔离级别对不可重复读的控制能力如下:
-
READ UNCOMMITTED:能看到未提交变更,必然出现不可重复读(以及脏读) -
READ COMMITTED:每次SELECT都新建 Read View,能看到其他事务已提交的更新 → 会出现不可重复读 -
REPEATABLE READ(MySQL 默认):事务启动时创建 Read View,后续快照读都复用 → 普通SELECT不会出现不可重复读 -
SERIALIZABLE:所有SELECT隐式转为SELECT ... LOCK IN SHARE MODE,强制串行化 → 理论上杜绝,但性能代价极大,极少使用
真正容易被忽略的是:即使在 REPEATABLE READ 下,只要用了 UPDATE、DELETE 或当前读,就不再受快照保护;而业务代码里往往没意识到自己触发了当前读。










