17370845950

REPEATABLE READ vs READ COMMITTED 的业务影响对比
REPEATABLE READ 并未真正消除幻读,而是通过间隙锁阻止新记录插入;READ COMMITTED 每次 SELECT 都新建快照,导致不可重复读;binlog_format 非 ROW 时切换隔离级别易致主从不一致;显式加锁和 CAS 更新比依赖隔离级别更可靠。

REPEATABLE READ 下的幻读问题其实没消失

很多人以为 REPEATABLE READ 能彻底避免幻读,但 MySQL InnoDB 的实现是通过间隙锁(Gap Lock)+ 行锁来“阻止”新记录插入,而不是真正消除幻读语义。这意味着:在同一个事务里两次执行

SELECT ... WHERE x > 10,第二次不会看到新插入的满足条件的行——但这只是锁机制压制了并发写入,并非快照隔离意义上的“无幻读”。

实际业务中容易踩的坑:

  • 高并发插入场景下,REPEATABLE READ 可能导致大量间隙锁冲突,出现 Lock wait timeout exceeded
  • 使用 SELECT ... FOR UPDATE 时,InnoDB 会锁住范围,哪怕 WHERE 条件命中的是空结果集,也会加间隙锁
  • ORM 自动生成的分页查询(如 WHERE id > ? LIMIT 20)在 REPEATABLE READ 下可能因间隙锁阻塞写入,拖慢吞吐

READ COMMITTED 不锁间隙,但每次 SELECT 都开新快照

READ COMMITTED 在 InnoDB 中意味着:每个 SELECT 语句都会生成一个新的一致性读视图(consistent read view),不复用事务内之前的快照。这直接导致同一事务中两次读取可能看到不同数据——不是脏读,而是“不可重复读”真实发生。

典型影响场景:

  • 转账类逻辑若写成「先查余额 → 判断 → 更新」三步且未加锁,在 READ COMMITTED 下第二步判断依据的余额可能已被其他事务改过
  • 报表导出类任务如果跨多个 SELECT 拼接数据,各表快照时间点不同,最终结果可能逻辑自洽但业务上失真(比如订单数和支付流水数对不上)
  • 触发器或存储过程中隐式调用的 SELECT,也各自开快照,行为更难预测

binlog 格式与隔离级别必须匹配

MySQL 启用 binlog_format = ROW 时,REPEATABLE READREAD COMMITTED 对主从复制的影响差异不大;但若设为 MIXEDSTATEMENT,问题就来了:

  • READ COMMITTED 下的非确定性函数(如 NOW()UUID())在 STATEMENT 模式下可能导致主从不一致
  • InnoDB 在 READ COMMITTED 下不启用间隙锁,而某些 DML 语句(如 DELETE ... LIMIT)在 STATEMENT binlog 中可能被重放为不同结果
  • 线上切换隔离级别前,务必确认 binlog_formatROW,否则复制延迟或数据错乱风险陡增

业务代码里的显式锁往往比隔离级别更可靠

依赖隔离级别“自动保护”很容易误判。比如认为 REPEATABLE READ 就能保证「查-改」原子性,实际上它只保证读结果不变,不保证后续更新不失败。

真正稳的做法是主动控制:

  • 关键路径用 SELECT ... FOR UPDATE 显式加锁,且确保 WHERE 条件能命中索引(否则升级为表锁)
  • 更新操作尽量走 UPDATE ... WHERE version = ? + CAS 逻辑,比依赖事务隔离更直观可控
  • 避免在事务里做 HTTP 调用、文件 IO 或长耗时计算——无论什么隔离级别,锁持有时间一长,所有并发优势都归零

最常被忽略的一点:应用层连接池配置的 defaultTransactionIsolation 可能被框架覆盖,上线前必须用 SELECT @@tx_isolation 实际验证,不能只信配置文件。