mysql锁是如何实现的_mysql底层机制解析

来源:这里教程网 时间:2026-02-28 20:51:53 作者:

行锁到底加在哪儿?不是数据行,而是索引上

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
锁,若两个事务交叉执行,极易触发死锁——这种“语法糖”背后的锁行为,比直觉复杂得多。

相关推荐