SELECT ... FOR UPDATE 在高并发下易卡住,本质是加行级排他锁,若无索引会升级为范围锁或表锁,且多事务乱序更新易致死锁;应确保条件走索引、缩短事务、用唯一键定位、拆分读写、避免大查询阻塞锁。

为什么 SELECT ... FOR UPDATE 在高并发下容易卡住
本质是它会在满足条件的行上加排他锁(X锁),如果查询没走索引,InnoDB 会升级为表级锁或锁住整个索引范围;更常见的是多个事务按不同顺序更新同一组行,直接触发死锁或长时间等待。
实操建议:
- 确保
WHERE条件字段有有效索引,用EXPLAIN验证是否走了索引扫描(type字段别是ALL或index) - 避免在事务里先
SELECT ... FOR UPDATE再做复杂计算或远程调用,锁持有时间越长,冲突概率越高 - 更新前尽量用唯一键定位,比如用
id而非模糊条件如status = 'pending',后者可能锁住数百行
用 INSERT ... ON DUPLICATE KEY UPDATE 替代“查再更”逻辑
典型场景:扣库存、生成幂等订单、计数器自增。传统写法是 SELECT 判断是否存在 → INSERT/UPDATE,中间存在竞态窗口,还多一次网络往返和锁等待。
实操建议:
- 给业务关键字段(如
order_no、user_id+item_id联合)建唯一索引,这是该语句生效的前提 - 注意
ON DUPLICATE KEY UPDATE中的赋值表达式不支持子查询,且VALUES(col)指的是本次INSERT尝试插入的值,不是原记录值 - 该语句本身是原子的,不会触发额外锁等待,但若发生重复键冲突,仍会对目标行加 X 锁 —— 所以唯一索引设计要尽量窄,避免锁扩散
把长事务拆成多个短事务,尤其避开 UPDATE 前的大查询
MySQL 的行锁在事务提交后才释放,一个事务里先跑个几秒的 SELECT COUNT(*) JOIN ...,再执行 UPDATE,等于把锁占用了几秒钟,别人全得排队。
实操建议:
- 读操作(统计、校验、渲染)和写操作(扣减、状态变更)必须分离到不同事务,读不加锁就用
READ COMMITTED隔离级别 - 批量更新别用单条
UPDATE循环,改用INSERT INTO ... SELECT或分片WHERE id BETWEEN ? AND ?,控制每次最多影响 1000 行 - 检查慢查询日志,重点关注
Rows_examined远大于Rows_affected的UPDATE,说明它在扫描大量无关行
innodb_lock_wait_timeout 不是解药,而是兜底报警开关
调大这个值(默认 50 秒)只会让等待更久,掩盖锁竞争真实压力;调太小又会导致业务频繁报 Lock wait timeout exceeded,但用户看到的只是失败,不知道哪条 SQL 卡住了谁。
实操建议:
- 线上保持默认 50 秒,配合监控抓取
SHOW ENGINE INNODB STATUS\G中的LATEST DETECTED DEADLOCK和TRANSACTIONS部分 - 用
performance_schema.data_locks表实时查谁在等哪把锁(MySQL 8.0+),比靠日志猜快得多 - 真正要优化的是锁粒度和事务长度,而不是让应用重试更勤快 —— 重试本身会加剧竞争
UPDATE 看似简单,只要 WHERE 条件匹配了 10 万行,或者索引失效导致全表扫描,它就可能成为整个库的瓶颈。










