mysql的repeatable read能防止不可重复读,但仅限于普通快照读;若使用加锁读(如select ... for update)或降级为read committed,则可能触发不可重复读。

什么是不可重复读,它在 MySQL 里怎么表现
不可重复读是指:同一个事务中,两次 SELECT 同一条记录,结果不一致(被其他事务的 UPDATE 或 DELETE 修改了)。这不是幻读(幻读是查出新插入的行),也不是脏读(脏读是读到未提交的修改)。
典型复现场景:REPEATABLE READ 隔离级别下,事务 A 查询某条订单状态为 'pending',事务 B 提交更新为 'shipped',事务 A 再查,发现值变了——这说明当前隔离级别没拦住不可重复读?错,其实是你用了 SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE 以外的普通查询,而 MySQL 的 REPEATABLE READ 默认靠 MVCC 快照读实现,本该避免不可重复读。但如果你显式加了锁、或用了 READ COMMITTED,就可能触发。
MySQL 默认的 REPEATABLE READ 真的能防不可重复读吗
能,但仅限于快照读(即普通 SELECT)。它的实现依赖的是事务启动时创建的一致性视图(consistent read view),后续所有普通 SELECT 都基于这个快照,不会看到其他事务的已提交修改。
-
REPEATABLE READ下,两次普通SELECT id FROM orders WHERE order_id = 123一定返回相同结果,不管其他事务是否已提交UPDATE - 但一旦用了加锁读(如
SELECT ... FOR UPDATE),就会读最新已提交版本,并加行锁——此时就可能“重复读不一致” -
READ COMMITTED每次SELECT都新建快照,所以必然出现不可重复读;这是它的设计行为,不是 bug - MySQL 8.0+ 的
READ COMMITTED在加锁读时还会用“间隙锁降级”,进一步放大不一致风险
想彻底避免不可重复读,该选哪个隔离级别和读法
如果业务逻辑要求“同一事务内多次读必须严格一致”,且不能接受锁等待或死锁,优先用 REPEATABLE READ + 普通 SELECT。若必须加锁(比如要防止并发修改),就需配合应用层逻辑或升级到 SERIALIZABLE——但代价是性能陡降、锁范围扩大。
- 别在
REPEATABLE READ下混用快照读和加锁读:比如先SELECT判断状态,再SELECT ... FOR UPDATE修改,中间可能被篡改 - 真需要强一致性读写,把判断和更新合并成原子语句,例如:
UPDATE orders SET status = 'shipped' WHERE order_id = 123 AND status = 'pending' -
SERIALIZABLE会让所有普通SELECT隐式转成SELECT ... LOCK IN SHARE MODE,容易导致锁冲突,线上慎用 - 监控
Innodb_row_lock_waits和slow_query_log中带FOR UPDATE的语句,它们往往是不可重复读问题的放大器
实战中最容易被忽略的三个点
很多不可重复读问题其实不出在隔离级别本身,而出在开发习惯和框架行为上。
- ORM(如 MyBatis、Hibernate)默认开启二级缓存或 Session 缓存,可能掩盖真实数据库状态,误以为“没变”,实则数据库早已被改——关掉缓存或显式
clearCache()测试 - 连接池(如 HikariCP)配置了
connection-init-sql自动执行SET SESSION TRANSACTION ISOLATION LEVEL ...,但某些中间件(如 ShardingSphere)会覆盖它,导致实际隔离级别与预期不符 -
autocommit=1时,每个SELECT都是独立事务,根本谈不上“重复读”——检查你的连接是否意外处于自动提交模式










