行锁到底加在哪儿?不是数据行,而是索引上
MySQL 的
InnoDB行锁从不直接锁“表里的某一行”,它锁的是**索引记录**——哪怕你没建任何索引,
InnoDB也会生成一个隐藏的聚簇索引(
GEN_CLUST_INDEX),所有行锁都落在这个索引的叶子节点上。
这意味着:
用主键更新:UPDATE t SET v=1 WHERE id=100→ 只在主键索引上加
X锁 用二级索引更新:
UPDATE t SET v=1 WHERE name='Alice'→ 先在
name索引上加
X锁,再回表到主键索引加
X锁(两次加锁) 没走索引的查询(如
WHERE status=1且
status无索引)→ 会退化为全表扫描,对**每条匹配记录的主键索引项**逐个加锁,等效于“伪表锁”,极易引发锁争用
SELECT ... FOR UPDATE
和 SELECT ... LOCK IN SHARE MODE
的真实行为
这两条语句是显式加锁的入口,但它们的效果高度依赖隔离级别和查询条件是否命中索引:
SELECT * FROM t WHERE id=5 FOR UPDATE(主键精确查找)→ 加
X记录锁(
LOCK_REC_NOT_GAP),只锁住 id=5 这一条
SELECT * FROM t WHERE age > 25 FOR UPDATE(范围查询,RR 隔离级)→ 加
Next-Key Lock(记录 + 间隙),既锁住所有满足条件的记录,也锁住这些记录之间的“间隙”,防止幻读
SELECT * FROM t WHERE name='Bob' LOCK IN SHARE MODE(二级索引+唯一值)→ 在
name索引和对应主键上各加
S锁;若
name不唯一,则可能锁住多个主键索引项
注意:
RC(读已提交)隔离级下,
Next-Key Lock会被降级为仅记录锁,间隙部分不锁 → 幻读可能发生,但锁范围更小、并发更高。
为什么 UPDATE
有时会锁全表?真相是没走索引或隐式类型转换
常见现象:一条看似简单的
UPDATE执行极慢,且阻塞其他事务。背后往往不是“引擎故意锁表”,而是优化器被迫放弃索引: 字段类型不一致:
WHERE phone='13800138000',但
phone是
BIGINT→ 触发隐式转换,索引失效,全表扫描+逐行加
X锁 函数操作:
WHERE DATE(create_time) = '2026-01-28'→ 索引无法使用,同样导致全表加锁 字符集/排序规则不匹配:关联字段或
WHERE条件中 collation 不一致,索引失效
验证方法:执行
EXPLAIN看
type是否为
ALL或
index,再结合
SHOW ENGINE INNODB STATUS\G查看
TRANSACTIONS部分的锁等待详情。
死锁不是 bug,是并发路径的必然产物——如何快速定位和规避
死锁在
InnoDB中由检测线程每秒唤醒一次主动发现,并牺牲其中一个事务(报错
Deadlock found when trying to get lock)。关键不在“避免死锁”,而在“让死锁更快暴露、更少发生”: 统一 DML 顺序:多个事务更新多张表时,始终按相同顺序(如先
orders再
order_items)加锁,打破循环等待 缩短事务长度:把非数据库操作(如 HTTP 调用、日志写入)移出事务块,减少持锁时间 避免在事务中用户交互:比如先
SELECT FOR UPDATE锁住记录,等用户点击“确认”才
UPDATE—— 这会让锁持有几十秒甚至几分钟 监控高频死锁点:
SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks'持续增长,配合慢日志和
information_schema.INNODB_TRX定位长事务
真正容易被忽略的一点:
INSERT ... ON DUPLICATE KEY UPDATE在有唯一索引冲突时,会先加
S锁再升级为
X锁,若两个事务交叉执行,极易触发死锁——这种“语法糖”背后的锁行为,比直觉复杂得多。
