select ... for update 在高并发下易卡住,本质是加 next-key lock(记录+间隙锁),事务未提交则阻塞其他更新或插入;应走唯一索引、紧邻 update 执行、避免混用一致性读,并优先用 update ... where 校验替代显式加锁。

为什么 SELECT ... FOR UPDATE 在高并发下容易卡住
本质是它会在满足条件的索引记录(甚至间隙)上加 next-key lock,只要事务没提交,其他事务想更新同一行或插入“相邻值”就会被阻塞。常见于秒杀、库存扣减等场景,一个慢查询拖住整个队列。
实操建议:
- 确保
WHERE条件走**最左匹配的唯一索引**(比如id或order_no),避免全表扫描导致锁住大量无关行 - 把
SELECT ... FOR UPDATE放在事务最末尾、紧挨着UPDATE执行,缩短锁持有时间 - 不要在事务里混用
SELECT ... FOR UPDATE和普通SELECT——后者可能触发一致性读,让前者更难定位目标行 - 如果只是校验后更新,优先用
UPDATE ... WHERE stock > 0+ 检查ROW_COUNT(),绕过显式加锁
如何判断是不是 INSERT 引发的间隙锁竞争
当多个线程同时插入主键/唯一键接近的值(如自增 ID 连续分配、订单号按时间生成),InnoDB 会对插入位置前后的间隙加锁,造成串行化。典型现象是 SHOW ENGINE INNODB STATUS 中看到 *** (1) HOLDS THE LOCK(S) 和 *** (2) WAITING FOR THIS LOCK TO BE GRANTED 并指向 supremum pseudo-record。
实操建议:
- 用
REPLACE INTO或INSERT ... ON DUPLICATE KEY UPDATE替代先SELECT再INSERT,减少锁范围 - 批量插入时,按主键升序排序后再执行,降低间隙重叠概率
- 对非核心业务表,可考虑关闭唯一检查:
SET UNIQUE_CHECKS=0(仅限导入场景,且需确保数据不冲突) - 若使用 UUID 或雪花 ID,注意避免因无序写入加剧 B+ 树分裂和间隙锁膨胀
innodb_lock_wait_timeout 调小真能缓解锁争用吗
不能。它只控制单个语句等待锁的上限时间(默认 50 秒),超时抛出 Lock wait timeout exceeded 错误,但不减少锁本身的存在或竞争强度。盲目调小只会让失败更快,掩盖真实瓶颈。
实操建议:
- 先用
SELECT * FROM information_schema.INNODB_TRX查活跃事务,结合INNODB_LOCK_WAITS定位谁在持锁、谁在等 - 检查长事务:未提交的事务、嵌套过深的存储过程、应用层连接未 close 都会导致锁滞留
- 确认隔离级别是否必要:多数场景用
READ COMMITTED就够了,REPEATABLE READ是间隙锁泛滥的主因 - 真正有效的“降权”手段是拆分大事务、拆分热点行(如把单条库存记录拆成多条 hash 分片)
乐观锁真的适合 MySQL 高并发更新吗
适合,但必须配合正确的实现。直接用 version 字段做 UPDATE ... SET version = version + 1 WHERE id = ? AND version = ? 是可行的,但要注意:MySQL 的 UPDATE 返回影响行数为 0 时,应用必须重试逻辑,且重试不能无限循环(要加退避或熔断)。
实操建议:
- 避免在乐观锁更新前再查一次全量数据——这会引入额外的一致性读开销,反而增加竞争
- version 字段必须是
INT UNSIGNED且有索引(哪怕只是单字段索引),否则WHERE可能走全表扫描 - 如果业务允许最终一致性,可用消息队列削峰,把“更新库存”转为异步状态机,彻底避开数据库锁
- 注意 binlog 格式:
STATEMENT模式下乐观锁重试可能导致主从不一致,推荐ROW模式
实际优化中最容易被忽略的,是把锁争用当成纯数据库问题去调参,而没回到业务逻辑看能不能用状态分离、预占、异步化等方式绕开锁。数据库锁不是性能瓶颈的终点,往往是设计拐点的提示灯。










