当前位置: 首页 > news >正文

MySQL InnoDB 锁机制深度解析:从共享锁到 Next-Key Lock,彻底搞懂并发控制

MySQL InnoDB 锁机制深度解析:从共享锁到 Next-Key Lock,彻底搞懂并发控制

MySQL InnoDB 锁机制深度解析:从共享锁到 Next-Key Lock,彻底搞懂并发控制

在数据库高并发场景下,锁是保证数据一致性的基石。但 MySQL InnoDB 的锁机制常常让开发者感到困惑:什么是间隙锁?Next-Key Lock 又是什么?为什么同样的 SQL,有时锁行,有时锁表?本文从基础到进阶,结合图解和实战案例,带你彻底吃透 InnoDB 的锁体系。

一、为什么需要锁?

数据库中的锁,本质上是一种并发控制机制。当多个事务同时读写同一份数据时,可能会出现脏读、不可重复读、幻读、更新丢失等问题。为了在数据一致性并发性能之间取得平衡,InnoDB 设计了精细的锁系统。

锁的核心目标:

  • 保证事务的隔离性

  • 尽可能提高并发度

  • 避免死锁和锁竞争

 

二、锁的基本分类:共享锁与排他锁

InnoDB 实现了标准的行级锁,分为两种类型:

 
锁类型别名作用兼容性
共享锁(S) 读锁 允许事务读取一行数据,阻止其他事务对该行加排他锁(但允许加共享锁) 共享锁之间兼容,与排他锁互斥
排他锁(X) 写锁 允许事务修改一行数据,阻止其他事务对该行加任何锁(共享锁或排他锁) 与所有锁互斥

 

加锁语法

  • 加共享锁:SELECT ... LOCK IN SHARE MODE;

  • 加排他锁:SELECT ... FOR UPDATE; (UPDATE、DELETE、INSERT 语句会自动加排他锁)

-- 加共享锁(读锁)
SELECT ... LOCK IN SHARE MODE;-- 加排他锁(写锁)
SELECT ... FOR UPDATE;-- UPDATE、DELETE、INSERT 自动加排他锁

示例说明

-- 事务 A
BEGIN;
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
-- 此时事务 A 持有 id=1 行的共享锁-- 事务 B(允许执行,因为共享锁兼容)
BEGIN;
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE; -- 立即成功-- 事务 C(被阻塞,因为排他锁需要等待共享锁释放)
BEGIN;
UPDATE user SET name = 'new' WHERE id = 1; -- 阻塞,等待事务 A 或 B 提交

 

三、意向锁(Intention Locks)

意向锁是表级别的锁,由 InnoDB 自动管理,不需要用户干预。它的作用是为了快速判断表中是否存在行锁,避免逐行检查。

  • 意向共享锁(IS):事务准备给某些行加共享锁时,先在表上加 IS 锁。

  • 意向排他锁(IX):事务准备给某些行加排他锁时,先在表上加 IX 锁。

加锁流程:

  • 加行共享锁 → 先加表意向共享锁(IS)

  • 加行排他锁 → 先加表意向排他锁(IX)

作用示例:另一个事务想要对整个表加排他锁(LOCK TABLES ... WRITE)时,可以直接看到表上有 IX 锁,从而立即阻塞,而不需要遍历每一行检查是否有行锁。

 

四、InnoDB 的三种行锁算法

nnoDB 的行锁不仅仅锁定单个记录,还包括间隙和区间。根据锁定范围的不同,分为三种算法:

 
锁算法锁定范围是否锁定记录是否锁定间隙主要场景
Record Lock(记录锁) 单条索引记录 等值查询唯一索引/主键
Gap Lock(间隙锁) 两个索引记录之间的间隙 防止幻读,锁住不存在的区间
Next-Key Lock(临键锁) 间隙 + 记录(左开右闭区间) RR 默认算法,范围查询时使用

4.1 记录锁(Record Lock)

  • 作用:锁定索引记录本身。

  • 场景:等值查询且索引唯一(如主键、唯一索引),或者简单的行更新。

  • 效果:阻止其他事务插入、更新、删除被锁定的同一行。

记录锁锁定的是索引记录。如果表没有索引,InnoDB 会使用隐藏的聚簇索引(row_id)来锁定。

-- id 是主键或唯一索引,加记录锁
SELECT * FROM user WHERE id = 10 FOR UPDATE;

  

image

 

4.2 间隙锁(Gap Lock)

  • 作用:锁定索引记录之间的间隙(不包含记录本身)。

  • 场景:范围查询、非唯一索引等值查询(RR 隔离级别下)。

  • 效果:防止其他事务在间隙中插入新记录,从而避免幻读。

间隙锁锁定索引记录之间的空隙(不包含任何实际记录)。它只在 RR 隔离级别下生效,目的是防止其他事务在间隙中插入新数据,从而避免幻读。

-- 假设 user 表的 id 列有值:1, 3, 5, 8
SELECT * FROM user WHERE id BETWEEN 3 AND 5 FOR UPDATE;
-- 会锁定 (3,5) 这个间隙,阻止插入 id=4 的记录

注意:间隙锁只在 Repeatable Read 及以上隔离级别存在;Read Committed 级别没有间隙锁。

间隙锁的特点:

  • 间隙锁之间不互斥。多个事务可以同时锁定同一个间隙(例如都是 SELECT ... FOR UPDATE 且范围相同),因为间隙锁的目标是“禁止插入”,而锁本身不保护数据内容。

  • 间隙锁只存在于 RR 级别,RC 级别下没有间隙锁。

 

4.3 临键锁(Next-Key Lock)

  • 作用:记录锁 + 间隙锁 的组合,既锁定记录,也锁定记录前面的间隙。

  • 范围:(上一个值, 当前值]

  • 效果:InnoDB 在 RR 级别下执行范围查询时,默认使用 Next-Key Lock,这是解决幻读的核心手段。

Next-Key Lock = Record Lock + Gap Lock,即锁定一个左开右闭的区间 (上一个值, 当前值]

这是 InnoDB RR 级别下默认的行锁算法。当一个 SQL 使用非唯一索引或范围条件时,就会触发 Next-Key Lock。

-- age 是普通索引
SELECT * FROM user WHERE age = 18 FOR UPDATE;

假设 age 索引上的值为:16, 18, 20。那么 Next-Key Lock 会锁定区间 (16, 18] 以及 (18, 20)(实际上 Next-Key 是左开右闭,但通常描述为包含记录和左侧间隙)。更准确地说,对 age=18 这条记录,会锁住 (16, 18] 这个区间,并同时锁住记录 18 本身。

下图展示了间隙锁与临键锁的范围区别:

image

 

为什么需要 Next-Key Lock?
因为只锁记录(Record Lock)无法防止幻读:如果其他事务在间隙中插入一条 age=19 的记录,当前事务再次查询时就会多出行。Next-Key Lock 锁住了间隙,就阻止了这种插入。

 

五、RR 隔离级别下的加锁策略(核心重点)

在 RR 隔离级别下,InnoDB 会根据查询条件和索引类型动态选择锁的类型。以下是最常见的几种情况:

 
场景锁类型说明
主键/唯一索引等值查询(记录存在) Record Lock 只锁住该行
主键/唯一索引等值查询(记录不存在) Gap Lock 锁住记录应该出现的间隙,防止幻读
非唯一索引等值查询 Next-Key Lock + 聚簇索引 Record Lock 锁住索引区间 + 对应主键行
范围查询(任何索引) Next-Key Lock(直至边界或无穷) 锁住范围内所有间隙和记录
无索引的条件(全表扫描) 对所有聚簇索引记录加 Record Lock + 所有间隙加 Gap Lock 实际上等于锁住了整张表(但不完全是表锁),并发性极差
 

图解:不同查询的锁范围

示例表(id 主键,age 普通索引):

 
idage
1 10
2 18
3 20
4 30

① 主键等值查询(存在)

SELECT * FROM user WHERE id = 2 FOR UPDATE;

锁:只在 id=2 上加 Record Lock。

 

image

 

② 非唯一索引等值查询

SELECT * FROM user WHERE age = 18 FOR UPDATE;

锁:age 索引上 Next-Key Lock 锁定 (10,18],同时主键 id=2 加 Record Lock。

image

 

③ 范围查询

SELECT * FROM user WHERE age > 18 FOR UPDATE;

锁:age 索引上从 20 开始的所有记录及之后的间隙(直到正无穷),都加上 Next-Key Lock。

image

六、插入意向锁(Insert Intention Lock)

插入意向锁是一种特殊的间隙锁,由 INSERT 操作在插入前获取。它表示:“我准备在这个间隙中插入一条记录”。

  • 插入意向锁与间隙锁互斥:如果一个事务已经持有了某个间隙的间隙锁(比如 SELECT ... FOR UPDATE 的范围查询),那么另一个事务试图在这个间隙中插入记录时,会被阻塞。

  • 插入意向锁之间不互斥:多个事务可以在同一个间隙中插入不同的位置(前提是不冲突,例如主键不同),因为它们只是表示“想插入”,而不是真正锁住整个间隙。

这正是上一篇「INSERT 和 UPDATE 并发问题」的根本原因:UPDATE 持有的 Gap Lock 会阻塞 INSERT 的插入意向锁。

 

自增锁(Auto-inc Lock)

自增锁是一种特殊的表锁,用于保证 AUTO_INCREMENT 列的值连续且不重复。它只会在插入数据时短暂持有,插入完成后立即释放(即使事务未提交)。

七、死锁的成因与避免

死锁发生的四个必要条件

  1. 互斥:资源只能被一个事务持有。

  2. 持有并等待:事务已持有锁,又去申请其他锁。

  3. 不可剥夺:不能强行剥夺已持有的锁。

  4. 循环等待:事务之间形成锁等待环路。

典型死锁场景

 
时间事务 A事务 B
t1 BEGIN; UPDATE user SET name='a' WHERE id=1;(持有 id=1 锁)  
t2   BEGIN; UPDATE user SET name='b' WHERE id=2;(持有 id=2 锁)
t3 UPDATE user SET name='aa' WHERE id=2;(等待事务 B 释放 id=2)  
t4   UPDATE user SET name='bb' WHERE id=1;(等待事务 A 释放 id=1,形成死锁)
 
InnoDB 会检测到死锁,并回滚其中一个事务(通常是执行代价较小的那个)。

如何避免死锁?

  • 多表操作按固定顺序访问

  • 避免大事务,将长事务拆分为多个短事务

  • 为查询条件建立索引,避免全表扫描导致锁升级

  • 合理使用隔离级别:如果业务允许,将 RR 降级为 RC 可减少间隙锁,从而降低死锁概率

  • 使用等值查询而非范围查询,减少锁范围

八、实战:查看锁信息的常用命令

-- 查看当前所有事务
SELECT * FROM information_schema.INNODB_TRX;-- 查看当前锁信息(MySQL 8.0 使用 performance_schema)
SELECT * FROM performance_schema.data_locks;-- 查看锁等待关系
SELECT * FROM sys.innodb_lock_waits;-- 查看 InnoDB 状态(包含死锁日志)
SHOW ENGINE INNODB STATUS;-- 查看锁等待超时设置(默认 50 秒)
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';

 

九、总结:一张图看懂 InnoDB 锁体系

image

 

核心结论

  • InnoDB 的行锁实际上是锁在索引上的。没有索引时,会使用聚簇索引全表扫描,导致锁范围扩大。

  • RR 级别下,InnoDB 通过 Next-Key Lock + MVCC 完美解决了幻读。

  • 间隙锁是好东西,但它会降低并发性,尤其在热点数据插入时容易成为瓶颈。

  • 死锁不可怕,可怕的是不会分析。定期查看 SHOW ENGINE INNODB STATUS,优化 SQL 和事务顺序。

掌握 InnoDB 的锁机制,是编写高性能、高并发数据库应用的关键。希望本文能帮助你从原理到实战,彻底弄懂 MySQL 的锁世界。

如果觉得本文对你有帮助,欢迎点赞、收藏、转发!
关注我,获取更多数据库底层原理与性能优化干货。