在日常开发中,你是否遇到过这样的“灵异事件”:明明只更新了一行数据,却发现其他完全不相关的插入、更新操作也被阻塞了?甚至引发了死锁?
当你看着监控面板上飙升的InnoDB_row_lock_waits指标,心里或许在咆哮:“我只是想改个状态,为什么要锁住整个表?”
别急,这并不是MySQL的Bug,而是其底层锁机制,特别是Next-Key Lock在“可重复读”(RR)隔离级别下的正常表现。理解了这套机制,你就能像侦探一样,从一堆阻塞的线程中,精准定位出真正的“元凶”。
一、MySQL的锁到底锁住了什么?
在InnoDB存储引擎中,行锁并不是简单地锁住一行,而是锁住索引上的某个范围。为了彻底解决“幻读”问题,InnoDB将行锁(Record Lock)和间隙锁(Gap Lock)结合,形成了Next-Key Lock。
核心公式:Next-Key Lock=Record Lock(锁住记录本身)+Gap Lock(锁住记录之前的间隙)

这意味着,当你通过某个索引条件去更新数据时,MySQL不仅会锁住满足条件的行,还会锁住该索引值“之前”的一段范围,防止其他事务在这个范围内插入新数据。
我们来看一个经典的加锁规则演示。假设我们有一张表t,主键为id,并有如下数据:
id(主键) | c(普通索引) | d |
|---|---|---|
0 | 0 | 0 |
5 | 5 | 5 |
10 | 10 | 10 |
15 | 15 | 15 |
20 | 20 | 20 |
25 | 25 | 25 |
场景A:主键等值查询(理想情况)
-- Session A
begin;
update t set d = d + 1 where id = 10;
由于id是唯一主键,MySQL只需要精准地锁住id=10这一行。此时,其他事务插入id=8或更新id=15都不会被阻塞。这是最高效的加锁模式。
场景B:非唯一索引等值查询(范围扩大)
-- Session A
begin;
select * from t where c = 5 lock in share mode;
这里c是普通索引。为了防止幻读,MySQL不仅会锁住c=5这条记录,还会锁住它前面的间隙。根据加锁规则,实际锁住的范围是(0, 5]和(5, 10]。这意味着,其他事务如果想插入c=7(位于(5,10)间隙内)的操作会被直接阻塞。
二、案例实战:那些年我们踩过的“坑”
很多时候,业务代码看似简单,但在数据库层面却触发了复杂的锁行为。以下是几个典型的“坑”:
1. “无中生有”的范围锁(唯一索引范围查询 Bug)
-- Session A
begin;
select * from t where id > 10 and id <= 15 for update;MySQL5.7及更早版本,会扫描到第一个不满足条件的值,额外锁住id=20的范围。

这本应是一个范围明确的查询。但在MySQL5.7及之前的版本中,InnoDB的实现存在一个“Bug”(或设计特性):它会向后扫描到第一个不满足条件的值为止。因此,除了(10,15],它还会锁住(15, 20]。如果你尝试更新id=20,会惊讶地发现被阻塞了。虽然MySQL8.0.18修复了此问题,但在旧版本中,这却是导致死锁的元凶。
2. Limit语句的“副作用”
-- Session A
begin;
delete from t where c = 10 limit 2;加上limit似乎让操作更安全了,但对锁的影响却是巨大的。如果找到前两条满足c=10的记录后,MySQL就会停止扫描。这导致它只锁住到这两条记录为止的范围,而不会继续向后锁住更大的范围。这在某些场景下能有效减少锁冲突,但也可能导致删除不彻底(如果还有符合条件的记录在后面)。

3. 死锁的“罗生门”
Session A | Session B |
|---|---|
begin; | begin; |
select * from t where c = 10 lock in share mode; | |
update t set d=d+1 where c = 10; --等待A释放S锁 | |
insert into t values(8,8,8); --等待B释放间隙锁,报错:Deadlock! |

这是一个经典的死锁场景。Session A持有S锁并等待插入(需要Gap Lock),而Session B持有Gap Lock并等待更新(需要X锁)。双方互相等待,最终MySQL会选择回滚其中一个事务。
三、 如何优雅地“解锁”?
面对复杂的锁机制,作为开发者,我们并非束手无策。以下是一些经过验证的优化建议:
1. 善用Limit进行分批操作
在执行大事务的删除或更新时,务必使用LIMIT。这不仅能减少单次事务持有的锁数量,降低主从延迟,还能有效避免锁住过大的范围。
2. 调整事务隔离级别
如果你的业务场景对“幻读”不敏感,或者可以通过应用层逻辑来保证一致性,可以考虑将隔离级别调整为读已提交(RC)。在RC级别下,InnoDB不会使用Gap Lock,从而彻底避免间隙锁带来的阻塞问题,极大提高并发插入效率。
3. 监控与诊断
当遇到锁等待时,不要盲目猜测。利用SHOW ENGINE INNODB STATUS命令查看最近的死锁信息,或者查询information_schema下的INNODB_TRX、INNODB_LOCKS(MySQL5.7及之前)等表,精准定位是哪个事务持有了什么锁。
4. 索引设计的艺术
尽量让查询走唯一索引。唯一索引的等值查询只会加行锁,而普通索引的查询往往会伴随着间隙锁,锁住一个范围。
四、结语
数据库的锁机制是一把双刃剑,它保证了数据的一致性,但也可能成为性能的瓶颈。理解MySQL“只改一行却锁一片”的背后逻辑,能让你在面对慢SQL和死锁报警时,多一份从容,少一份慌张。