MySQL 锁知识点大全源码级分析【共享锁、排他锁、意向锁、记录锁、间隙锁、临键锁、死锁、锁退化】
核心思想
MySQL 的锁机制,尤其是在 InnoDB 存储引擎中,其核心目标是在保证数据一致性(ACID 中的 I-Isolation)的前提下,最大限度地提高数据库的并发性能。它通过多粒度锁定(Multigranularity Locking)和意向锁(Intention Locking)来实现这一目标。
一、 锁的类型与详细用法
MySQL 的锁可以按多个维度进行分类。
1. 按操作类型划分
a. 共享锁 (Shared Lock, S-Lock)
- 用处:用于读取操作。多个事务可以同时持有对同一资源的共享锁。
- 用法:
SELECT ... LOCK IN SHARE MODE; -- 老语法,但仍有效 SELECT ... FOR SHARE; -- MySQL 8.0+ 推荐语法 - 特点:
- 兼容性:与其他的
S-Lock兼容,与X-Lock不兼容。 - 目的:确保在您读取数据的过程中,数据不会被其他事务修改(但可以被其他事务并发读取)。它提供了一种“读锁”的机制。
- 兼容性:与其他的
b. 排他锁 (Exclusive Lock, X-Lock)
- 用处:用于写入操作(
UPDATE,DELETE,INSERT)。一个事务持有某资源的排他锁后,其他事务无法再获取该资源的任何类型的锁。 - 用法:
- 自动加锁:
UPDATE,DELETE,INSERT语句会自动为受影响的数据加 X-Lock。 - 手动加锁:
SELECT ... FOR UPDATE;
- 自动加锁:
- 特点:
- 兼容性:与任何其他锁(S 或 X)都不兼容。
- 目的:确保在您修改数据的过程中,数据不会被其他事务以任何方式读取(
FOR SHARE)或修改(FOR UPDATE),从而避免脏写和脏读。
2. 按锁的粒度划分
这是理解 InnoDB 锁的关键。
a. 行级锁 (Row-Level Lock)
- 用处:锁定表中的单行或多行记录。这是 InnoDB 实现高并发的最重要特性。
- 用法:通过
SELECT ... FOR UPDATE或 DML 语句在满足条件的行上自动获取。 - 特点:
- 粒度小,并发度高,但开销最大。
- 行锁实际上是加在索引记录上的。这一点至关重要!
- 如果查询条件有索引,则锁住对应的索引记录。
- 如果查询条件无索引或索引失效,会导致 行锁升级为表锁(实际上是锁住所有扫描过的记录和间隙,效果类似表锁,性能极差)。
行级锁的细分类型:
-
记录锁 (Record Lock)
- 用处:锁定索引中的一条具体记录。
- 场景:
SELECT * FROM t WHERE id = 1 FOR UPDATE;会在id=1的索引记录上加 X 型记录锁。 - 源码:
lock_rec_lock函数,锁对象存储在lock_sys->rec_hash哈希表中,通过(space_id, page_no, heap_no)可以快速定位到某一行上的锁。
-
间隙锁 (Gap Lock)
- 用处:锁定一个索引记录之间的范围,但不包括记录本身。目的是防止其他事务在这个范围内插入新数据,从而解决幻读问题。
- 场景:
SELECT * FROM t WHERE id BETWEEN 5 AND 10 FOR UPDATE;会在(5, 10)这个开区间加 Gap Lock,阻止id=6,7,8,9的新记录插入。 - 特点:
- 只在
READ COMMITTED及以上隔离级别生效(但 MySQL 在READ COMMITTED下又会禁用 Gap Lock)。 - 仅用于防止插入。
- 只在
- 源码:同样是
lock_rec_lock函数,通过LOCK_GAP模式标识。
-
临键锁 (Next-Key Lock)
- 用处:记录锁 (Record Lock) + 间隙锁 (Gap Lock) 的组合。锁定一个索引记录及其之前的间隙。这是 InnoDB 在
REPEATABLE READ隔离级别下默认的行锁算法。 - 场景:如果表中有数据
10, 20, 30,执行SELECT * FROM t WHERE id = 20 FOR UPDATE;不仅会锁住id=20这条记录,还会锁住(10, 20]这个区间。这既防止了其他事务修改id=20的记录,也防止了在10和20之间插入新记录(如id=15)。 - 目的:彻底解决幻读问题。
- 源码:默认的行锁模式,由
LOCK_ORDINARY标识。
- 用处:记录锁 (Record Lock) + 间隙锁 (Gap Lock) 的组合。锁定一个索引记录及其之前的间隙。这是 InnoDB 在
-
插入意向锁 (Insert Intention Lock)
- 用处:一种特殊的间隙锁,由
INSERT操作在插入行之前申请。它** signaling 其意图,目的是为了优化多个事务在相同间隙内的插入操作**,只要插入的位置不冲突,就不需要互相等待。 - 特点:互相兼容(如果插入的位置不同)。例如,事务 A 想在间隙中插入
id=15,事务 B 想插入id=16,它们不会互相阻塞。 - 源码:
lock_rec_insert_intention函数,模式为LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION。
- 用处:一种特殊的间隙锁,由
b. 表级锁 (Table-Level Lock)
- 用处:锁定整张表。
- 用法:
- 自动加锁:DDL 语句(如
ALTER TABLE)会自动加表级锁。 - 手动加锁:
LOCK TABLES ... READ/WRITE;(服务器层命令,与事务不同,一般不建议在 InnoDB 中使用)。
- 自动加锁:DDL 语句(如
- 特点:
- 粒度大,并发度极低,但开销小。
- InnoDB 通常不需要手动表锁,因为其行锁粒度更细。
LOCK TABLES会隐式提交当前事务。
c. 意向锁 (Intention Lock)
- 用处:表级锁,用于快速判断表中是否有行被锁定。它是一种“信号灯”锁,表明一个事务即将(或正在)对表中的某些行施加 S-Lock 或 X-Lock。
- 类型:
- 意向共享锁 (Intention Shared Lock, IS):事务打算给某些行加 S-Lock。
- 意向排他锁 (Intention Exclusive Lock, IX):事务打算给某些行加 X-Lock。
- 特点:
- 意向锁之间是兼容的(例如,多个事务可以同时持有 IX 锁,因为它们可能只是修改不同的行)。
- 意向锁的主要目的是为了在加表锁时(例如,执行
ALTER TABLE)能够快速判断是否可以立即获得表锁,而无需逐行检查是否有行锁冲突。如果表上有 IS/IX,说明有行被锁定,表锁请求可能会被阻塞。
二、 死锁 (Deadlock)
1. 条件与产生场景
死锁是指两个或多个事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象,若无外力干涉,它们都无法继续执行。
经典场景:
-
场景一:交叉更新
- 事务 A:
UPDATE t SET ... WHERE id = 1;(持有 id=1 的 X-Lock) - 事务 B:
UPDATE t SET ... WHERE id = 2;(持有 id=2 的 X-Lock) - 事务 A:
UPDATE t SET ... WHERE id = 2;(尝试获取 id=2 的 X-Lock,被 B 阻塞) - 事务 B:
UPDATE t SET ... WHERE id = 1;(尝试获取 id=1 的 X-Lock,被 A 阻塞) -> 死锁形成。
- 事务 A:
-
场景二:间隙锁冲突
- 两个事务在相同的间隙上执行
SELECT ... FOR UPDATE和INSERT,也可能因 Next-Key Lock 和 Insert Intention Lock 的交互而产生死锁。
- 两个事务在相同的间隙上执行
2. 检测与解决
- 检测:InnoDB 使用等待图 (Wait-for Graph) 算法来主动检测死锁。它维护一个事务等待链表,如果检测到循环(A 等 B,B 等 A),即判定为死锁。
- 解决:
- 自动解决:InnoDB 的死锁检测引擎一旦发现死锁,会立即回滚其中代价较小的事务(通常是根据修改的行数来判断)。另一个事务因此可以继续执行。应用程序会收到
1213错误:Deadlock found when trying to get lock; try restarting transaction。 - 手动处理:应用程序必须捕获这个死锁错误,并选择重试整个事务。这是处理死锁的标准做法。
- 自动解决:InnoDB 的死锁检测引擎一旦发现死锁,会立即回滚其中代价较小的事务(通常是根据修改的行数来判断)。另一个事务因此可以继续执行。应用程序会收到
3. 避免策略
- 保持事务小巧且简短,尽快提交,减少锁的持有时间。
- 以固定的顺序访问表和数据行。这是避免死锁最有效的方法。如果所有事务都按
先id=1,后id=2的顺序操作,上例中的死锁就不会发生。 - 在事务中尽量一次锁定所有需要的资源。
- 为查询建立合适的索引,避免因全表扫描导致锁住大量不需要的数据,增加冲突概率。
- 如果死锁不频繁,可以临时关闭死锁检测 (
innodb_deadlock_detect = OFF),依赖锁等待超时 (innodb_lock_wait_timeout) 来解锁。但这在高压环境下可能导致长时间等待,需谨慎使用。
三、 锁退化 (Lock Degradation)
锁退化是指将锁的粒度降低的过程,例如将一个保护范围很大的锁(如表锁)替换为多个保护范围更小的锁(如行锁)。
-
InnoDB 中的体现:严格来说,InnoDB 的设计中没有传统意义上的“锁退化”。它遵循的是“锁升级”(Lock Escalation)的反面逻辑。InnoDB 的理念是:始终从最细的粒度(行锁)开始加锁。
-
过程:当一个查询需要扫描大量数据时,InnoDB 不会先加一个表锁,而是会逐行(或逐页)地加行锁。它并不会先持有粗粒度锁再退化为细粒度锁。
-
“类似退化”的行为:可以理解为 InnoDB 在事务执行过程中,根据优化器的判断,动态地调整锁定的范围。例如,在
WHERE条件复杂时,可能一开始锁定的范围较大,随着过滤条件的应用,实际生效的锁范围可能缩小。但这更多是锁的释放,而非主动的退化机制。
锁退化的过程
这与某些数据库(如 SQL Server)先加意向锁再加行锁,或在行锁过多时升级为表锁的机制有根本区别。InnoDB 的架构决定了它优先使用行锁,且没有锁升级机制。
四、 源码级简要分析
锁系统的核心代码在 /storage/innobase/lock/ 目录下。
- 锁对象结构 (
lock_t): 代表一个事务对一个资源加的锁。包含事务指针、锁类型、模式、哈希指针等。 - 锁系统结构 (
lock_sys_t): 全局锁管理器。最重要的是rec_hash,一个哈希表,用于根据(space_id, page_no)快速找到页上的所有锁。 - 加锁流程:
- 调用入口:
lock_rec_lock(行锁) 或lock_table_lock(表锁)。 - 冲突检查:通过锁模式兼容性矩阵检查当前请求的锁与现有锁是否兼容。
- 无冲突:创建
lock_t对象,加入到对应记录的锁队列中。 - 有冲突:创建锁对象,但设置为等待状态 (
LOCK_WAIT),并启动死锁检测。
- 调用入口:
- 死锁检测 (
lock_deadlock_check): 以请求锁的事务为起点,深度优先遍历(DFS)等待图。如果发现环,则触发回滚。
总结表
| 锁类型 | 英文名 | 粒度 | 主要用途 | 兼容性 | 备注 |
|---|---|---|---|---|---|
| 共享锁 (S) | Shared Lock | 行/表 | 读取,防止被修改 | 与 S 兼容,与 X 不兼容 | SELECT ... FOR SHARE |
| 排他锁 (X) | Exclusive Lock | 行/表 | 写入,防止任何其他操作 | 与任何锁都不兼容 | DML 语句默认持有,SELECT ... FOR UPDATE |
| 意向共享锁 (IS) | Intention Shared Lock | 表 | 表明事务即将对某些行加 S 锁 | 与 IX, IS, S 兼容 | 信号锁,用于快速判断表锁是否可行 |
| 意向排他锁 (IX) | Intention Exclusive Lock | 表 | 表明事务即将对某些行加 X 锁 | 与 IS, IX 兼容 | 信号锁,用于快速判断表锁是否可行 |
| 记录锁 | Record Lock | 行 | 锁定索引中的一条具体记录 | 行锁的基础 | |
| 间隙锁 | Gap Lock | 行(间隙) | 锁定一个区间,防止插入,解决幻读 | RC 隔离级别下禁用 | |
| 临键锁 | Next-Key Lock | 行(记录+间隙) | 默认行锁模式,锁记录+间隙,彻底解决幻读 | RR 隔离级别的默认算法 | |
| 插入意向锁 | Insert Intention Lock | 行(间隙) | 优化在相同间隙内的并发插入 | 互相兼容(如果位置不冲突) | 特殊的间隙锁 |
锁源码详解
1. 共享锁(S锁)与排他锁(X锁)
共享锁(S Lock):
// 源码: storage/innobase/lock/lock0lock.cc
/* 共享锁特性 */
#define LOCK_S_SHARED 1 /* 共享读锁 */
#define LOCK_S_ALLOW_READ /* 允许其他事务读 */
#define LOCK_S_BLOCK_WRITE /* 阻塞其他事务写 */
排他锁(X Lock):
// 源码: storage/innobase/lock/lock0lock.cc
/* 排他锁特性 */
#define LOCK_X_EXCLUSIVE 2 /* 独占写锁 */
#define LOCK_X_BLOCK_READ /* 阻塞其他事务读 */
#define LOCK_X_BLOCK_WRITE /* 阻塞其他事务写 */
兼容性矩阵:
| 当前锁 \ 请求锁 | S锁 | X锁 |
|---|---|---|
| S锁 | ✓ | ✗ |
| X锁 | ✗ | ✗ |
2. 意向锁(Intention Locks)
意向锁是表级锁,用于快速判断表中是否有行被锁定:
// 源码: storage/innobase/include/lock0types.h
typedef enum {
LOCK_IS = 4, /* 意向共享锁 */
LOCK_IX = 5, /* 意向排他锁 */
LOCK_S = 6, /* 共享锁 */
LOCK_X = 7, /* 排他锁 */
/* ... */
} lock_mode_t;
意向锁兼容矩阵:
| 当前锁 \ 请求锁 | IS | IX | S | X |
|---|---|---|---|---|
| IS | ✓ | ✓ | ✓ | ✗ |
| IX | ✓ | ✓ | ✗ | ✗ |
| S | ✓ | ✗ | ✓ | ✗ |
| X | ✗ | ✗ | ✗ | ✗ |
3. 行级锁的具体实现
(1) 记录锁(Record Lock)
锁定索引中的单条记录:
-- 锁定id=1的记录
SELECT * FROM table WHERE id = 1 FOR UPDATE;
源码实现:
// storage/innobase/lock/lock0lock.cc
lock_rec_lock(...) {
/* 创建记录锁 */
lock_rec_add_to_queue(
LOCK_REC | LOCK_X, /* 记录排他锁 */
block, heap_no, index, thr);
}
(2) 间隙锁(Gap Lock)
锁定索引记录间的间隙,防止幻读:
-- 锁定10到20之间的间隙
SELECT * FROM table WHERE id BETWEEN 10 AND 20 FOR UPDATE;
源码实现:
// storage/innobase/lock/lock0lock.cc
lock_rec_lock(...) {
if (gap_lock_needed) {
/* 创建间隙锁 */
lock_rec_add_to_queue(
LOCK_GAP | LOCK_X, /* 间隙排他锁 */
block, heap_no, index, thr);
}
}
(3) 临键锁(Next-Key Lock)
记录锁 + 间隙锁的组合(默认行锁模式):
// storage/innobase/include/lock0types.h
#define LOCK_ORDINARY 0 /* 临键锁 */
(4) 插入意向锁(Insert Intention Lock)
特殊的间隙锁,优化并发插入:
// storage/innobase/lock/lock0lock.cc
lock_rec_insert_intention(...) {
/* 创建插入意向锁 */
lock_rec_add_to_queue(
LOCK_GAP | LOCK_INSERT_INTENTION,
block, heap_no, index, thr);
}
锁的内存结构与管理系统
1. 锁对象结构
// storage/innobase/include/lock0priv.h
struct lock_t {
trx_t* trx; /* 所属事务 */
lock_t* hash_next; /* 哈希链指针 */
dict_index_t* index; /* 索引 */
lock_type_t type; /* 锁类型 */
/* ... */
};
// 锁表结构
struct lock_sys_t {
hash_table_t* rec_hash; /* 行锁哈希表 */
/* ... */
};
2. 锁等待机制
// storage/innobase/lock/lock0wait.cc
lock_wait_suspend_thread(...) {
/* 线程挂起等待锁 */
thd_wait_begin(thd, THD_WAIT_ROW_LOCK);
os_event_wait(lock->event);
thd_wait_end(thd);
}
死锁检测与处理
1. 死锁条件
- 互斥条件:资源独占
- 请求与保持:持有锁并请求新锁
- 不剥夺条件:锁只能自愿释放
- 循环等待:等待环路形成
2. 死锁检测算法
// storage/innobase/lock/lock0lock.cc
lock_deadlock_check(...) {
/* 使用等待图(Wait-for Graph)检测 */
if (lock_deadlock_bfs(trx)) {
/* 发现死锁,选择代价小的事务回滚 */
trx_rollback_to_savepoint( victim_trx );
}
}
检测流程:
- 构建等待图(事务为节点,锁等待为边)
- 深度优先搜索检测环路
- 选择回滚代价最小的事务
3. 死锁避免策略
-- 设置锁超时
SET innodb_lock_wait_timeout = 50;
-- 按固定顺序访问表
-- 事务1: A → B → C
-- 事务2: A → B → C (而非 C → B → A)
锁退化机制
1. 定义与原理
锁退化(Lock Degradation)是指从高粒度锁退化为低粒度锁的过程,如从表锁退化为行锁。
源码实现:
// storage/innobase/lock/lock0lock.cc
lock_rec_convert_to_gap(...) {
/* 将记录锁转换为间隙锁 */
lock_rec_remove_from_queue(lock);
lock_rec_add_to_queue(LOCK_GAP, ...);
}
2. 触发条件
- 精确锁定范围可确定时
- 索引可用性变化时
- 统计信息更新后
3. 性能优化意义
- 减少不必要的锁定范围
- 提高系统并发性能
- 降低锁争用和死锁概率
锁监控与诊断
1. 性能模式监控
-- 查看当前锁信息
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
-- 查看锁等待链
SELECT * FROM sys.innodb_lock_waits;
2. InnoDB状态监控
SHOW ENGINE INNODB STATUS\G
-- 关注 LATEST DETECTED DEADLOCK 和 TRANSACTIONS 部分
3. 关键性能指标
-- 锁等待统计
SELECT * FROM sys.metrics
WHERE VARIABLE_NAME LIKE '%lock%wait%';
-- 死锁次数
SELECT * FROM information_schema.INNODB_METRICS
WHERE NAME = 'lock_deadlocks';
最佳实践与优化建议
1. 索引设计优化
-- 确保查询使用索引,避免全表锁
ALTER TABLE orders ADD INDEX idx_customer (customer_id);
-- 使用覆盖索引减少回表锁
CREATE INDEX idx_covering ON orders (status, created_date)
INCLUDE (amount, customer_id);
2. 事务设计原则
-- 保持事务短小
START TRANSACTION;
-- 最小化锁持有时间
UPDATE ... WHERE id = 1;
COMMIT;
-- 避免长事务
SET SESSION max_execution_time = 5000; -- 5秒超时
3. 隔离级别选择
-- 读多写少场景
SET SESSION transaction_isolation = 'READ-COMMITTED';
-- 需要避免幻读
SET SESSION transaction_isolation = 'REPEATABLE-READ';
源码级调优参数
# InnoDB锁相关配置
innodb_lock_wait_timeout=50 # 锁等待超时(秒)
innodb_deadlock_detect=ON # 死锁检测开关
innodb_print_all_deadlocks=OFF # 死锁日志打印
# 锁系统内存配置
innodb_buffer_pool_size=8G # 缓冲池大小
innodb_lru_scan_depth=1024 # LRU扫描深度
MySQL的锁机制是一个复杂而精密的系统,深入理解其原理和实现对于构建高性能、高可用的数据库应用至关重要。通过合理的索引设计、事务控制和系统调优,可以最大限度地发挥MySQL的并发性能优势。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120407

浙公网安备 33010602011771号