read committed 是高并发写场景下更优的隔离级别,但需配合 row 格式 binlog、索引优化及事务精简才有效;repeatable read 默认启用间隙锁导致过度加锁,而 rc 下基本关闭间隙锁,可显著减少锁竞争。

READ COMMITTED 是多数高并发写场景下最值得优先尝试的隔离级别,但它必须配合 ROW 格式 binlog 和索引优化才真正生效,单独改级别反而可能引入主从不一致或不可重复读问题。
为什么默认的 REPEATABLE READ 容易引发锁竞争
MySQL 默认用 REPEATABLE READ,它为防止幻读,默认启用间隙锁(Gap Lock)。比如执行 UPDATE orders SET status = 'done' WHERE user_id = 123,即使 user_id 有索引,InnoDB 仍可能锁住 user_id 对应索引范围内的所有间隙——哪怕表里只有 user_id=123 这一行,也可能把 120~125 的整个区间锁死,阻塞其他事务插入或更新 nearby 数据。
这种“防御性加锁”在金融类强一致性场景合理,但在电商订单、日志归档、消息队列消费等场景中,往往过度保守。
- 间隙锁只在
REPEATABLE READ下默认启用,READ COMMITTED中基本关闭(唯一索引上的重复键检查除外) - 间隙锁不是“锁某几行”,而是“锁住一段索引空隙”,极易导致看似无关的 SQL 相互阻塞
- 使用
EXPLAIN看不到间隙锁,但通过SHOW ENGINE INNODB STATUS能观察到 lock_mode GAP 或 lock_mode Insert Intention
设置 READ COMMITTED 的硬性前提
直接执行 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED 只影响当前连接,对锁竞争改善有限;真正起效需要全局配置 + binlog 格式匹配。
- 必须确认
binlog_format是ROW:SHOW VARIABLES LIKE 'binlog_format',非ROW时主从可能不一致,且部分版本会静默退回到间隙锁行为 - 全局设置才有效:
SET GLOBAL tx_isolation = 'READ-COMMITTED'(注意是tx_isolation,不是transaction_isolation,旧版本字段名不同) - 应用层需接受“不可重复读”:同一事务内两次
SELECT可能返回不同结果,比如库存查询后被其他事务扣减 - 重启 MySQL 后该变量会恢复默认,需写入
my.cnf持久化:transaction-isolation = READ-COMMITTED
WHERE 条件没走索引时,隔离级别再低也救不了你
即使设成 READ COMMITTED,如果 UPDATE 或 SELECT ... FOR UPDATE 的 WHERE 条件没命中索引,InnoDB 仍会扫描全表并为每一行加记录锁——这时锁粒度和 REPEATABLE READ 下几乎一样,甚至更糟(因为 RC 下锁释放更早,但等待队列可能更密集)。
- 用
EXPLAIN检查执行计划:type字段不能是ALL或index,理想是const、ref或range - 联合索引要遵守最左前缀,
WHERE created_at > '2024-01-01' AND status = 'pending'若索引是(status, created_at),则无法利用 - 避免隐式转换:
WHERE id = '123'(id 是 INT)会导致索引失效,锁范围暴增
innodb_lock_wait_timeout 不是越大越好
默认 50 秒的锁等待超时,在高并发写入下容易引发雪崩:一个慢事务卡住后,大量请求排队等待,最终在同一秒内集体抛出 Lock wait timeout exceeded,触发批量重试,流量尖峰回压。
- 建议设为
3~5秒:SET innodb_lock_wait_timeout = 3(会话级,需在连接池初始化时设置) - 应用层必须配套快速失败 + 指数退避重试,不能只依赖数据库超时
- 这个参数不解决锁冲突根源,只是让系统“疼得更快、分散些”,真正的瓶颈还在索引、事务粒度和隔离级别协同上
真正降低锁竞争的关键不在单点调参,而在于三者咬合:隔离级别降下来、索引让 WHERE 精准命中、事务生命周期缩到最小。任何一个环节掉链子,其他优化都打折扣。











