Jw
首页
技术
英语
首页
技术
英语
  • MySQL

    • MySQL 基础
    • 环境变量配置(PATH)
    • SQL 语法与分类
    • MySQL 数据类型
    • SQL - DDL 数据定义语言
    • SQL - DML 数据操作语言
    • SQL - DQL 数据查询语言
    • SQL - DCL 数据控制语言
    • MySQL 函数
    • SQL 约束
    • SQL 外键约束
    • 多表关系
    • 多表查询(连接查询)
    • 隐式内连接 vs 显式内连接
    • 子查询
    • 多表查询案例
    • 事务
    • 并发事务问题与隔离级别
    • MySQL 体系结构与存储引擎
    • 索引概述
    • 索引结构
    • 索引分类
    • 索引语法
    • SQL 性能分析
    • 索引使用
    • 索引设计原则
    • SQL 优化 - 插入数据与主键优化
    • SQL 优化 - order by 与 group by
    • SQL 优化 - limit / count / update
    • 视图
    • 存储过程
    • 存储过程 - 变量
    • 存储过程 - 流程控制、游标与条件处理
    • 存储函数
    • 触发器
    • 锁概述与全局锁
    • 表级锁
    • 行级锁
    • InnoDB 逻辑存储结构
    • InnoDB 架构
    • 事务原理(redo / undo log)
    • MVCC
    • MySQL 系统数据库
    • MySQL 常用工具
    • 数据备份与导入导出
    • 错误日志与二进制日志
    • 查询日志与慢查询日志
    • 主从复制 - 概述与原理
    • 主从复制 - 搭建
    • Mycat 配置
    • Mycat 垂直拆分与水平拆分
    • Mycat 分片规则
    • Mycat 管理与监控
    • Mycat 读写分离 - 一主一从
    • Mycat 读写分离 - 双主双从

行级锁

行级锁每次操作锁住对应的行数据:粒度最小、冲突概率最低、并发度最高,仅 InnoDB 支持。分行锁(Record Lock)、间隙锁(Gap Lock)、**临键锁(Next-Key Lock)**三类。

一、介绍

  • 行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在 InnoDB 存储引擎中。
  • InnoDB 的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。
#类型说明隔离级别
1行锁(Record Lock)锁定单个行记录,防止其他事务对此行进行 UPDATE 和 DELETERC、RR 都支持
2间隙锁(Gap Lock)锁定索引记录间隙(不含该记录),确保间隙不变,防止其他事务在该间隙 INSERT 产生幻读RR 支持
3临键锁(Next-Key Lock)行锁 + 间隙锁组合,同时锁住数据及数据前面的间隙 GapRR 支持

演示用表:

CREATE TABLE stu(
    id   INT PRIMARY KEY,
    name VARCHAR(20),
    age  INT
);

INSERT INTO stu VALUES (1,'tom',1), (3,'PHP',3), (8,'rose',8),
                       (11,'jetty',11), (19,'lily',19), (25,'luci',25);

二、行锁

InnoDB 实现了以下两种类型的行锁:

  1. 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
  2. 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。

兼容性矩阵

当前锁 \ 请求锁S(共享锁)X(排他锁)
S(共享锁)兼容冲突
X(排他锁)冲突冲突

SQL 与行锁类型

SQL行锁类型说明
INSERT ...排他锁自动加锁
UPDATE ...排他锁自动加锁
DELETE ...排他锁自动加锁
SELECT(正常)不加任何锁
SELECT ... LOCK IN SHARE MODE共享锁需要手动在 SELECT 之后加
SELECT ... FOR UPDATE排他锁需要手动在 SELECT 之后加

MySQL 8.0 起 LOCK IN SHARE MODE 已由 FOR SHARE 取代,旧写法仍可识别但会告警。

演示要点:默认情况下,InnoDB 在 REPEATABLE READ 事务隔离级别运行,使用 next-key 锁进行搜索和索引扫描,以防止幻读。

  1. 针对唯一索引检索时,对已存在的记录进行等值匹配,会自动优化为行锁;
  2. InnoDB 的行锁是针对索引加的锁,不通过索引条件检索数据时,InnoDB 将对表中的所有记录加锁,此时就会升级为表锁。

2.1 实测:普通 SELECT 不加锁

-- 会话 A
BEGIN;
SELECT * FROM stu WHERE id = 1;            -- 锁粒度:不加锁
SELECT ... FROM performance_schema.data_locks;
Empty set (0.00 sec)

2.2 实测:共享行锁

-- 会话 A
BEGIN;
SELECT * FROM stu WHERE id = 1 LOCK IN SHARE MODE;   -- 锁粒度:行锁(主键等值命中)
+---------------+-------------+------------+-----------+---------------+-----------+
| object_schema | object_name | index_name | lock_type | lock_mode     | lock_data |
+---------------+-------------+------------+-----------+---------------+-----------+
| itcast        | stu         | NULL       | TABLE     | IS            | NULL      |
| itcast        | stu         | PRIMARY    | RECORD    | S,REC_NOT_GAP | 1         |
+---------------+-------------+------------+-----------+---------------+-----------+

两个会话同时执行该语句时,data_locks 会同时出现两组 IS + S,REC_NOT_GAP —— 共享锁之间兼容。

2.3 实测:排他行锁

-- 会话 A
BEGIN;
UPDATE stu SET name = 'Java' WHERE id = 1;   -- Query OK, 1 row affected

-- 会话 B:更新同一行 → 阻塞
BEGIN;
UPDATE stu SET name = 'Java' WHERE id = 1;   -- 等会话 A 提交后才执行

WHERE id = 1 走主键等值匹配 → 只锁这一行(X,REC_NOT_GAP),其他行不受影响。

2.4 实测:无索引条件 → 升级为表锁

-- 会话 A:name 列上还没有索引
BEGIN;
UPDATE stu SET name = 'Lei' WHERE name = 'lily';   -- Query OK, 1 row affected

-- 会话 B:按主键更新另一行,却同样被阻塞
BEGIN;
UPDATE stu SET name = 'PHP' WHERE id = 3;
-- Query OK, 1 row affected (26.82 sec) —— 一直等到会话 A 提交
-- 给 name 建索引后重做同样操作
CREATE INDEX idx_stu_name ON stu(name);
-- 会话 A:BEGIN; UPDATE stu SET name = 'Lily' WHERE name = 'Lei';
-- 会话 B:BEGIN; UPDATE stu SET name = 'PHP' WHERE id = 3;   -- 立即成功,不再阻塞

name 上没有索引时,InnoDB 无法用索引定位目标行,只能全表扫描并对扫描到的每条记录加锁 —— 效果等同锁住整张表。这就是「UPDATE 的 WHERE 不走索引 ⇒ 行锁升级为表锁」的原因。

三、间隙锁与临键锁

演示要点(均针对 RR 隔离级别):

#场景加锁结果
1索引上的等值查询(唯一索引),给不存在的记录加锁优化为间隙锁
2索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求next-key lock 退化为间隙锁
3索引上的范围查询(唯一索引)会访问到不满足条件的第一个值为止

间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。

3.1 唯一索引等值查询不存在 → 间隙锁

表内主键:1, 3, 8, 11, 19, 25

-- 会话 A
BEGIN;
UPDATE stu SET age = 10 WHERE id = 5;      -- Query OK, 0 rows affected(id = 5 不存在)
+------------+-----------+-----------+-----------+
| index_name | lock_type | lock_mode | lock_data |
+------------+-----------+-----------+-----------+
| NULL       | TABLE     | IX        | NULL      |
| PRIMARY    | RECORD    | X,GAP     | 8         |
+------------+-----------+-----------+-----------+

没有命中记录,于是锁住 3 ~ 8 之间的间隙(lock_data 指向间隙右侧的记录 8),防止别的事务插入 id = 5 这类记录。

3.2 唯一索引范围查询 → 访问到不满足条件的第一个值

-- 会话 A
BEGIN;
SELECT * FROM stu WHERE id >= 19 LOCK IN SHARE MODE;
+------------+-----------+---------------+------------------------+
| index_name | lock_type | lock_mode     | lock_data              |
+------------+-----------+---------------+------------------------+
| NULL       | TABLE     | IS            | NULL                   |
| PRIMARY    | RECORD    | S,REC_NOT_GAP | 19                     |
| PRIMARY    | RECORD    | S             | supremum pseudo-record |
| PRIMARY    | RECORD    | S             | 25                     |
+------------+-----------+---------------+------------------------+

>= 19 会一直向右扫描到最后一个记录之后的伪记录 supremum pseudo-record,因此 25 之后的间隙也被临键锁锁住。

3.3 普通索引等值查询 → next-key lock 退化为间隙锁

CREATE INDEX idx_stu_age ON stu(age);

-- 会话 A
BEGIN;
SELECT * FROM stu WHERE age = 3 LOCK IN SHARE MODE;
+-------------+-----------+---------------+-----------+
| index_name  | lock_type | lock_mode     | lock_data |
+-------------+-----------+---------------+-----------+
| NULL        | TABLE     | IS            | NULL      |
| idx_stu_age | RECORD    | S             | 3, 3      |
| PRIMARY     | RECORD    | S,REC_NOT_GAP | 3         |
| idx_stu_age | RECORD    | S,GAP         | 7, 7      |
+-------------+-----------+---------------+-----------+

对应三种锁:

  • idx_stu_age 上的 S(无后缀)是临键锁:锁住 age = 3 这条记录及其前面的间隙;
  • PRIMARY 上的 S,REC_NOT_GAP 是行锁:回表取出整行时给主键行加锁;
  • idx_stu_age 上的 S,GAP 是间隙锁:因为 age 是普通索引,值可能重复,必须继续向右遍历;遍历到 7 发现不满足条件,next-key lock 退化为间隙锁,锁住 3 ~ 7 的间隙。

速查

-- 普通 SELECT → 不加任何锁
BEGIN;
SELECT * FROM stu WHERE id = 1;

-- 共享行锁(S)+ 意向共享锁(IS)→ 读且不允许别人改
BEGIN;
SELECT * FROM stu WHERE id = 1 LOCK IN SHARE MODE;

-- 排他行锁(X)+ 意向排他锁(IX)→ 更新自动加
BEGIN;
UPDATE stu SET name = 'Java' WHERE id = 1;

-- 行锁升级为表锁 → where 条件列上没有索引
BEGIN;
UPDATE stu SET name = 'Lei' WHERE name = 'lily';

-- 查看加锁情况 → REC_NOT_GAP = 行锁,GAP = 间隙锁,无后缀 = 临键锁
SELECT object_schema, object_name, index_name, lock_type, lock_mode, lock_data
FROM performance_schema.data_locks;
场景关键点
只想读不想别人改SELECT ... LOCK IN SHARE MODE(共享锁之间兼容,与排他锁冲突)
想改且不让别人读SELECT ... FOR UPDATE / UPDATE / DELETE / INSERT(自动加排他锁)
WHERE 走唯一索引等值命中优化为行锁,只锁这一行
WHERE 列无索引全表扫描加锁 ⇒ 退化为表锁,并发急剧下降
等值查询没命中(唯一索引)给间隙加间隙锁,防止别的事务插入该值
普通索引等值查询临键锁;向右扫描到不满足条件的值后退化为间隙锁
间隙锁之间不互斥,只用于阻止 INSERT

相关

  • 表级锁:表锁、元数据锁(MDL)、意向锁
  • 锁概述与全局锁:锁的分类与全局锁
  • 索引概述 / 索引结构:行锁加在索引项上,理解间隙锁先理解 B+ 树
  • 并发事务问题与隔离级别:幻读与 RR 隔离级别
  • SQL 优化 - limit / count / update:UPDATE 不走索引导致行锁升级为表锁
最近更新: 2026/10/14 21:03
Contributors: liujw
Prev
表级锁
Next
InnoDB 逻辑存储结构