对于MySQL的锁,我习惯从几个不同的维度去理解它。首先,从锁定粒度上,有我们熟知的行锁和表锁。其次,从锁的兼容性上,可以分为共享锁(S锁)和排它锁(X锁),这决定了它们是否可以共存。
在这两个基础维度之上,InnoDB为了提升性能,还引入了意向锁,它是一种表级锁,用来预示事务接下来想要加的行锁类型。
而在InnoDB的行锁实现中,又有三种具体的锁算法,这也是面试中的重点:记录锁、间隙锁和临键锁。它们是InnoDB在RR级别下解决幻读问题的核心。
最后,从编程思想的层面,我们还有乐观锁和悲观锁的概念,它们是我们在应用层进行并发控制的两种不同策略。”
MySQL 锁机制全面解析
前言
在高并发场景下,数据库锁是保证数据一致性和完整性的核心机制。对于 MySQL 的锁,我们可以从多个维度去理解它:锁定粒度、锁的兼容性、意向锁、行锁算法以及编程思想层面的锁。本文将从这几个维度全面剖析 MySQL 的锁机制。
一、从锁定粒度划分:表锁与行锁
1.1 表锁(Table Lock)
表锁是 MySQL 中粒度最大的锁,它会锁定整张表。
特点:
- 开销小,加锁快
- 不会出现死锁
- 锁定粒度大,发生锁冲突的概率高,并发度低
适用场景:
- MyISAM 存储引擎默认使用表锁
- InnoDB 在执行 DDL 语句时也会使用表锁
-- 手动加表锁
LOCK TABLES table_name READ; -- 读锁
LOCK TABLES table_name WRITE; -- 写锁
-- 释放表锁
UNLOCK TABLES;
1.2 行锁(Row Lock)
行锁是粒度最细的锁,它只锁定需要操作的那一行数据。
特点:
- 开销大,加锁慢
- 会出现死锁
- 锁定粒度小,发生锁冲突的概率低,并发度高
适用场景:
- InnoDB 存储引擎默认使用行锁
- 适合高并发的 OLTP 场景
⚠️ 注意:InnoDB 的行锁是基于索引实现的。如果 SQL 语句没有使用索引,行锁会退化为表锁。
二、从锁的兼容性划分:共享锁与排它锁
2.1 共享锁(Shared Lock,S 锁)
共享锁又称读锁,允许事务读取一行数据。
特点:
- 多个事务可以同时持有同一资源的共享锁
- 持有 S 锁时,其他事务只能再加 S 锁,不能加 X 锁
-- 显式加共享锁
SELECT * FROM table_name WHERE id = 1 LOCK IN SHARE MODE;
-- MySQL 8.0+ 推荐写法
SELECT * FROM table_name WHERE id = 1 FOR SHARE;
2.2 排它锁(Exclusive Lock,X 锁)
排它锁又称写锁,允许事务更新或删除一行数据。
特点:
- 持有 X 锁时,其他事务既不能加 S 锁也不能加 X 锁
- 保证在同一时间只有一个事务能对数据进行写操作
-- 显式加排它锁
SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
-- DML 语句会自动加排它锁
UPDATE table_name SET name = 'test' WHERE id = 1;
DELETE FROM table_name WHERE id = 1;
2.3 兼容性矩阵
| S 锁 | X 锁 | |
|---|---|---|
| S 锁 | ✅ 兼容 | ❌ 冲突 |
| X 锁 | ❌ 冲突 | ❌ 冲突 |
三、意向锁(Intention Lock)
3.1 为什么需要意向锁?
假设事务 A 对某行加了行级 X 锁,此时事务 B 想对整张表加表级 S 锁。如果没有意向锁,数据库需要逐行检查是否有行锁存在,效率极低。
意向锁的出现就是为了快速判断表中是否有行锁,从而提升加表锁时的效率。
3.2 意向锁的类型
- 意向共享锁(IS):事务打算给某些行加 S 锁
- 意向排它锁(IX):事务打算给某些行加 X 锁
3.3 加锁规则
- 事务在获取行级 S 锁之前,必须先获取表的 IS 锁或更强的锁
- 事务在获取行级 X 锁之前,必须先获取表的 IX 锁
3.4 意向锁兼容性矩阵
| IS | IX | S | X | |
|---|---|---|---|---|
| IS | ✅ | ✅ | ✅ | ❌ |
| IX | ✅ | ✅ | ❌ | ❌ |
| S | ✅ | ❌ | ✅ | ❌ |
| X | ❌ | ❌ | ❌ | ❌ |
💡 关键点:意向锁之间互相兼容,它们的作用仅仅是表明意向,真正的冲突检测发生在行锁级别。
四、InnoDB 行锁的三种算法(重点)
InnoDB 在 RR(可重复读) 隔离级别下,通过以下三种锁算法来解决幻读问题。
4.1 记录锁(Record Lock)
记录锁是精确锁定索引记录的锁,只锁定符合条件的那一行。
-- 假设 id 是主键,以下语句会对 id=1 的记录加记录锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
特点:
- 锁定的是索引记录本身
- 如果表没有索引,InnoDB 会使用隐藏的聚簇索引
4.2 间隙锁(Gap Lock)
间隙锁锁定的是索引记录之间的间隙,不包括记录本身。
-- 假设表中有 id: 1, 5, 10 三条记录
-- 以下语句会锁定 (5, 10) 这个间隙
SELECT * FROM users WHERE id > 5 AND id < 10 FOR UPDATE;
特点:
- 防止其他事务在间隙中插入新记录
- 间隙锁之间不互斥(都是为了防止插入)
- 只在 RR 隔离级别下生效
4.3 临键锁(Next-Key Lock)
临键锁是记录锁 + 间隙锁的组合,锁定的是一个左开右闭的区间。
-- 假设表中有 id: 1, 5, 10 三条记录
-- 以下语句的锁范围是 (5, 10],包含记录 10 以及 10 之前的间隙
SELECT * FROM users WHERE id = 10 FOR UPDATE;
特点:
- InnoDB 默认的行锁算法
- 有效解决幻读问题
- 锁定范围:(前一个索引值, 当前索引值]
4.4 三种锁的关系图示
索引值: 1 5 10 15
|--------|--------|--------|--------|
记录锁: [5] -- 只锁记录 5
间隙锁: (1, 5) -- 锁 1 和 5 之间的间隙
临键锁: (1, 5] -- 间隙 + 记录 5
4.5 加锁规则总结
| 查询类型 | 等值查询(唯一索引) | 等值查询(非唯一索引) | 范围查询 |
|---|---|---|---|
| 锁类型 | 记录锁 | 临键锁 + 间隙锁 | 临键锁 |
五、编程思想层面:乐观锁与悲观锁
乐观锁和悲观锁不是数据库真正的锁,而是并发控制的两种设计思想。
5.1 悲观锁(Pessimistic Lock)
悲观锁假设冲突一定会发生,因此在操作数据前先加锁。
实现方式:
-- 使用数据库的锁机制
BEGIN;
SELECT * FROM products WHERE id = 1 FOR UPDATE; -- 加排它锁
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
适用场景:
- 写操作频繁
- 冲突概率高
- 对数据一致性要求严格
5.2 乐观锁(Optimistic Lock)
乐观锁假设冲突不太会发生,只在提交时检查是否有冲突。
实现方式一:版本号机制
-- 查询时获取版本号
SELECT id, stock, version FROM products WHERE id = 1;
-- 假设查到 version = 1
-- 更新时校验版本号
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 1;
-- 如果 affected_rows = 0,说明有其他事务修改过,需要重试
实现方式二:CAS(Compare And Swap)
-- 直接比较原值
UPDATE products SET stock = 99 WHERE id = 1 AND stock = 100;
适用场景:
- 读操作频繁
- 冲突概率低
- 追求高并发性能
5.3 对比总结
| 特性 | 悲观锁 | 乐观锁 |
|---|---|---|
| 实现层面 | 数据库层(真实锁) | 应用层(逻辑控制) |
| 冲突假设 | 一定会冲突 | 不太会冲突 |
| 性能 | 有锁等待开销 | 无锁等待,但可能重试 |
| 死锁 | 可能产生 | 不会产生 |
| 适用场景 | 写多读少 | 读多写少 |
六、总结
MySQL 的锁机制是一个多层次的体系,我们可以用下图来总结:
MySQL 锁机制
│
┌───────────────────┼───────────────────┐
│ │ │
锁定粒度 锁的兼容性 编程思想
│ │ │
┌───┴───┐ ┌───┴───┐ ┌───┴───┐
表锁 行锁 S锁 X锁 乐观锁 悲观锁
│
意向锁(IS/IX)
│
┌───────┼───────┐
记录锁 间隙锁 临键锁
核心要点回顾:
- 表锁 vs 行锁:粒度越小,并发越高,但开销也越大
- S 锁 vs X 锁:S 锁兼容 S 锁,X 锁与任何锁都互斥
- 意向锁:快速判断表中是否存在行锁,提升加表锁效率
- 记录锁/间隙锁/临键锁:InnoDB 在 RR 级别解决幻读的核心
- 乐观锁/悲观锁:根据业务场景选择合适的并发控制策略
理解 MySQL 锁机制,不仅能帮助我们写出高效的 SQL,更能在排查死锁、优化性能时游刃有余。
参考资料
- MySQL 官方文档:InnoDB Locking
- 《高性能 MySQL》第三版
- 《MySQL 技术内幕:InnoDB 存储引擎》