行锁加在索引上,而非数据行;即使无显式索引,innodb 也会通过隐藏聚簇索引加锁。主键更新只锁主键索引,二级索引更新需锁二级索引及主键索引,全表扫描则对每条匹配记录的主键索引项加锁。

行锁到底加在哪儿?不是数据行,而是索引上
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 会被降级为仅记录锁,间隙部分不锁 → 幻读可能发生,但锁范围更小、并发更高。
本书是全面讲述PHP与MySQL的经典之作,书中不但全面介绍了两种技术的核心特性,还讲解了如何高效地结合这两种技术构建健壮的数据驱动的应用程序。本书涵盖了两种技术新版本中出现的最新特性,书中大量实际的示例和深入的分析均来自于作者在这方面多年的专业经验,可用于解决开发者在实际中所面临的各种挑战。 本书内容全面深入,适合各层次PHP和MySQL开发人员阅读,既是优秀的学习教程,也可用作参考手册。
为什么 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 锁,若两个事务交叉执行,极易触发死锁——这种“语法糖”背后的锁行为,比直觉复杂得多。









