MySQL 锁机制与死锁分析深度解析
锁是数据库并发控制的核心。本文深入剖析 InnoDB 的锁类型体系、Next-Key Lock 的加锁规则、意向锁与表锁的关系、死锁产生的原因与排查方法,以及生产环境的锁优化实战技巧。
一、InnoDB 锁概述
1.1 为什么需要锁
解决方式
并发问题
并发冲突
事务A: 读取 x=100
事务B: 读取 x=100
事务A: x=100+50
事务B: x=100+100
事务A: 提交 x=150
事务B: 提交 x=200
最终 x=200 (错误!)
锁机制
排他锁 (X)
共享锁 (S)
保证数据一致性
1.2 InnoDB 锁分类全景
锁类型
特殊锁
意向锁 (IX/IS)
自增锁 (AUTO-INC)
临键锁
按算法
Record Lock
Gap Lock
Next-Key Lock
Insert Intention Lock
按模式
共享锁 (S)
排他锁 (X)
按作用域
行级锁
表级锁
InnoDB 锁
1.3 锁兼容性矩阵
兼容结论
X 与任何锁都不兼容
S 与 S 兼容
IX 与 IS 兼容
IX 与 S 互斥
锁兼容性
X
S
IX
IS
X
S
IX
IS
X
✅
X
✅
X
S
IX
IS
❌
❌
❌
❌
❌
✅
❌
✅
❌
❌
❌
❌
❌
❌
❌
✅
二、行级锁
2.1 Record Lock(记录锁)
记录锁锁住索引记录本身:
示例
SELECT * FROM t WHERE id = 5 FOR UPDATE
锁住 id=5 这条记录
其他事务无法修改 id=5
但可以修改其他记录
Record Lock
索引记录
Record Lock
锁住 id=5 的记录
2.2 Gap Lock(间隙锁)
间隙锁锁住索引记录之间的间隙:
作用
防止幻读
阻止其他事务插入新记录
锁定间隙内的插入
Gap Lock
索引值: 1, 5, 10, 15
Gap Lock 锁住 (1, 5) 间隙
Gap Lock 锁住 (5, 10) 间隙
Gap Lock 锁住 (10, 15) 间隙
Gap Lock 锁住 (15, +∞) 间隙
2.3 Next-Key Lock
Next-Key Lock = Record Lock + Gap Lock:
示例
索引值: 1, 5, 10
WHERE id >= 5 FOR UPDATE
锁住 [5, 10) 范围
Record Lock on id=5
Gap Lock on (5, 10)
组合结构
Next-Key Lock
Record Lock
Gap Lock
锁住索引记录本身
锁住索引记录之间的间隙
2.4 Next-Key Lock 示例详解
sql
-- 假设索引有值: 1, 5, 10, 15
-- 执行语句: SELECT * FROM t WHERE id >= 5 FOR UPDATE;
-- Next-Key Lock 锁住的区间:
-- 1. 对于 id=5: Next-Key Lock on [5, 10)
-- - Record Lock on id=5
-- - Gap Lock on (5, 10)
-- 2. 对于 id=10: Next-Key Lock on [10, 15)
-- - Record Lock on id=10
-- - Gap Lock on (10, 15)
-- 3. 对于 id=15: Next-Key Lock on [15, +∞)
-- - Record Lock on id=15
-- - Gap Lock on (15, +∞)
2.5 唯一索引的特殊性
对比
普通索引: 锁 (pre, 5] + (5, next)
范围更大
更容易死锁
示例
SELECT * FROM t WHERE id = 5 FOR UPDATE
假设 id 是唯一索引
只锁 id=5 这条记录
不锁 (5, next) 间隙
等值查询优化
唯一索引 + 等值查询
只锁住记录本身
不锁间隙
三、表级锁
3.1 表锁
不推荐原因
粒度太粗
并发能力差
容易造成锁等待
InnoDB 推荐使用行级锁
表锁类型
LOCK TABLES ... READ
表级共享锁
其他事务可读不可写
LOCK TABLES ... WRITE
表级排他锁
阻塞其他事务读写
3.2 意向锁
意向锁 用于协调行级锁 和表级锁:
作用
遍历检查每行
检查 IX/IS
检查表锁时
是否有行级锁?
效率低
O(1) 判断
快速判断是否可以加表锁
意向锁类型
IX: 意图排他锁
表示事务将在某行加 X 锁
IS: 意图共享锁
表示事务将在某行加 S 锁
3.3 意向锁获取顺序
示例
事务A: SELECT * FROM t WHERE id=5 FOR UPDATE
获取行级 X 锁
获取表级 IX
其他事务无法获取表级 X
其他事务可以获取表级 S
加锁流程
有
无
有
无
请求表级 S 锁
检查 IX?
❌ 阻塞
检查 X?
❌ 阻塞
✅ 获取 IS
3.4 意向锁兼容性
理解
IX 与 S 互斥
因为 IX 意味着要加 X 锁
X 与 S 互斥
兼容性矩阵
| IS | IX | S | X
IS | ✅ | ✅ | ✅ | ❌
IX | ✅ | ✅ | ❌ | ❌
S | ✅ | ❌ | ✅ | ❌
X | ❌ | ❌ | ❌ | ❌
四、自增锁
4.1 AUTO-INC 锁
配置参数
innodb_autoinc_lock_mode
0: 传统 (表级锁)
1: 连续 (默认)
2: 交错 (性能最好)
AUTO-INC 锁特性
表级自增锁
插入时自动获取
插入完成后释放
确保自增 ID 连续
4.2 三种自增锁模式
innodb_autoinc_lock_mode = 2
交错锁模式
所有 INSERT 使用互斥量
性能最好
不保证自增ID连续
主从复制可能有问题
innodb_autoinc_lock_mode = 1
连续锁模式
简单 INSERT 使用互斥量
批量 INSERT 使用 AUTO-INC 锁
保证自增ID连续
innodb_autoinc_lock_mode = 0
传统锁模式
使用 AUTO-INC 锁
表级锁,效率低
语句执行完后释放
四·续、锁的内存结构与 MDL 锁
4.3 锁的内存结构
Record Lock 内存结构
lock_rec_t
rec_id: 页号 + 记录号
n_bits: 位图大小
bits[]: 记录锁状态
每个 bit 对应一条记录
InnoDB 锁对象结构
lock_t 结构
lock_type_t type
锁类型 (RECORD/TABLE)
ibool impl_trx
持有锁的事务
dict_table_t *tab_def
锁关联的表
lock_rec_t rec_lock
行锁信息
ulint space, page_no, n_bits
表空间、页号、位图
4.4 锁信息查询详解
sql
-- 查看当前所有锁
SELECT
ENGINE_LOCK_ID,
ENGINE_TRANSACTION_ID,
THREAD_ID,
EVENT_ID,
OBJECT_SCHEMA,
OBJECT_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_STATUS,
LOCK_DATA
FROM performance_schema.data_locks
ORDER BY ENGINE_TRANSACTION_ID;
-- 查看锁等待关系
SELECT
REQUESTING_THAPHREAD_ID,
REQUESTING_ENGINE_LOCK_ID,
REQUESTING_ENGINE_TRANSACTION_ID,
BLOCKING_ENGINE_LOCK_ID,
BLOCKING_ENGINE_TRANSACTION_ID
FROM performance_schema.data_lock_waits;
-- 查看具体锁模式
SELECT
OBJECT_NAME,
INDEX_NAME,
LOCK_MODE,
LOCK_STATUS,
LOCK_DATA
FROM performance_schema.data_locks
WHERE OBJECT_NAME = 'orders';
4.5 MDL 锁(元数据锁)
MDL 锁获取场景
MDL_EXCLUSIVE 获取场景
ALTER TABLE
DROP TABLE
RENAME TABLE
ANALYZE TABLE
MDL 锁类型
MDL 锁
MDL_SHARED: 只读访问
MDL_EXCLUSIVE: 修改表结构
MDL_INTENTION_EXCLUSIVE
MDL_SHARED_READ_ONLY
MDL_SHARED_WRITE_LOW_PRIO
4.6 MDL 锁问题排查
sql
-- 查看 MDL 锁等待
SELECT
THREAD_ID,
PROCESSLIST_ID,
PROCESSLIST_USER,
PROCESSLIST_HOST,
PROCESSLIST_DB,
PROCESSLIST_COMMAND,
PROCESSLIST_STATE,
PROCESSLIST_INFO
FROM performance_schema.THREADS
WHERE PROCESSLIST_COMMAND != 'Sleep';
-- 查看 MDL 锁详情
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
COLUMN_NAME,
COLUMN_COUNT,
STORAGE,
ENGINE,
TABLE_ID,
PARTITION_NAME,
SUBPARTITION_NAME,
INDEX_NAME,
SCOPE
FROM performance_schema.METADATA_LOCKS;
-- 查看 MDL 锁等待
SELECT
REQUESTING_THREAD_ID,
REQUESTING_EVENT_ID,
REQUESTING_OBJECT_SCHEMA,
REQUESTING_OBJECT_NAME,
BLOCKING_THREAD_ID,
BLOCKING_EVENT_ID,
BLOCKING_OBJECT_SCHEMA,
BLOCKING_OBJECT_NAME,
BLOCKING_METADATA_LOCK_STATUS
FROM performance_schema.METADATA_LOCK_WAITS;
4.7 常见 MDL 锁问题
sql
-- 问题1: 大查询阻塞 DDL
-- 会话1: 执行大查询
SELECT * FROM orders INTO OUTFILE '/tmp/orders.csv';
-- 会话2: 尝试修改表结构 (阻塞)
ALTER TABLE orders ADD COLUMN remark VARCHAR(500);
-- 会话3: 尝试插入数据 (阻塞)
INSERT INTO orders VALUES (...);
-- 解决方案: 使用 pt-online-schema-change
-- 问题2: 长事务阻塞 DDL
-- 会话1: 开启事务
BEGIN;
SELECT * FROM orders LIMIT 1;
-- 会话2: 尝试 DDL (获取 MDL 锁失败)
ALTER TABLE orders ADD COLUMN remark VARCHAR(500);
-- 解决方案: 设置 lock_wait_timeout
SET SESSION lock_wait_timeout = 10;
四·续续、锁升级与锁优化
4.8 锁升级机制
锁升级影响
锁升级的影响
并发能力下降
死锁可能性降低
锁等待时间可能增加
锁升级过程
行锁 -> 表锁
统计 UPDATE/DELETE 影响的记录数
如果 > 阈值,升级为表锁
避免锁结构过多
锁升级条件
锁升级触发
innodb_table_locks = 1 (默认)
UPDATE/DELETE 影响多行
超过阈值 (默认 2000 行)
4.9 锁优化最佳实践
监控指标
关键监控指标
lock_wait_timeout
innodb_row_lock_time_avg
lock_row_lock_current_count
InnoDB row locks
具体措施
优化措施
- 使用主键或唯一索引查询
- 避免范围查询加锁
- 业务层控制并发
- 合理设计索引
- 拆分大事务
优化原则
锁优化核心
减少锁持有时间
降低锁粒度
避免锁冲突
4.10 乐观锁实现
java
// 基于版本号的乐观锁
@Entity
@Table(name = "accounts")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal balance;
@Version
private Long version; // 版本字段
}
// Service 实现
@Service
public class AccountService {
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 重试机制
int retry = 3;
while (retry > 0) {
try {
Account from = accountRepository.findById(fromId)
.orElseThrow(() -> new RuntimeException("账户不存在"));
Account to = accountRepository.findById(toId)
.orElseThrow(() -> new RuntimeException("账户不存在"));
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
break; // 成功则退出
} catch (OptimisticLockException e) {
retry--;
if (retry == 0) throw e;
// 等待后重试
Thread.sleep(100);
}
}
}
}
五、死锁原理
5.1 死锁的定义
死锁四要素
互斥: 资源只能被一个事务持有
持有并等待: 持有资源同时等待其他资源
不抢占: 事务不能抢别人的资源
循环等待: 形成资源等待环
死锁形成
事务A 持有锁1,等待锁2
事务B 持有锁2,等待锁1
形成循环等待
双方都无法继续
死锁
5.2 死锁产生场景
sql
-- 场景1: 不同顺序更新多行
-- 事务A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 锁住 id=1
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待 id=2
-- 事务B
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 2; -- 锁住 id=2
UPDATE accounts SET balance = balance + 100 WHERE id = 1; -- 等待 id=1
-- 死锁!
-- 场景2: 索引范围锁
-- 事务A
SELECT * FROM orders WHERE amount > 100 FOR UPDATE;
-- 锁住 (100, +∞) 范围
-- 事务B
INSERT INTO orders VALUES (200, ...);
-- 被阻塞
-- 事务A 回滚后,事务B 可能与事务C 死锁
5.3 死锁时序图
事务管理器 事务B 事务A 事务管理器 事务B 事务A 锁住 id=1 锁住 id=2 事务A 成功 BEGIN BEGIN UPDATE WHERE id=1 (获取X锁1) UPDATE WHERE id=2 (获取X锁2) UPDATE WHERE id=2 (等待X锁2) UPDATE WHERE id=1 (等待X锁1) 检测到死锁 发送 ROLLBACK 继续执行 COMMIT
5.4 InnoDB 死锁处理
处理策略
回滚代价最小的事务
基于 undo log 数量判断
undo 少的事务先回滚
检测机制
等待图 (Wait-For Graph)
节点: 事务
边: 等待关系
检测到环 -> 死锁
六、死锁排查与解决
6.1 查看死锁信息
sql
-- 查看当前锁等待
SHOW ENGINE INNODB STATUS;
-- 输出示例
---TRANSACTION 12345, ACTIVE 5 sec
-- mysql tables in use 1, locked 1
-- LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
-- MySQL thread id 123, OS thread handle 0x7f8a, query id 456
-- UPDATE accounts SET balance = balance - 100 WHERE id = 2
-- Waiting for lock at gap of index `idx_id` of table `test`.`accounts`
-- LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
-- locks gap after rec in index `idx_id` of table `test`.`accounts`
6.2 死锁日志分析
sql
-- 查看锁信息
SELECT
trx_id,
trx_state,
trx_started,
trx_rows_locked,
trx_rows_modified,
trx_wait_for
FROM information_schema.INNODB_TRX;
-- 查看锁等待关系
SELECT
requesting_trx_id,
blocking_trx_id,
lock_mode,
lock_type,
lock_table,
lock_index,
lock_space,
lock_page,
lock_rec,
lock_data
FROM information_schema.INNODB_LOCK_WAITS;
-- 查看锁详情
SELECT
lock_id,
lock_trx_id,
lock_mode,
lock_type,
lock_table,
lock_index,
lock_space,
lock_page,
lock_rec,
lock_data
FROM information_schema.INNODB_LOCKS;
6.3 避免死锁的策略
预防策略
固定顺序访问
按 ID 顺序更新
避免循环等待
减少锁范围
使用主键查询
避免范围查询
减小事务
及时提交
避免长事务
合理隔离级别
读已提交可减少锁
但需注意幻读
6.4 代码层面优化
java
// 反例: 随机顺序更新
@Transactional
public void transferRandom(int fromId, int toId, BigDecimal amount) {
// 随机顺序,可能死锁
if (Math.random() > 0.5) {
accountMapper.decrease(fromId, amount);
accountMapper.increase(toId, amount);
} else {
accountMapper.decrease(toId, amount);
accountMapper.increase(fromId, amount);
}
}
// 正例: 固定顺序更新
@Transactional
public void transferOrdered(int fromId, int toId, BigDecimal amount) {
// 按 ID 大小固定顺序
int firstId = Math.min(fromId, toId);
int secondId = Math.max(fromId, toId);
if (firstId == fromId) {
accountMapper.decrease(fromId, amount);
accountMapper.increase(toId, amount);
} else {
accountMapper.decrease(toId, amount);
accountMapper.increase(fromId, amount);
}
}
// 更优: 使用乐观锁
@Version
private Long version;
public boolean transferOptimistic(int fromId, int toId, BigDecimal amount) {
// 先扣减,利用乐观锁冲突检测
int affected = accountMapper.decreaseWithVersion(
fromId, amount, version);
if (affected == 0) {
throw new OptimisticLockException("余额已变化");
}
accountMapper.increase(toId, amount);
return true;
}
6.5 SQL 层面优化
sql
-- 反例: 范围查询(锁住大量记录)
UPDATE orders
SET status = 'completed'
WHERE create_time > '2024-01-01' AND create_time < '2024-12-31';
-- 可能锁住数万条记录,死锁风险高
-- 正例1: 主键精准查询
UPDATE orders
SET status = 'completed'
WHERE id = 123;
-- 只锁一条记录
-- 正例2: 分批处理
UPDATE orders
SET status = 'completed'
WHERE id IN (SELECT id FROM orders WHERE status = 'pending' LIMIT 1000);
-- 分批小事务
-- 正例3: 使用低区分度字段
UPDATE orders
SET status = 'completed'
WHERE status = 'pending' AND DATE(create_time) = '2024-06-01';
-- 按小批量日期拆分
七、锁监控与优化
7.1 锁相关参数
sql
-- 查看锁相关配置
SHOW VARIABLES LIKE 'innodb_lock%';
-- innodb_lock_wait_timeout: 锁等待超时时间(默认50秒)
-- innodb_lock_strict_mode: 严格模式,忽略唯一索引空值
-- innodb_print_lock_atomic_bugs: 打印锁相关的原子性bug
7.2 监控慢查询
sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过1秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 查找长时间锁等待
SELECT
trx_mysql_thread_id,
trx_query,
trx_state,
trx_started,
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(trx_started) AS duration
FROM information_schema.INNODB_TRX
WHERE trx_started < NOW() - INTERVAL 10 SECOND;
7.3 性能优化建议
隔离级别
选择合适隔离级别
读已提交可减少锁
但需注意幻读处理
事务优化
减小事务范围
避免长事务
及时提交
查询优化
避免全表扫描
使用主键或索引查询
减少锁覆盖的记录数
索引优化
合理创建索引
减少扫描范围
使用覆盖索引
八、面试高频问题
8.1 InnoDB 行级锁是如何实现的?
InnoDB 行级锁依赖于索引:
1. 锁住索引而非数据行
- 如果 UPDATE 使用主键索引
-> 锁住主键索引
- 如果 UPDATE 使用普通索引
-> 锁住普通索引 + 主键索引
2. 加锁过程
- 定位到第一个满足条件的记录
- 对记录加 Record Lock
- 对记录之间的间隙加 Gap Lock
- 组合为 Next-Key Lock
- 继续扫描,对每条记录加 Next-Key Lock
3. 释放过程
- 按加锁顺序反向释放
- 事务提交时统一释放
8.2 什么是 Next-Key Lock?解决了什么问题?
Next-Key Lock = Record Lock + Gap Lock
解决的问题:幻读
示例:
假设表有数据: id=1, 5, 10
事务A:
SELECT * FROM t WHERE id >= 5; -- 读到 id=5, 10
事务B:
INSERT INTO t VALUES (6); -- 插入 id=6
-- 如果没有 Gap Lock,事务A 再次查询会看到 id=6(幻读)
-- 如果有 Gap Lock,INSERT 会被阻塞或回滚
-- Next-Key Lock 锁住 [5, 10),INSERT INTO (6) 被阻塞
8.3 什么是意向锁?为什么需要意向锁?
意向锁是表级锁,表示事务将在某行加锁:
- IS (Intent Share Lock): 意图加共享锁
- IX (Intent Exclusive Lock): 意图加排他锁
为什么需要:
假设要加表级 X 锁
-> 需要检查是否有行级 X/S 锁
-> 逐行检查效率低 O(n)
使用意向锁:
-> 只需检查表级 IX/IS
-> O(1) 判断
8.4 如何避免死锁?
1. 固定顺序访问资源
- 按主键 ID 排序后操作
- 避免循环等待
2. 减小锁范围
- 使用主键精准查询
- 避免范围查询
3. 使用低隔离级别
- READ COMMITTED 减少 Gap Lock
- 但需要注意幻读
4. 使用乐观锁
- 通过版本号控制并发
- 减少锁冲突
5. 监控和超时处理
- 设置合理的锁等待超时
- 及时发现和处理死锁
8.5 死锁和锁等待的区别?
┌─────────────────────────────────────────────────────────────┐
│ 锁等待 │
├─────────────────────────────────────────────────────────────┤
│ 事务A 持有锁,等待事务B 释放锁 │
│ 事务B 持有锁,等待事务C 释放锁 │
│ 事务C 持有锁,等待事务A 释放锁 │
│ -> 形成等待链,但还没有死锁 │
│ -> 正常情况,设置超时时间即可 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 死锁 │
├─────────────────────────────────────────────────────────────┤
│ 事务A 持有锁1,等待锁2 │
│ 事务B 持有锁2,等待锁1 │
│ -> 形成循环等待 │
│ -> InnoDB 检测到死锁后,选择代价小的事务回滚 │
└─────────────────────────────────────────────────────────────┘
区别:
- 锁等待: A等B,可能最终A或B完成
- 死锁: A等B,B等A,无法自行解除,需要回滚
九、总结
9.1 锁类型总结
表级锁
LOCK TABLES
意向锁 (IX/IS)
AUTO-INC 锁
行级锁
Record Lock
Gap Lock
Next-Key Lock
9.2 死锁处理流程
死锁处理流程:
1. 检测死锁
- InnoDB 使用 Wait-For Graph
- 检测到环则死锁
2. 选择回滚事务
- 基于 undo log 数量
- 选择代价小的事务回滚
3. 回滚并通知
- 发送错误给客户端
- 错误码: 1213 Deadlock found
4. 业务处理
- 业务层捕获异常
- 重试或人工处理
9.3 最佳实践
锁优化最佳实践:
1. 索引设计
✅ 使用主键或索引查询
✅ 创建合适的索引减少扫描范围
2. SQL 编写
✅ 使用精准查询
❌ 避免范围查询 FOR UPDATE
3. 事务设计
✅ 事务尽量短小
✅ 避免长事务
✅ 固定顺序访问资源
4. 隔离级别
✅ 根据业务选择合适隔离级别
✅ 读多写少可考虑 RC
5. 监控
✅ 监控锁等待时间
✅ 分析慢查询日志
✅ 定期检查死锁日志