mysql中隔离级别的选择与并发控制优化

READ COMMITTED 是大多数业务的合理起点,因其不使用间隙锁、锁粒度更小、并发更高,适用于订单更新、库存扣减等对延迟敏感且不强依赖事务内多次读一致性的场景。

为什么 READ COMMITTED 是大多数业务的合理起点

MySQL 默认隔离级别是 REPEATABLE READ,但它在可重复读语义下依赖间隙锁(gap lock)来防止幻读,这会显著放大锁范围,尤其在 WHERE 条件未命中索引或使用范围查询时,容易导致锁冲突和死锁。而 READ COMMITTED 下,每次快照读都基于语句开始时刻的最新已提交版本,不使用间隙锁(仅用记录锁),锁粒度更小、并发更高,且能避免大部分因间隙锁引发的阻塞问题。

适用场景包括:订单状态更新、库存扣减、日志写入等「读-改-写」链路短、不强依赖事务内多次读一致性、但对响应延迟敏感的业务。

  • 切换前确认应用没依赖 REPEATABLE READ 的“事务内多次读结果一致”特性(比如先查再判断再更新,中间不能被其他事务干扰)
  • 需配合 innodb_locks_unsafe_for_binlog = OFF(默认值),否则可能在 RC 下也退化出间隙锁行为
  • 主从复制建议使用 ROW 格式,避免 RC + statement 格式下因执行顺序差异引发数据不一致

什么时候必须用 SERIALIZABLE 或显式加锁

SERIALIZABLE 会将所有普通 SELECT 自动转为 SELECT ... LOCK IN SHARE MODE,开销极大,生产环境几乎不用。真正需要强一致控制的场景,应主动用更细粒度的锁:

  • 超卖防控:用 SELECT ... FOR UPDATE 配合唯一索引条件(如 SELECT stock FROM items WHERE id = 123 FOR UPDATE),确保只锁目标行
  • 避免幻读关键逻辑:若业务要求「本次事务中新增的数据不能被后续同条件查询看到」,且无法接受应用层重试,则需 SELECT ... FOR UPDATE 覆盖范围(如 WHERE category = 'A'),但务必确认该字段有合适索引,否则升级为表锁
  • 不要依赖 SERIALIZABLE 解决并发问题——它掩盖设计缺陷,且吞吐断崖下跌

READ UNCOMMITTED 的真实风险不止是脏读

很多人以为 READ UNCOMMITTED 只是可能读到未提交数据,实际上它还会导致 MySQL 优化器跳过 MVCC 快照机制,直接读取聚簇索引最新物理行。这意味着:

  • 可能读到正在被 DELETE 但尚未提交的行(即“幽灵行”)
  • 可能读到 UPDATE 中间状态(如半更新的字段值,尤其涉及大字段或二级索引维护时)
  • 备份工具(如 mysqldump --single-transaction)在 RU 级别下无法保证一致性快照
  • 除极少数审计类只读服务(明确接受数据毛刺),不建议在任何业务表上启用

如何验证当前事务实际持有的锁

光看隔离级别不够,得观察运行时锁行为。关键命令只有两个:

SELECT * FROM performance_schema.data_locks;

它显示每个事务当前持有的锁类型(RECORD / GAP / NEXT-KEY)、锁住的索引、事务 ID;配合下面这条查阻塞关系:

SELECT * FROM performance_schema.data_lock_waits;

常见误判点:

  • data_locks 不显示意向锁(IX/IS),但它们是行锁前提,排查死锁时必须意识到其存在
  • 间隙锁在 REPEATABLE READ 下即使没有显式 FOR UPDATE 也可能自动加,比如 UPDATE t SET x=1 WHERE y > 100 会锁住 y>100 的所有间隙
  • 唯一索引等值查询(含主键)在 RC 和 RR 下都只加记录锁,不会加间隙锁——这是调优突破口

复杂业务逻辑里,并发瓶颈往往不在 SQL 写法,而在事务边界是否过宽、是否在事务内混入 RPC 或文件 IO。锁只是症状,事务设计才是根因。