InnoDB Next-Key Lock / Gap Lock 测试总结
2026/9/17 19:02:53 网站建设 项目流程
    • 测试环境
    • 一、表结构和测试数据
    • 表结构
      • 测试数据
    • 二、基准加锁 SQL(会话1,全程不提交)
    • 三、核心问题与结论
      • Q1:Next-Key Lock 会不会退化成 Record Lock?
      • Q2:锁的到底是"stock 字段的区间",还是别的?
      • Q3:Gap 的左右边界谁大谁小,是不是按 id 排?
      • Q4:`X` 后面的"记录锁"锁的是间隙里所有记录吗?
      • Q5:二级索引和聚簇索引的锁是不是联动的?
    • 四、实测数据排序(按四元组字典序)
    • 五、加锁范围图解
    • 六、performance_schema.data_locks 实测验证
      • LOCK_MODE 后缀判断规则
      • 二级索引 `idx_category_status_stock` 上的锁
      • 主键 PRIMARY 索引上的锁
      • performance_schema.data_locks 数据
    • 七、验证过的具体场景
      • 关键推论(第 4、5 条的原理)
    • 八、验证过程
      • 验证前操作(会话1: 加锁)
      • 验证 No.1
      • 验证 No.2
      • 验证 No.3
      • 验证 No.4
      • 验证 No.5
      • 验证 No.6
      • 验证 No.7
    • 九、最终结论汇总

测试环境

环境:MySQL 8.4.9 (InnoDB, REPEATABLE-READ)
测试表:product,联合索引idx_category_status_stock (category_id, status, stock)

一、表结构和测试数据

表结构

CREATETABLE`product`(`id`bigintunsignedNOTNULLAUTO_INCREMENTCOMMENT'商品ID',`category_id`bigintunsignedNOTNULLCOMMENT'所属分类ID',`name`varchar(128)NOTNULLCOMMENT'商品名称',`price`decimal(10,2)NOTNULLDEFAULT'0.00'COMMENT'商品单价(元)',`stock`intunsignedNOTNULLDEFAULT'0'COMMENT'库存数量',`description`varchar(500)DEFAULT''COMMENT'商品描述',`status`tinyintNOTNULLDEFAULT'1'COMMENT'状态:1-上架,0-下架',`create_time`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`id`),KEY`idx_category_status_stock`(`category_id`,`status`,`stock`))ENGINE=InnoDBAUTO_INCREMENT=16DEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_0900_ai_ciCOMMENT='商品表';

关键点:idx_category_status_stock非唯一二级索引

测试数据

INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(100,4,'iPhone 15 Pro 256G',7999.00,120,'A17 Pro芯片,钛金属机身',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(200,4,'华为Mate 60 Pro',6999.00,85,'麒麟芯片,卫星通话',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(300,4,'小米14 Ultra',5999.00,200,'徕卡光学,骁龙8 Gen3',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(400,5,'MacBook Pro 14英寸',14999.00,45,'M3 Pro芯片,18GB内存',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(500,5,'联想拯救者Y9000P',8999.00,60,'i9-14900HX,RTX4070',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(600,5,'罗技MX Master 3S鼠标',799.00,300,'静音按键,8000DPI',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(700,6,'优衣库纯棉T恤',99.00,500,'100%纯棉,舒适透气',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(800,6,'李维斯501牛仔裤',599.00,150,'经典直筒,水洗做旧',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(900,6,'耐克Air Jordan 1',1299.00,80,'高帮复古篮球鞋',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(1000,7,'ZARA碎花连衣裙',399.00,220,'雪纺面料,收腰设计',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(1100,7,'优衣库羊毛大衣',899.00,90,'70%羊毛含量,中长款',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(1200,8,'烟台红富士苹果5斤',39.90,1000,'脆甜多汁,新鲜采摘',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(1300,8,'海南金煌芒果5斤',49.90,600,'果肉细腻,核小无丝',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(1400,9,'三只松鼠坚果大礼包',89.00,450,'7种坚果,每日坚果',1,'2026-08-24 14:37:17','2026-08-26 19:27:08');INSERTINTO`product`(`id`,`category_id`,`name`,`price`,`stock`,`description`,`status`,`create_time`,`update_time`)VALUES(1500,9,'可口可乐330ml*24罐',59.90,800,'经典口味,整箱装',0,'2026-08-24 14:37:17','2026-08-26 19:27:08');

二、基准加锁 SQL(会话1,全程不提交)

SELECT*FROMproductWHEREcategory_id=4ANDstatus=1ANDstock>=120FORUPDATE;

三、核心问题与结论

Q1:Next-Key Lock 会不会退化成 Record Lock?

结论:不会。

退化条件必须同时满足

  1. 使用唯一索引(主键 / unique key)
  2. 等值查询且能唯一确定一行

本例中idx_category_status_stock是普通联合索引(非唯一),且stock >= 120是范围条件,两个条件都不满足,因此全程走 Next-Key Lock(Record Lock + Gap Lock)。

补充:即使把stock >= 120换成stock = 120,因为索引本身非唯一(可能存在多行同值),仍然不会退化。

Q2:锁的到底是"stock 字段的区间",还是别的?

结论:锁的是联合索引(category_id, status, stock)在 B+ 树上按字典序排列的物理相邻区间,不是单独 stock 字段的取值区间。

对于非唯一二级索引,InnoDB 会在索引 key 末尾自动追加主键值做唯一标识和排序,真实排序键是四元组:

(category_id, status, stock, id)

Q3:Gap 的左右边界谁大谁小,是不是按 id 排?

结论:不是。排序优先级严格按索引定义顺序,id只是最后的 tie-breaker(决胜属性)。

即:先比category_id,相同再比status,相同再比stock只有前三者都相同时才比较id。id 数值大小本身不影响排序位置,除非前三个字段完全相同。

Q4:X后面的"记录锁"锁的是间隙里所有记录吗?

结论:不是。只锁LOCK_DATA标出的那一条具体记录;间隙本身不含任何行,Gap 锁锁的是这段空白,阻止插入。

Q5:二级索引和聚簇索引的锁是不是联动的?

结论:不是。只有真正满足 WHERE 全部条件、会被返回/修改的行,才会在聚簇索引(PRIMARY)上加锁;仅作为扫描边界经过的行,只在二级索引层加锁,聚簇索引上完全干净。

四、实测数据排序(按四元组字典序)

(4, 1, 85, 200) ← 华为 Mate 60 Pro (4, 1, 120, 100) ← iPhone 15 Pro ← stock>=120 命中起点 (4, 1, 200, 300) ← 小米 14 Ultra (5, 1, 45, 400) ← MacBook Pro 14英寸 ← 扫描终止边界 (5, 1, 60, 500) ← 联想拯救者 Y9000P ← 未被扫描/未锁

五、加锁范围图解

(4,1,85,200) ──gapA── (4,1,120,100) ──gapB── (4,1,200,300) ──gapC── (5,1,45,400) ──未锁── (5,1,60,500) ↑ record+gapA ↑ record+gapB ↑ record+gapC

规则:

  • 扫描命中的每条记录都加Next-Key Lock(记录锁 + 记录前面的间隙锁)
  • 遇到第一条不满足WHERE 条件的记录((5,1,45,400),category_id 已经变成 5)时,仍会锁住这条记录 + 它前面的 gap,然后扫描停止
  • 该记录之后的 gap((5,1,45,400)(5,1,60,500)之间)完全不会被锁

六、performance_schema.data_locks 实测验证

LOCK_MODE 后缀判断规则

LOCK_MODE含义
X(无后缀)Next-Key Lock = 记录锁 + 记录前面的 Gap 锁
X,REC_NOT_GAP纯记录锁,不含 gap,只锁这一行本身
X,GAP纯 Gap 锁,不锁记录本身(本例中没出现)

二级索引idx_category_status_stock上的锁

LOCK_DATALOCK_MODE含义
4, 1, 120, 100XNext-Key Lock:记录 + 前置 gap
4, 1, 200, 300XNext-Key Lock:记录 + 前置 gap
5, 1, 45, 400XNext-Key Lock:记录 + 前置 gap

(4,1,85,200)未出现独立记录 —— 它的记录锁隐含在(4,1,120,100)的"前置 gap"里。
(5,1,60,500)完全没出现 —— 印证扫描确实止步于(5,1,45,400)

主键 PRIMARY 索引上的锁

LOCK_DATALOCK_MODE含义
100X,REC_NOT_GAP纯记录锁,无 gap
300X,REC_NOT_GAP纯记录锁,无 gap

id=400(MacBook Pro)在 PRIMARY 上完全无锁

performance_schema.data_locks 数据

结论:二级索引层——只要扫描到就加锁(用于界定扫描边界、防幻读);聚簇索引层——只有真正完全满足 WHERE 条件、会被返回/修改的行才会回表加锁。LOCK_MODEREC_NOT_GAP后缀就是判断"纯记录锁 vs Next-Key Lock"的直接依据。

七、验证过的具体场景

#场景条件(category_id, status, stock, id)落入的 Gap是否阻塞原因
1Insert(4, 1, 100, 新id)gapA/gapB 区间内✅ 阻塞stock=100 落在 85~120 之间
2Insert(4, 1, 999, 新id)gapC(边界 gap)✅ 阻塞落在(4,1,200,300)(5,1,45,400)之间
3Insert(6, 1, 100, 新id)无关区间❌ 不阻塞category_id=6 完全不在锁定范围内
4Insert(5, 1, 45, 新id),新 id > 400(5,1,45,400)之后的未锁 gap❌ 不阻塞自增 id 必然更大,排在已锁记录之后,落入未扫描区间
5Insert(4, 1, 85, 10000)gapA 内✅ 阻塞前三段与(4,1,85,200)相同,id=10000>200 排其后,但仍在 gapA 与(4,1,120,100)之间
6SELECT FOR UPDATE(,,,400)PRIMARY 索引,未锁❌ 不阻塞该行只在二级索引加锁,聚簇索引干净
7SELECT FOR UPDATE(5, 1, 45,)二级索引,命中记录锁✅ 阻塞超时精确命中(5,1,45,400)的 Record Lock 部分

关键推论(第 4、5 条的原理)

  • 能否被阻塞,看的是新 key 落在哪个 gap,不是看 id 数值本身大小。
  • 自增主键保证新插入行的 id 一定大于表中已有 id,因此在(category_id,status,stock)相同的情况下,新行必然排在已有同值记录之后
  • 如果这个"之后"的位置恰好是已扫描并加锁的 gap 内部(如(4,1,85,10000)),则阻塞;
  • 如果这个"之后"的位置落在扫描已经停止、未被触及的 gap(如(5,1,45,新id)),则不阻塞。

八、验证过程

验证前操作(会话1: 加锁)

验证 No.1

会话2: Insert数据

performance_schema.data_locks 数据

验证 No.2

会话2: Insert数据

performance_schema.data_locks 数据

验证 No.3

会话2: Insert数据

performance_schema.data_locks 数据

验证 No.4

会话2:

performance_schema.data_locks 数据

验证 No.5

会话2:

performance_schema.data_locks 数据

验证 No.6

会话2:

performance_schema.data_locks 数据

补充:id=400 那行只在二级索引 idx_category_status_stock 上被锁(因为它是扫描边界,用于确定 Next-Key Lock 的范围),从未在 PRIMARY 索引上加锁(data_locks 里 PRIMARY 只有 100、300 两条)。窗口2 直接走 PRIMARY 查找,检测不到冲突,加锁成功。

验证 No.7

会话2:

performance_schema.data_locks 数据

补充:这是纯等值查询,在 idx_category_status_stock 上精确定位到的 key 正是 (5,1,45,400)——与窗口1 持有的那条 Next-Key Lock 完全相同,直接命中它的 Record Lock 部分,产生冲突,最终等待超时。

九、最终结论汇总

  1. 非唯一联合索引下,FOR UPDATE走范围查询不会退化为 Record Lock,全程 Next-Key Lock。
  2. 非唯一二级索引的真实排序键是(索引定义字段..., 主键id),id 只是最后的 tie-breaker,不是第一排序依据。
  3. Gap 锁的边界由索引字段的物理排序位置决定,不是 WHERE 条件里某字段的逻辑取值范围。
  4. 扫描到第一条不满足 WHERE 条件的记录后即停止,该记录之后的区域不会被锁——这是"看似应该锁却没锁"的常见原因。
  5. Next-Key Lock 里的"记录锁"只锁LOCK_DATA标出的那一条具体记录,间隙锁只锁两条相邻记录之间的空白,不会波及间隙"里面"的其他行(因为间隙内本就没有行)。
  6. 二级索引和聚簇索引的锁是完全独立的两套锁空间:只有真正满足 WHERE 全部条件的行才会在聚簇索引上加锁;仅作为扫描边界经过的行,只在二级索引层留痕,可以被其他事务通过主键正常访问,不会阻塞。
  7. 判断某次操作是否会被阻塞,本质是判断它访问的索引树索引 key是否与已有锁重叠(记录锁重叠 or 落入某个 gap),不能凭直觉推断。
  8. performance_schema.data_locks是验证以上结论的直接手段:通过INDEX_NAME区分锁在哪棵树上,通过LOCK_MODE有无REC_NOT_GAP后缀判断是纯记录锁还是 Next-Key Lock。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询