前阵子帮一个团队排查线上死锁,报错日志特别典型:两条 UPDATE 执行时间只差了几毫秒,最后各自等到了对方持有的锁。开发同事的第一反应是“是不是SQL写错了”,但等我把锁等待链一条条拆开,他们才明白问题根本不在SQL语法,而在事务的加锁顺序和InnoDB的锁实现细节上。这也是我总在团队里强调的一句话:如果不懂锁,就谈不上真正懂MySQL的并发控制。
这篇文章想跟你聊的,就是MySQL(主要是InnoDB引擎)里锁的实现细节。我会从锁的类型和粒度讲起,然后钻到InnoDB的锁结构、隐式锁、死锁检测机制,再用真实案例把排查过程完整走一遍。内容面向后端开发、DBA和有志于搞懂MySQL面试核心题的同学,能帮你把“行锁”“间隙锁”“死锁”这些概念串成一条能落地的知识链,而不是靠背结论应付面试。
1. 先搞清楚一个基础问题:MVCC都解决“读”了,为什么还要锁
很多人刚接触InnoDB时会有个困惑:不是有MVCC多版本并发控制吗?读和写不就能互不阻塞了吗?那锁到底在锁什么?
要理解MySQL的锁实现细节,第一步恰恰是要把MVCC和锁的边界划清楚。MVCC解决的问题是“快照读”和“当前写”之间的阻塞关系,它能让普通SELECT不用等待正在进行的修改事务提交;但MVCC并没有解决“多个写事务同时修改同一行”的问题。两个事务同时对同一条记录做UPDATE,如果都没有锁机制,那就必然出现丢失更新。所以InnoDB设计了一套组合拳:MVCC负责让读不加锁也能拿到一致快照,锁负责保证写操作之间严格串行化。
1.1 MVCC的快照读与不加锁读
MVCC的核心手段是undo log版本链和ReadView。每个事务在REPEATABLE READ隔离级别下首次执行普通SELECT时,会生成一个ReadView,用来决定当前事务能看到哪个版本的记录。记录被修改时,旧版本不会立刻覆盖,而是通过undo log串成一条版本链,新版本记录上还带着trx_id。
普通SELECT走的就是快照读,它不需要加任何锁,这是InnoDB并发读性能高的根源。注意,即便是快照读,也要根据ReadView去版本链里找可见版本。如果不对应版本,就顺着undo回滚指针往上找,找到满足可见性判断的历史版本为止。整个过程只读undo,不触及其他事务持有的行锁,天然不会产生阻塞。
但快照读也有局限:它读到的可能是历史版本,而不是所有事务提交后的最新值。有些业务场景必须看到最新结果,比如“先查再改”的库存扣减、订单状态流转、账户余额增减,这类操作不能容忍读到旧版本后再覆盖写。于是有了当前读,以及当前读需要加的锁。
1.2 当前读与三种加锁操作
当前读也常被叫作“锁定读”,它读取的一定是记录的最新已提交版本,并在读取时对目标记录加锁。触发当前读的操作主要有四类:
sql复制-- 显式锁定读
SELECT ... FOR UPDATE;
SELECT ... FOR SHARE; (MySQL 8.0,之前的 LOCK IN SHARE MODE 同理)
-- 隐式锁定读(DML天然就是当前读)
UPDATE ...;
DELETE ...;
-- INSERT 也一样要加锁,只是情况更特殊
INSERT ...;
当一条UPDATE执行时,InnoDB会先以当前读的方式定位到目标记录,拿到记录后加上排他锁(X锁),再去做数据修改。为什么UPDATE不用快照读?因为UPDATE之后还要写,如果基于一个旧版本去覆盖新值,就会产生“丢失更新”,这是事务隔离绝不允许的。
SELECT ... FOR UPDATE和SELECT ... FOR SHARE则是开发者主动放弃快照读,强制读取最新版本并加锁。它们对应的语义是“我要基于这条记录做后续业务操作,期间不允许其他人修改”。SELECT ... FOR SHARE加的是共享锁(S锁),多个事务可以同时对同一行加S锁;而SELECT ... FOR UPDATE加的是排他锁(X锁),与任何其他锁都互斥。
注意:这里的互斥要考虑锁模式兼容矩阵。S与S兼容,S与X不兼容,X与任何锁都不兼容。但实际还要结合锁类型和隔离级别综合判断,这一点后文会展开。
1.3 隔离级别一变,锁的范围全变
另一个关键点是:同一个SQL,在不同隔离级别下,加锁范围可能完全不同。InnoDB默认隔离级别是REPEATABLE READ(可重复读),在这个级别下InnoDB不仅要防止已提交的事务修改目标行,还要防止其他事务往目标范围内插入新行,也就是防幻读。防幻读靠的就是间隙锁(Gap Lock)和临键锁(Next-Key Lock)。
如果你把隔离级别调整成READ COMMITTED(读已提交),InnoDB就会放弃间隙锁,只对命中的索引记录加记录锁(Record Lock)。这样并发插入能力会上升,但代价是同一事务两次快照读可能读到不同结果,你需要通过其他手段保证业务逻辑一致。
在实际生产环境里,如果binlog格式是ROW,很多团队会把隔离级别调成READ COMMITTED来降低锁冲突,因为ROW格式下事务执行结果已经确定,不再依赖REPEATABLE READ的间隙锁来确保主从一致。但这么调整的前提是业务可以接受“当前读会话之间可能插入新数据”的隔离语义。不能为了省事一刀切,要逐业务评估。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 行锁、间隙锁、临键锁与表锁,这些“锁”到底锁住了什么
我第一次接触InnoDB锁分类时,觉得Record Lock、Gap Lock、Next-Key Lock这几个概念特别绕。后来在排查死锁过程中反复看日志,才真正理解它们不是三个独立功能,而是“为了在不同场景下保证数据不异常”而设计的一套连续方案。
2.1 Record Lock、Gap Lock、Next-Key Lock:从语义到实例
Record Lock是记录锁,锁的是索引记录本身。它是最直观的行级锁,锁住某行后,其他事务不能修改和删除这一行。很多人以为UPDATE就是单纯锁住“那一行”,这个说法在“通过唯一索引等值命中且记录存在”时是对的,因为InnoDB会退化为Record Lock。但在更多查询场景下,锁的不只是目标记录。
举个例子,user表里有id主键,id从1到100,你按id=1做主键等值查询倒是简单。但如果写的是这样一条SQL。
sql复制SELECT * FROM user WHERE age BETWEEN 20 AND 30
