READ COMMITTED 能缓解锁竞争是因为关闭间隙锁、收缩锁粒度至命中行,但需ROW格式binlog、全局设置且接受不可重复读;它不是万能解药,因无法解决长事务、低效索引、锁升级等问题。

为什么 READ COMMITTED 能缓解锁竞争,但不是万能解药
MySQL 默认的 REPEATABLE READ 隔离级别下,普通 SELECT 不加锁,但 UPDATE/DELETE 会基于间隙锁(Gap Lock)锁定索引范围,容易造成不相关行被锁住——比如更新 ID=100 的记录,可能顺带锁住 ID=95~105 的整个间隙,阻塞其他事务插入或更新 nearby 数据。
READ COMMITTED 关闭间隙锁(仅在唯一索引等极少数场景保留),让锁粒度收缩到实际命中的行,显著降低锁冲突概率。但它只对当前事务生效,且要求 binlog 格式为 ROW(否则主从不一致)。
- 必须全局设置:
SET GLOBAL tx_isolation = 'READ-COMMITTED';,仅会话级设置无效 - 确认 binlog 格式:
SHOW VARIABLES LIKE 'binlog_format';,非ROW时需同步改配置并重启 MySQL - 应用层需接受“不可重复读”:同一事务中两次
SELECT可能返回不同结果
如何用 SELECT ... FOR UPDATE 精准加锁,避免锁升级
盲目在 UPDATE 前加 SELECT ... FOR UPDATE 可能引发更重的锁(如锁全表或大范围索引),尤其当 WHERE 条件没走索引时。本质是让 MySQL 提前明确锁范围,而非靠 UPDATE 自动推断。
关键在 WHERE 条件必须命中**有效索引**,且尽量窄。例如 WHERE status = 'pending' AND created_at > '2024-01-01' 若 status 是低基数字段(如只有 pending/done),即使有索引,MySQL 仍可能放弃使用、转为全表扫描加锁。
- 先执行
EXPLAIN SELECT ... FOR UPDATE,确认key和rows字段是否合理 - 避免在
FOR UPDATE查询中使用OR、函数、隐式类型转换(如WHERE id = '123'对 INT 字段) - 如果业务允许,用主键或唯一索引精确查询,如
SELECT * FROM orders WHERE order_id = 12345 FOR UPDATE
为什么 innodb_lock_wait_timeout 设太小反而加剧雪崩
默认 50 秒的锁等待超时看似合理,但在高并发写入场景下,一个慢事务卡住后,大量后续请求会在 50 秒内排队等待,最终同时抛出 Lock wait timeout exceeded,触发批量重试或失败,形成尖峰流量回压。
更稳妥的做法是主动缩短等待时间(如设为 3~5 秒),配合应用层快速失败 + 指数退避重试,把压力分散开。但注意:这个参数是会话级的,SET innodb_lock_wait_timeout = 3; 只影响当前连接。
- 不要全局设为 1 秒——短于多数正常事务执行时间,会导致误杀
- 在连接池初始化时统一设置,避免每个请求都手动 SET
- 监控
Innodb_row_lock_waits和Innodb_row_lock_time_avg,若平均等待超 100ms,说明锁已成瓶颈
分库分表前,先检查 autocommit 和事务边界是否合理
很多锁竞争其实源于事务过大:一个事务里混着日志写入、缓存更新、消息发送、多张表修改。哪怕只是单库,长事务也会持锁过久,拖垮并发能力。
核心原则是「事务只做数据一致性必须的事」。比如订单创建事务应只包含 insert order + insert order_item,支付状态更新、积分发放、通知推送这些都该剥离出去异步处理。
- 确认所有写操作都在显式事务中:
SET autocommit = 0;后必须配COMMIT或ROLLBACK,否则连接长期占用锁 - 避免在事务中调用外部 HTTP 接口或执行耗时计算
- 用
SELECT ... LOCK IN SHARE MODE替代部分读+写场景,比FOR UPDATE冲突概率更低
真正难的不是加锁或分片,而是判断哪条 SQL 的锁范围超出业务需要,以及谁在不知不觉中延长了事务生命周期。











