可重复读(RR)与可串行化(SER)隔离级别下的行锁/间隙锁/临键锁加锁规则(InnoDB实现)

首先明确InnoDB核心前提:锁均基于索引实现(聚簇索引/二级索引),无索引会触发全表锁;三个基础锁的定义:

  • 行锁(记录锁Record Lock):仅锁定索引上的具体一条记录,不影响记录前后的间隙,是最细粒度的行级锁。
  • 间隙锁(Gap Lock):仅锁定两个索引记录之间的空隙(也包括首条记录前、末条记录后的间隙),不锁定任何实际记录,目的是阻止其他事务向该间隙插入新记录,解决幻读。
  • 临键锁(Next-Key Lock)行锁 + 间隙锁的组合锁,InnoDB默认采用左开右闭 (a,b] 的区间锁定规则,既锁定区间内的实际记录,也锁定记录前的间隙,是解决幻读的核心锁机制。

以下所有说明均基于MySQL InnoDB(主流实现),先介绍可重复读(RR,InnoDB默认隔离级别),再介绍可串行化(SER,最高隔离级别),每个隔离级别下按「行锁→间隙锁→临键锁」的顺序讲解加锁场景+实操举例,统一使用测试表/数据简化理解:

-- 测试表:主键聚簇索引id,后续会新增普通索引/唯一索引
CREATE TABLE t (
  id INT PRIMARY KEY,
  name VARCHAR(20),
  age INT
);
-- 测试数据:主键id为有序离散值(便于体现间隙)
INSERT INTO t VALUES (10, '张三', 20), (20, '李四', 25), (30, '王五', 30);

一、可重复读(RR)隔离级别:MVCC+锁结合,临键锁为默认

RR是InnoDB最常用的隔离级别,通过「MVCC(快照读)+ 行级锁(当前读)」实现,仅当前读(select ... for update/lock in share mode、update、delete)会加锁,快照读(普通select)不加锁;加锁时会根据索引类型(主键/唯一索引/普通索引)和查询方式(精准命中/范围/未命中)区分锁类型,是实际开发中需要重点掌握的规则。

1. 行锁(Record Lock):仅精准命中「主键/唯一二级索引」时加锁

加锁场景

当前读操作,通过主键或唯一二级索引 精准命中一条实际记录(无范围、无模糊匹配),此时仅锁定该索引上的具体记录,不锁定任何间隙,是RR下最细粒度的加锁方式。

核心特点

唯一索引(主键是特殊的唯一索引)的唯一性决定了查询结果唯一,无需锁定间隙,避免锁范围扩大。

举例说明

-- 事务1:当前读,主键精准命中id=20的记录
BEGIN;
SELECT * FROM t WHERE id=20 FOR UPDATE; -- 仅加行锁(锁定id=20这条记录)
  • 事务2修改id=20UPDATE t SET name='李四2' WHERE id=20; → 阻塞(行锁排他);
  • 事务2修改id=10/30UPDATE t SET name='张三2' WHERE id=10; → 正常执行(未锁其他记录);
  • 事务2向间隙插入数据INSERT INTO t VALUES (15, '赵六', 22); → 正常执行(未锁10-20的间隙);
  • 事务2查询id=20:普通select(快照读)→ 正常读快照,不阻塞。

2. 间隙锁(Gap Lock):范围/等值查询未命中任何记录时加锁

加锁场景

当前读操作,基于任意索引(主键/唯一/普通)进行范围查询等值查询,但未命中任何实际记录,此时仅锁定查询条件对应的索引间隙不锁定任何实际记录,核心目的是阻止其他事务向该间隙插入新记录,避免幻读。

核心特点

无实际记录可锁,仅锁定「可能插入新记录的间隙」,仅限制插入操作,不限制对现有记录的修改/查询。

举例说明

-- 事务1:当前读,主键等值查询id=15(无该记录,未命中)
BEGIN;
SELECT * FROM t WHERE id=15 FOR UPDATE; -- 仅加间隙锁,锁定间隙 (10,20)
  • 事务2向该间隙插入数据INSERT INTO t VALUES (12, '孙七', 21)INSERT INTO t VALUES (18, '周八', 23) → 均阻塞(间隙锁阻止插入);
  • 事务2修改现有记录UPDATE t SET name='张三2' WHERE id=10; → 正常执行(未锁现有记录);
  • 事务2向其他间隙插入INSERT INTO t VALUES (25, '吴九', 26) → 正常执行(仅锁10-20的间隙)。

3. 临键锁(Next-Key Lock):RR的默认加锁方式,覆盖多数当前读场景

加锁场景

RR下当前读操作,以下情况会默认加临键锁(左开右闭),也是解决幻读的核心场景:

  1. 基于任意索引范围查询(无论是否命中实际记录);
  2. 基于普通二级索引等值查询(即使精准命中记录,因普通索引非唯一,可能存在重复值);
  3. 无任何索引的当前读(走聚簇索引全表扫描,加全表临键锁,等价于表锁)。

核心特点

同时锁定「间隙+实际记录」,既限制对现有记录的修改,也限制向间隙插入新记录,彻底解决幻读;若为普通索引等值查询,InnoDB会在事务提交/释放锁前,对唯一的聚簇索引记录临键锁降级(仅保留行锁),但间隙锁仍保留。

举例说明(分3种核心场景)

场景1:主键范围查询(命中部分记录)
-- 事务1:当前读,主键范围查询10<id<30(命中id=20)
BEGIN;
SELECT * FROM t WHERE id>10 AND id<30 FOR UPDATE; -- 加2个临键锁:(10,20]、(20,30]
  • 阻塞操作:修改id=20(行锁)、插入10-20间隙(15)、插入20-30间隙(25);
  • 正常操作:修改id=10/30、插入id=5(首条前间隙)、插入id=35(末条后间隙)。
场景2:普通索引等值查询(精准命中,非唯一)
-- 新增普通索引:便于测试
ALTER TABLE t ADD INDEX idx_age (age);
-- 新增测试数据:让age存在重复值(普通索引非唯一)
INSERT INTO t VALUES (40, '郑十', 25);
-- 事务1:当前读,普通索引age等值查询(命中id=20/40,age=25)
BEGIN;
SELECT * FROM t WHERE age=25 FOR UPDATE; -- 加临键锁:(20,25](左开右闭,基于age索引)
  • 阻塞操作:插入age=22(20-25间隙)、修改id=20/40(行锁);
  • 正常操作:修改id=10/30、插入age=28(25-30间隙);
  • 特殊点:事务1提交后,age索引的临键锁会降级,仅保留id=20/40的行锁,间隙锁释放。
场景3:无索引的当前读(全表临键锁,等价于表锁)
-- 先删除age索引,让name无任何索引
ALTER TABLE t DROP INDEX idx_age;
-- 事务1:当前读,基于无索引的name查询
BEGIN;
UPDATE t SET age=26 WHERE name='李四'; -- 无索引,全表扫描,加全表临键锁
  • 阻塞操作:其他事务对t表的所有增删改操作(如插入、修改任意id、删除记录);
  • 正常操作:仅普通select(快照读),无其他正常操作(等价于表锁)。

二、可串行化(SER)隔离级别:禁用MVCC,全量加锁,临键锁为核心

SER是InnoDB的最高隔离级别,核心前提

  1. 完全禁用MVCC,所有查询(包括普通select)均为当前读,隐式加共享锁(S锁)
  2. 基于索引的范围锁定,加锁逻辑比RR更严格、更广泛
  3. 所有索引类型的等值/范围查询,默认加临键锁,行锁和间隙锁几乎不单独出现(仅为临键锁的组成部分)。

SER下的加锁规则是RR的「超集」,且无临键锁降级,锁范围不会缩小,实际开发中极少使用(性能极低,仅适用于严格无幻读、并发量极低的场景)。

1. 行锁(Record Lock):仅特殊显式指定时加锁(几乎为特例)

加锁场景

SER下唯一能单独加行锁的情况:通过主键/唯一二级索引 精准命中一条记录,且显式指定仅锁定该索引列(通过for update of 表名.索引列限制锁范围),是开发中几乎不会用到的特例。

举例说明

-- 事务1:显式指定仅锁主键列,精准命中id=20
BEGIN;
SELECT * FROM t WHERE id=20 FOR UPDATE OF t.id; -- 仅加行锁,锁定id=20记录
  • 阻塞操作:其他事务修改id=20;
  • 正常操作:插入10-20间隙的记录、修改id=10/30(与RR下行锁规则一致)。

2. 间隙锁(Gap Lock):几乎不会单独加锁(可忽略)

加锁场景

SER下不存在单独的间隙锁场景,仅当临键锁被手动释放(如事务提交、解锁)时,间隙锁可能短暂单独存在,实际开发中可完全忽略,所有间隙锁定均为临键锁的一部分。

3. 临键锁(Next-Key Lock):SER的核心加锁方式,覆盖所有场景

加锁场景

SER下所有操作均会加临键锁(左开右闭),无任何例外,包括:

  1. 所有索引(主键/唯一/普通)的等值/范围查询(无论是否命中记录);
  2. 普通select(隐式加共享临键锁,这是与RR的核心区别);
  3. 无索引的任何查询/修改(全表临键锁,等价于表锁,且普通select也会加);
  4. insert/update/delete等写操作(加排他临键锁)。

核心特点

  • 共享临键锁(S)与排他临键锁(X)互斥:普通select加S锁后,其他事务的写操作(X锁)会被阻塞;
  • 无锁降级:无论何种索引,加锁后范围永不缩小,直至事务提交;
  • 彻底解决幻读:但并发性能极低,因为所有操作都要加锁,且锁范围大。

举例说明(3个核心场景,体现与RR的差异)

场景1:普通select(无显式锁)→ 隐式加共享临键锁(RR下不加锁)
-- 事务1:普通select,范围查询(SER下隐式加共享临键锁)
BEGIN;
SELECT * FROM t WHERE id>10 AND id<30; -- 加共享临键锁:(10,20]、(20,30]
  • 事务2写操作阻塞UPDATE t SET name='李四2' WHERE id=20;INSERT INTO t VALUES (15, '赵六', 22); → 均阻塞(S锁与X锁互斥);
  • 事务2读操作正常:普通select(SER下也加S锁,S锁之间兼容)→ 正常执行。
场景2:主键等值查询(无显式指定锁列)→ 加临键锁(RR下加行锁)
-- 事务1:主键等值查询,无显式锁列限制
BEGIN;
SELECT * FROM t WHERE id=20 FOR UPDATE; -- SER下加临键锁 (10,20](RR下仅加行锁)
  • 事务2插入间隙阻塞INSERT INTO t VALUES (15, '赵六', 22); → 阻塞(临键锁包含间隙);
  • 事务2修改id=20阻塞:与RR一致;
  • 关键差异:RR下该场景仅锁记录,SER下同时锁记录+前间隙。
场景3:无索引普通select → 全表共享临键锁(RR下快照读不加锁)
-- 事务1:无索引列name的普通select
BEGIN;
SELECT * FROM t WHERE name='李四'; -- 无索引,全表加共享临键锁
  • 事务2所有写操作阻塞:插入、修改、删除任意记录 → 均阻塞;
  • 事务2读操作正常:普通select(加共享临键锁,S锁兼容);
  • 关键差异:RR下该场景为快照读,无任何锁;SER下即使是无索引的普通查询,也会加全表锁。

三、RR与SER隔离级别加锁规则核心对比

隔离级别 锁类型 核心加锁场景 关键特点 幻读解决 并发性能
可重复读 行锁 主键/唯一索引精准命中当前读 仅锁记录,无间隙锁定 ✅(MVCC+锁)
(RR) 间隙锁 任意索引范围/等值查询未命中当前读 仅锁间隙,阻止插入
临键锁 任意索引范围查询、普通索引等值查询、无索引当前读 行锁+间隙锁,默认左开右闭,支持锁降级 中-高
可串行化 行锁 主键/唯一索引精准命中,且显式指定锁列(特例) 几乎不单独使用,仅为特殊场景 ✅(全量加锁) 极低
(SER) 间隙锁 临键锁的组成部分,无单独加锁场景(可忽略) 仅随临键锁存在 极低
临键锁 所有查询(含普通select)、所有增删改、所有索引/无索引场景 覆盖所有操作,隐式加锁,无锁降级,S/X锁互斥 极低

四、实际开发建议

  1. 优先使用RR隔离级别:兼顾「无幻读」和「高并发」,是InnoDB默认且最实用的选择;
  2. 必须建立有效索引:无索引会触发全表临键锁(等价于表锁),彻底丧失并发能力;
  3. 精准查询用主键/唯一索引:尽可能触发行锁,缩小锁范围,提升并发;
  4. 避免大范围范围查询:范围查询会触发临键锁,锁范围扩大,增加阻塞概率;
  5. SER仅用于极致严格场景:如金融核心交易的极小并发场景,普通业务禁止使用(性能损耗过大)。
posted @ 2026-01-23 17:08  先弓  阅读(5)  评论(0)    收藏  举报