可重复读(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=20:
UPDATE t SET name='李四2' WHERE id=20;→ 阻塞(行锁排他); - 事务2修改id=10/30:
UPDATE 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下当前读操作,以下情况会默认加临键锁(左开右闭),也是解决幻读的核心场景:
- 基于任意索引的范围查询(无论是否命中实际记录);
- 基于普通二级索引的等值查询(即使精准命中记录,因普通索引非唯一,可能存在重复值);
- 无任何索引的当前读(走聚簇索引全表扫描,加全表临键锁,等价于表锁)。
核心特点
同时锁定「间隙+实际记录」,既限制对现有记录的修改,也限制向间隙插入新记录,彻底解决幻读;若为普通索引等值查询,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的最高隔离级别,核心前提:
- 完全禁用MVCC,所有查询(包括普通select)均为当前读,隐式加共享锁(S锁);
- 基于索引的范围锁定,加锁逻辑比RR更严格、更广泛;
- 所有索引类型的等值/范围查询,默认加临键锁,行锁和间隙锁几乎不单独出现(仅为临键锁的组成部分)。
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下所有操作均会加临键锁(左开右闭),无任何例外,包括:
- 所有索引(主键/唯一/普通)的等值/范围查询(无论是否命中记录);
- 普通select(隐式加共享临键锁,这是与RR的核心区别);
- 无索引的任何查询/修改(全表临键锁,等价于表锁,且普通select也会加);
- 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锁互斥 | ✅ | 极低 |
四、实际开发建议
- 优先使用RR隔离级别:兼顾「无幻读」和「高并发」,是InnoDB默认且最实用的选择;
- 必须建立有效索引:无索引会触发全表临键锁(等价于表锁),彻底丧失并发能力;
- 精准查询用主键/唯一索引:尽可能触发行锁,缩小锁范围,提升并发;
- 避免大范围范围查询:范围查询会触发临键锁,锁范围扩大,增加阻塞概率;
- SER仅用于极致严格场景:如金融核心交易的极小并发场景,普通业务禁止使用(性能损耗过大)。

浙公网安备 33010602011771号