SELECT ... FOR UPDATE有时锁住不该锁的行,是因为InnoDB在唯一索引查找失败时将间隙锁升级为临键锁以防止幻读,如WHERE id=100查不到记录却锁住(95,105)区间。

为什么SELECT ... FOR UPDATE有时会锁住不该锁的行
因为InnoDB在唯一索引查找失败时,会将“间隙锁”升级为“临键锁”,而你没意识到自己触发了锁升级条件。比如用WHERE id = 100查一条不存在的记录,若id是唯一索引,InnoDB会锁住(95, 105)这个区间——不是因为你写了FOR UPDATE,而是因为优化器判定该查询需要防止幻读。
常见错误现象:UPDATE t SET x=1 WHERE id=100被阻塞,但SELECT * FROM t WHERE id=100返回空;同时另一个事务正持有id=99或id=101的行锁。
- 显式加锁前先确认执行计划是否走唯一索引:用
EXPLAIN SELECT ... FOR UPDATE看key列 - 如果
type是range或index,哪怕条件看着像等值,也可能触发范围锁 - 非唯一索引+等值查询一定产生临键锁,哪怕只命中一行——这是默认行为,无法绕过
INSERT ... ON DUPLICATE KEY UPDATE到底锁哪些记录
它不是只锁冲突行,而是先尝试插入,失败后才升级为更新;这个“尝试插入”过程会申请插入意向锁(Insert Intention Lock),和间隙锁冲突。所以即使最终走的是UPDATE分支,也已经对目标间隙加了锁。
使用场景:高并发下单号去重、幂等写入。但容易踩坑的是,两个事务同时插入同一order_no,其中一个会等另一个释放间隙锁,而不是直接报错。
- 若主键/唯一键存在,
ON DUPLICATE KEY UPDATE会先获取插入意向锁,再获取对应行的X锁 - 若唯一键冲突发生在二级索引上,还会额外锁住该二级索引记录的聚簇索引主键行
- 不建议在热点唯一键上频繁用这个语法,锁等待链容易变长
怎么判断当前事务正在等待哪个锁
靠INFORMATION_SCHEMA.INNODB_TRX只能看到事务状态,真正要定位锁等待关系,必须连查INFORMATION_SCHEMA.INNODB_LOCK_WAITS和INNODB_LOCKS(MySQL 8.0.1后已废弃,改用performance_schema.data_locks)。
常见错误现象:应用日志显示SQL执行超时,但SHOW PROCESSLIST里看不到明显阻塞,其实是锁等待被内部消化了。
- MySQL 8.0+ 必须查
performance_schema.data_locks和data_lock_waits,旧视图返回空 -
LOCK_TRX_ID字段是十六进制,需转成十进制才能和INNODB_TRX.TRX_ID对上 - 注意
LOCK_MODE值:比如X,GAP表示间隙锁,X,REC_NOT_GAP才是纯行锁
显式锁能避免锁升级吗
不能。InnoDB没有“锁升级”机制(不像SQL Server),所谓“升级”其实是优化器根据隔离级别和索引类型自动选择锁粒度的结果。你写SELECT ... FOR UPDATE,它该加临键锁还是加间隙锁,由查询条件和索引结构决定,和是否显式无关。
最容易被忽略的一点:可重复读(RR)下,即使是SELECT ... FOR UPDATE,只要走的是非唯一索引,就会锁住整个扫描范围,不只是结果集里的行。
- 唯一索引 + 等值查询 → 只锁匹配行(行锁)
- 非唯一索引 + 等值查询 → 锁匹配行 + 对应间隙(临键锁)
- 无索引或全表扫描 → 所有行 + 所有间隙(相当于表级逻辑效果)
复杂点在于,同一个SQL在不同数据分布下锁范围可能完全不同;比如WHERE status = 'pending',当只有2条pending记录时锁2行,当有200万条时可能锁住整个索引段。这没法靠加提示解决,只能靠索引设计和业务分片收敛锁范围。










