Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • MySQL InnoDB
  • MySQL缓冲池与WAL机制
  • MySQL索引底层
  • MySQL事务与MVCC
  • MySQL锁机制
    • 一、锁的基础:先搞懂这两个核心分类
      • 1.1 按锁粒度分类:从表到行的精准控制
      • 1.2 按锁模式分类:共享与排他的核心区别
    • 二、行锁:InnoDB高并发的核心锁机制
      • 2.1 行锁的两种核心类型(InnoDB专属)
      • 2.1.1 记录锁(Record Lock):锁定具体的索引记录
      • 2.1.2 临键锁(Next-Key Lock):记录锁+间隙锁的组合
      • 2.2 行锁的核心依赖:索引是关键
      • 2.3 行锁的加锁与释放时机
    • 三、间隙锁:解决幻读的“隐形锁”
      • 3.1 间隙锁的核心特性
      • 3.2 间隙锁的锁定范围:如何确定间隙?
      • 3.2.1 基于主键索引的间隙锁
      • 3.2.2 基于二级索引的间隙锁
      • 3.3 间隙锁的“坑”:RC级别下的特殊情况
    • 四、意向锁:表锁与行锁的“协调者”
      • 4.1 意向锁的设计初衷:解决表锁与行锁的冲突检查问题
      • 4.2 意向锁的两种类型
      • 4.3 意向锁的加锁与释放时机
      • 4.4 意向锁与其他锁的兼容性矩阵
    • 五、MySQL锁机制的核心问题与实践建议
      • 5.1 常见锁问题:死锁的产生与解决
      • 5.1.1 死锁的定义与产生条件
      • 5.1.2 死锁案例与排查方法
      • 5.1.3 死锁的解决与预防
      • 5.2 锁机制实践优化建议
    • 六、总结
  • MySQL Binlog
  • MySQL崩溃恢复
  • MySQL查询优化
  • 《MySQL》笔记
Tavio
2022-08-04
目录

MySQL锁机制

# 深度解析MySQL锁机制:行锁/间隙锁/意向锁的底层逻辑

在MySQL高并发场景中,数据一致性与并发性能的平衡核心依赖锁机制。锁的本质是“并发控制的同步工具”,用于解决多事务并发执行时的写-写冲突、读-写冲突问题。InnoDB作为MySQL默认的事务型存储引擎,实现了粒度精细的锁机制,其中行锁、间隙锁、意向锁是支撑高并发的核心——行锁保证精准锁粒度提升并发,间隙锁解决幻读保证一致性,意向锁优化锁冲突检查效率。

# 一、锁的基础:先搞懂这两个核心分类

在深入具体锁类型前,先明确MySQL锁的两个核心分类维度(按粒度、按锁模式),这是理解后续锁机制的基础,避免出现术语混淆。

# 1.1 按锁粒度分类:从表到行的精准控制

锁粒度指锁的作用范围,粒度越小,并发性能越好(锁冲突概率低),但锁管理开销越大;粒度越大,并发性能越差,但锁管理开销越小。MySQL支持三种核心粒度的锁:

  • 表锁:作用范围是整张表。加锁时直接锁定全表,其他事务无法对该表执行写操作(更新/删除/插入),读操作可正常进行(若加的是读锁)。特点:开销小、加锁快,并发性能差。MyISAM存储引擎默认使用表锁,InnoDB也支持表锁(如LOCK TABLES),但极少使用。
  • 行锁:作用范围是单条数据行。仅锁定事务操作的具体行,其他事务可正常操作表中其他行。特点:开销大、加锁慢,并发性能好(锁冲突概率低)。InnoDB的核心锁类型,也是高并发场景的首选。
  • 间隙锁:作用范围是“数据行之间的间隙”(不锁定具体数据行)。用于防止其他事务在间隙中插入新数据,从而解决幻读问题。仅InnoDB支持,且仅在可重复读(RR)隔离级别下生效。

# 1.2 按锁模式分类:共享与排他的核心区别

锁模式决定了锁的“互斥规则”,MySQL核心支持两种锁模式,其他锁(如意向锁)均基于这两种模式衍生:

  • 共享锁(Shared Lock,简称S锁):也叫读锁。多个事务可同时对同一资源加S锁(读-读不冲突),但加了S锁的资源无法被加排他锁(读-写冲突)。事务加S锁后,仅能执行读操作,无法执行写操作。
  • 排他锁(Exclusive Lock,简称X锁):也叫写锁。仅允许一个事务对资源加X锁(写-写冲突),加了X锁的资源无法被其他事务加任何锁(读-写、写-读均冲突)。事务加X锁后,可执行读、写操作。

核心原则:共享锁之间兼容,排他锁与任何锁都不兼容。这是后续所有锁兼容性的基础。

# 二、行锁:InnoDB高并发的核心锁机制

行锁是InnoDB实现“精准锁控制”的核心,也是支撑高并发写操作的关键。与表锁相比,行锁仅锁定需要修改的行,极大降低了锁冲突概率。但行锁的实现依赖索引,这是很多开发者踩坑的核心点——“无索引则行锁升级为表锁”。

# 2.1 行锁的两种核心类型(InnoDB专属)

InnoDB的行锁并非直接锁定数据行本身,而是锁定“索引项”——通过索引定位到数据行后,对对应的索引项加锁。因此,行锁的类型与索引类型强相关,分为两种:

# 2.1.1 记录锁(Record Lock):锁定具体的索引记录

记录锁是最基础的行锁,直接锁定索引对应的具体数据行。仅当事务操作“精准命中索引”(如主键等值查询、唯一索引等值查询)时,才会加记录锁。

案例演示(基于InnoDB,RR隔离级别): 假设user表结构:id(主键,INT)、name(VARCHAR)、age(INT),数据如下:

id name age
1 张三 25
2 李四 30

场景1:事务A精准命中主键索引,加记录锁(X锁):

-- 事务A(未提交)
UPDATE user SET age=26 WHERE id=1; -- 命中主键索引,对id=1的索引项加X锁
1
2

此时,事务B操作id=1的行会被阻塞(写-写冲突),但操作id=2的行可正常执行(仅锁id=1的行):

-- 事务B
UPDATE user SET age=31 WHERE id=1; -- 阻塞,等待事务A释放X锁
UPDATE user SET age=31 WHERE id=2; -- 正常执行,无锁冲突
1
2
3

# 2.1.2 临键锁(Next-Key Lock):记录锁+间隙锁的组合

临键锁是InnoDB RR级别下的默认行锁类型(当查询未精准命中索引时触发),本质是“记录锁+间隙锁”的组合,锁定范围是“左开右闭”的区间。其核心目的是解决“幻读”问题——通过锁定索引记录及其相邻间隙,防止其他事务插入新数据。

案例演示(基于上述user表,无age索引):

-- 事务A(未提交):范围查询,未命中索引(age无索引)
UPDATE user SET name='张三三' WHERE age > 25 AND age < 35;
1
2

由于age无索引,InnoDB会走全表扫描,此时行锁升级为表锁?不!实际是加临键锁,但因无索引,临键锁的范围会覆盖全表所有间隙+记录,等价于表锁。因此,事务B操作任何行都会被阻塞:

-- 事务B
UPDATE user SET age=27 WHERE id=1; -- 阻塞
INSERT INTO user(id, name, age) VALUES(3, '王五', 28); -- 阻塞
1
2
3

若给age添加索引(二级索引),则临键锁会基于age索引的间隙锁定:

-- 给age加索引
CREATE INDEX idx_age ON user(age);

-- 事务A(未提交):范围查询,命中age索引
UPDATE user SET name='张三三' WHERE age > 25 AND age < 35;
1
2
3
4
5

此时,age索引的有效数据是25、30,间隙包括:(-∞,25)、(25,30)、(30,+∞)。事务A的查询条件是25<age<35,因此临键锁锁定的范围是(25,30](包含30的记录锁+25-30的间隙锁)和(30,+∞)的间隙锁。因此:

-- 事务B
INSERT INTO user(id, name, age) VALUES(3, '王五', 26); -- 阻塞(26在25-30间隙)
INSERT INTO user(id, name, age) VALUES(4, '赵六', 31); -- 阻塞(31在30-+∞间隙)
INSERT INTO user(id, name, age) VALUES(5, '孙七', 24); -- 正常执行(24在-∞-25间隙,未被锁定)
1
2
3
4

# 2.2 行锁的核心依赖:索引是关键

前面的案例已经体现:InnoDB的行锁是“基于索引的锁”,若查询条件未命中任何索引(或使用全表扫描),则行锁会升级为“全表临键锁”(等价于表锁),这是高并发场景的性能杀手。

核心原因:InnoDB通过索引定位数据行,若没有索引,InnoDB无法精准定位到具体行,只能通过全表扫描遍历所有行,此时为了保证数据一致性,会对全表的所有记录和间隙加锁,即升级为表级锁。

避坑建议:任何需要加行锁的写操作(update/delete),必须保证查询条件命中索引,避免行锁升级。

# 2.3 行锁的加锁与释放时机

InnoDB行锁的加锁时机:事务执行写操作(update/delete/insert)或手动加锁(select ... for share/for update)时,自动/手动为对应的索引项加锁。

行锁的释放时机:仅当事务提交(COMMIT)或回滚(ROLLBACK)时释放,而非操作执行完成后立即释放。这意味着“长事务”会长时间持有行锁,导致其他事务阻塞,是锁等待的主要原因之一。

案例演示(长事务导致锁等待):

-- 事务A(未提交,长事务)
START TRANSACTION;
UPDATE user SET age=26 WHERE id=1; -- 加X锁

-- 事务B(此时执行)
UPDATE user SET age=27 WHERE id=1; -- 阻塞,等待事务A释放锁

-- 事务A 30秒后提交
COMMIT; -- 释放锁,事务B才会执行
1
2
3
4
5
6
7
8
9

# 三、间隙锁:解决幻读的“隐形锁”

间隙锁(Gap Lock)是InnoDB为了解决“幻读”问题而设计的特殊锁,其核心特点是“不锁定具体的数据行,仅锁定数据行之间的间隙”。间隙锁本身不影响已存在的数据行操作,但能阻止其他事务在间隙中插入新数据,从而避免同一事务内多次范围查询出现不同的结果集(幻读)。

# 3.1 间隙锁的核心特性

  • 锁定范围:仅锁定“间隙”,不锁定间隙内的具体记录。间隙的定义:两个索引项之间的区间,或索引项与无穷小/无穷大之间的区间。
  • 生效条件:仅InnoDB支持,且仅在“可重复读(RR)”隔离级别下生效(RC级别为了提升并发性能,默认关闭间隙锁)。
  • 核心目的:防止其他事务在间隙中插入新数据,解决幻读问题。
  • 兼容性:间隙锁之间是兼容的——多个事务可同时对同一个间隙加间隙锁,因为间隙锁的目的是防止插入,而非阻止其他锁的添加。

# 3.2 间隙锁的锁定范围:如何确定间隙?

间隙锁的锁定范围完全基于“索引”,不同的索引类型(主键、二级索引)、不同的查询条件(等值、范围),对应的间隙范围不同。核心规则:间隙锁的间隙由查询条件匹配的索引项决定。

# 3.2.1 基于主键索引的间隙锁

假设user表主键id的索引项为:1、3、5、7,对应的间隙包括:

  • (-∞, 1):id小于1的间隙
  • (1, 3):id在1和3之间的间隙
  • (3, 5):id在3和5之间的间隙
  • (5, 7):id在5和7之间的间隙
  • (7, +∞):id大于7的间隙

案例1:等值查询不存在的主键,加间隙锁

-- 事务A(未提交,RR级别):查询id=4(不存在),加间隙锁
SELECT * FROM user WHERE id=4 FOR UPDATE; -- 手动加X锁
1
2

此时,id=4落在(3,5)间隙中,因此InnoDB会对(3,5)间隙加间隙锁。事务B在该间隙插入数据会被阻塞:

-- 事务B
INSERT INTO user(id, name, age) VALUES(4, '王五', 28); -- 阻塞(落在3-5间隙)
INSERT INTO user(id, name, age) VALUES(2, '赵六', 29); -- 正常执行(落在1-3间隙,未被锁定)
1
2
3

# 3.2.2 基于二级索引的间隙锁

二级索引的间隙锁与主键索引类似,但需注意:二级索引可能存在重复值(非唯一索引),因此间隙的划分会包含重复值的区间。

假设user表的age(二级索引)数据为:25、25、30、35,对应的间隙包括:

  • (-∞, 25):age小于25的间隙
  • (25, 30):age在25和30之间的间隙
  • (30, 35):age在30和35之间的间隙
  • (35, +∞):age大于35的间隙

案例2:范围查询二级索引,加间隙锁

-- 事务A(未提交,RR级别):范围查询age>25 and age<35
SELECT * FROM user WHERE age>25 AND age<35 FOR UPDATE;
1
2

此时,锁定的间隙为(25,30)、(30,35),同时对age=30的记录加记录锁(临键锁)。因此,事务B插入age在25-35之间的数据会被阻塞:

-- 事务B
INSERT INTO user(id, name, age) VALUES(6, '孙七', 28); -- 阻塞(28在25-30间隙)
INSERT INTO user(id, name, age) VALUES(7, '周八', 32); -- 阻塞(32在30-35间隙)
INSERT INTO user(id, name, age) VALUES(8, '吴九', 24); -- 正常执行(24在-∞-25间隙)
1
2
3
4

# 3.3 间隙锁的“坑”:RC级别下的特殊情况

默认情况下,RC级别会关闭间隙锁(通过参数innodb_locks_unsafe_for_binlog控制,默认ON),因此RC级别下不会出现间隙锁,也就无法解决幻读问题。但有一个例外:若查询条件是“唯一索引等值查询”,即使在RC级别,也会加记录锁(而非间隙锁),因为唯一索引能精准定位到具体行,无需通过间隙锁防止插入。

避坑建议:若业务需要避免幻读,必须使用RR隔离级别;若追求高并发读性能(可接受不可重复读、幻读),可使用RC级别,但需注意数据一致性风险。

# 四、意向锁:表锁与行锁的“协调者”

意向锁(Intention Lock)是InnoDB为了解决“表锁与行锁冲突检查效率”而设计的“辅助锁”。其核心作用是“提前声明事务的锁意图”,让后续加表锁时无需遍历所有行检查是否有行锁,只需检查意向锁即可,大幅提升锁冲突检查效率。

# 4.1 意向锁的设计初衷:解决表锁与行锁的冲突检查问题

假设没有意向锁,当事务A对表中的某行加了行锁(X锁),此时事务B想对整张表加表锁(X锁),数据库需要做什么?——必须遍历表中所有行,检查是否有行锁存在。若表中有百万级数据,这个检查过程会极其耗时,效率极低。

意向锁的出现就是为了解决这个问题:事务在加行锁前,会先对整张表加“意向锁”,声明“我要对表中的某行加某种类型的锁”。后续加表锁时,只需检查表上的意向锁是否与表锁冲突,无需遍历所有行。

# 4.2 意向锁的两种类型

意向锁与行锁的模式对应,分为两种,均为表级锁:

  • 意向共享锁(Intention Shared Lock,简称IS锁):事务计划对表中的某行加共享锁(S锁)前,先对表加IS锁。
  • 意向排他锁(Intention Exclusive Lock,简称IX锁):事务计划对表中的某行加排他锁(X锁)前,先对表加IX锁。

核心规则:意向锁是“声明性锁”,不影响其他事务对表的读/写操作,仅用于锁冲突检查。

# 4.3 意向锁的加锁与释放时机

  • 加锁时机:事务执行加行锁的操作(如select ... for share加S锁、update加X锁)时,InnoDB自动先对表加对应的意向锁(IS/IX),再对具体行加行锁。无需手动加意向锁,InnoDB自动管理。
  • 释放时机:与行锁一致,仅当事务提交或回滚时,意向锁与行锁一同释放。

案例演示:

-- 事务A(未提交):对id=1的行加X锁,InnoDB自动先对表加IX锁
UPDATE user SET age=26 WHERE id=1;

-- 事务B:尝试对表加表级S锁
LOCK TABLES user READ; -- 检查user表的意向锁:IX锁与表级S锁冲突,因此阻塞
1
2
3
4
5

若没有IX锁,事务B需要遍历所有行检查是否有X锁;有了IX锁,只需检查表上的IX锁与表级S锁冲突,直接阻塞,效率大幅提升。

# 4.4 意向锁与其他锁的兼容性矩阵

意向锁的兼容性核心:意向锁之间兼容;意向锁与表锁的兼容性取决于行锁模式;意向锁与行锁不冲突(因为粒度不同)。具体兼容性矩阵如下:

当前锁\请求锁 IS锁(表级) IX锁(表级) S锁(表级) X锁(表级) S锁(行级) X锁(行级)
IS锁(表级) 兼容 兼容 兼容 冲突 兼容 兼容
IX锁(表级) 兼容 兼容 冲突 冲突 兼容 兼容
S锁(表级) 兼容 冲突 兼容 冲突 兼容 冲突
X锁(表级) 冲突 冲突 冲突 冲突 冲突 冲突

核心总结:

  • 意向锁(IS/IX)之间完全兼容,因为它们只是“声明意图”,不实际阻塞操作。
  • 表级S锁与IS锁兼容,与IX锁冲突;表级X锁与所有意向锁都冲突。
  • 意向锁与行锁完全兼容,因为粒度不同(表级 vs 行级),不会产生冲突。

# 五、MySQL锁机制的核心问题与实践建议

理解了三种核心锁的底层逻辑后,我们需要结合实际工作场景,解决常见的锁问题(如死锁、锁等待),并给出可落地的优化建议。

# 5.1 常见锁问题:死锁的产生与解决

# 5.1.1 死锁的定义与产生条件

死锁:两个或多个事务相互持有对方需要的锁,且都无法释放自己的锁,导致所有事务永久阻塞。死锁的产生必须满足四个条件(缺一不可):

  • 互斥条件:锁是排他的,同一时间只能被一个事务持有。
  • 持有并等待条件:事务持有一个锁后,又等待另一个锁,且不释放已持有的锁。
  • 不可剥夺条件:锁只能由持有事务主动释放,无法被其他事务强制剥夺。
  • 循环等待条件:多个事务形成“A等待B的锁,B等待C的锁,C等待A的锁”的循环链。

# 5.1.2 死锁案例与排查方法

死锁案例(基于user表,id和age均有索引):

-- 事务A
START TRANSACTION;
UPDATE user SET age=26 WHERE id=1; -- 持有id=1的X锁,等待age=30的X锁
UPDATE user SET name='李四四' WHERE age=30;

-- 事务B
START TRANSACTION;
UPDATE user SET name='张三三' WHERE age=30; -- 持有age=30的X锁,等待id=1的X锁
UPDATE user SET age=27 WHERE id=1;
1
2
3
4
5
6
7
8
9

此时,事务A持有id=1的X锁,等待事务B的age=30的X锁;事务B持有age=30的X锁,等待事务A的id=1的X锁,形成循环等待,触发死锁。

死锁排查方法:通过SHOW ENGINE INNODB STATUS;查看死锁日志,日志中会显示死锁的事务ID、持有/等待的锁类型、SQL语句等信息,帮助定位问题。

# 5.1.3 死锁的解决与预防

  1. 破坏循环等待条件:统一事务的加锁顺序。例如,所有事务都先加id索引的锁,再加age索引的锁,避免循环等待。
  2. 控制事务大小,减少锁持有时间:避免长事务,及时提交/回滚,减少锁的持有时间,降低死锁概率。
  3. 避免一次性锁定过多资源:拆分事务,将一次性锁定多个资源的操作拆分为多个小事务,逐个锁定资源。
  4. 使用低隔离级别:如RC级别,关闭间隙锁,减少锁的数量,降低死锁概率(需权衡一致性)。
  5. 开启死锁检测与自动回滚:InnoDB默认开启死锁检测(参数innodb_deadlock_detect默认ON),检测到死锁后会自动回滚“持有锁最少”的事务,避免永久阻塞。

# 5.2 锁机制实践优化建议

  1. 优先使用行锁,避免表锁:所有写操作(update/delete)必须保证查询条件命中索引,避免行锁升级为表锁;尽量使用主键/唯一索引进行精准查询,减少临键锁的范围。
  2. 合理选择隔离级别:高并发读场景(如电商详情)使用RC级别,关闭间隙锁提升并发;需要避免幻读的场景(如金融交易)使用RR级别,依赖间隙锁保证一致性。
  3. 控制事务粒度,避免长事务:事务内只包含必要的操作,避免在事务内执行耗时操作(如RPC调用、文件IO);及时提交/回滚事务,减少锁持有时间。
  4. 避免间隙锁的不必要锁定:若业务无需避免幻读,使用RC级别;若必须使用RR级别,尽量使用等值查询(加记录锁),避免范围查询(加临键锁/间隙锁)。
  5. 监控锁等待与死锁:通过SHOW ENGINE INNODB STATUS查看锁等待情况;通过information_schema.INNODB_LOCKS、information_schema.INNODB_LOCK_WAITS表查询当前的锁信息和锁等待关系;定期分析死锁日志,优化SQL和事务。
  6. 手动加锁需谨慎:避免滥用SELECT ... FOR UPDATE(加X锁),若仅需读一致性,可使用SELECT ... FOR SHARE(加S锁),或依赖MVCC的快照读(无需加锁)。

# 六、总结

MySQL InnoDB的锁机制是平衡并发性能与数据一致性的核心,其底层逻辑可总结为:

  1. 锁的核心价值是解决并发冲突(写-写、读-写),按粒度分为表锁、行锁、间隙锁,按模式分为S锁、X锁。
  2. 行锁是高并发的核心,分为记录锁(精准锁)和临键锁(记录锁+间隙锁),依赖索引实现,无索引则升级为表锁。
  3. 间隙锁是RR级别解决幻读的关键,锁定数据间隙防止插入新数据,仅在RR级别生效,RC级别关闭。
  4. 意向锁是表锁与行锁的协调者,提前声明锁意图,提升锁冲突检查效率,分为IS锁和IX锁,自动管理无需手动操作。
  5. 实际优化的核心是:优先使用行锁、保证索引命中、控制事务大小、避免死锁,根据业务场景选择合适的隔离级别。
编辑 (opens new window)
#MySQL锁机制
上次更新: 2026/01/21, 19:29:14
MySQL事务与MVCC
MySQL Binlog

← MySQL事务与MVCC MySQL Binlog→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式