核心概念
锁是数据库实现并发控制的核心机制,用于保证多个事务并发访问数据时的正确性和一致性。MySQL 中的锁可以从不同维度进行分类:
锁的粒度分类
| 锁类型 | 锁定范围 | 并发度 | 适用场景 |
|---|---|---|---|
| 全局锁 | 整个实例 | 最低 | 全库逻辑备份 |
| 表级锁 | 整张表 | 较低 | DDL 操作、MyISAM 引擎 |
| 行级锁 | 单行记录 | 最高 | InnoDB 引擎、高并发场景 |
锁的行为分类
- 共享锁(S 锁):读锁,允许其他事务读取已锁定的资源
- 排他锁(X 锁):写锁,阻止其他事务读取或写入已锁定的资源
锁的状态分类
- 意向锁:表级锁,表示事务希望在行上获取什么类型的锁
- 实际锁:真正作用在数据上的锁(行锁、表锁)
锁机制底层原理
InnoDB 锁的实现方式
InnoDB 的行锁是通过索引项实现的,而不是锁住实际的物理行。这意味着:
-- 如果 where 条件没有使用索引,InnoDB 会退化为表锁
SELECT * FROM users WHERE name = 'john'; -- name 无索引,锁全表
-- 使用主键查询,只锁单行
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 只锁 id=1 的行
锁的存储结构
InnoDB 将锁信息存储在内存的锁结构中,主要包括:
lock_t (锁结构)
├── lock_heap_t (堆锁信息)
├── lock_rec_t (记录锁)
└── lock_gap_t (间隙锁)
每个锁记录包含:
- 事务 ID:持有锁的事务
- 锁类型:S 锁/X 锁/间隙锁等
- 锁模式:等待/已获取
- 索引信息:锁定的是哪个索引记录
各种锁类型详解
1. 记录锁(Record Lock)
记录锁是最基本的行锁,锁定索引记录本身。
-- 示例:锁定单条记录
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 此时其他事务无法修改 id=1 的记录
COMMIT;
特点:
- 只锁住索引记录,不锁住间隙
- 必须有索引,否则退化为表锁
- 支持 S 锁和 X 锁两种模式
2. 间隙锁(Gap Lock)
间隙锁锁定索引记录之间的间隙,防止其他事务在间隙中插入新记录。
-- 示例:范围查询产生间隙锁
BEGIN;
SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
-- 锁定 (10, 20) 区间,其他事务无法插入 id=15 的记录
COMMIT;
锁定范围:
- 开区间:(10, 20) 不包含 10 和 20
- 如果 10 和 20 存在,则记录本身也被锁定
- 如果没有记录,锁定整个区间
3. 临键锁(Next-Key Lock)
临键锁 = 记录锁 + 间隙锁,是 InnoDB 的默认锁机制。
Next-Key Lock = Record Lock + Gap Lock
锁定范围:
- 左开右闭区间:(prev, current]
- 既防止修改现有记录,也防止插入新记录
-- 可重复隔离级别下,范围查询使用临键锁
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM users WHERE id > 10 AND id < 20 FOR UPDATE;
-- 锁定区间:(10, 20],包含 20 但不包含 10
COMMIT;
4. 意向锁(Intention Lock)
意向锁是表级锁,用于表明事务希望在行上获取什么类型的锁。
| 意向锁类型 | 含义 | 兼容的表锁 |
|---|---|---|
| 意向共享锁(IS) | 事务打算获取行的 S 锁 | IS, IX |
| 意向排他锁(IX) | 事务打算获取行的 X 锁 | IX |
作用:
- 快速判断表是否可以被锁定
- 避免表锁与行锁冲突检查的开销
-- 执行以下语句会自动添加意向锁
SELECT ... FOR SHARE; -- 添加 IS 锁
SELECT ... FOR UPDATE; -- 添加 IX 锁
5. 自增锁(AUTO-INC Lock)
自增锁是表级锁,用于保证自增列的连续性和唯一性。
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
amount DECIMAL(10, 2)
);
-- 插入时会自动获取自增锁
INSERT INTO orders (user_id, amount) VALUES (1, 100.00);
配置参数:
-- 控制自增锁的分配策略
innodb_autoinc_lock_mode = 0; -- 传统模式(语句级锁)
innodb_autoinc_lock_mode = 1; -- 连续模式(默认)
innodb_autoinc_lock_mode = 2; -- 交错模式(最高并发)
锁的兼容性矩阵
行锁兼容性
| 当前锁\请求锁 | S 锁 | X 锁 |
|---|---|---|
| S 锁 | ✅ 兼容 | ❌ 冲突 |
| X 锁 | ❌ 冲突 | ❌ 冲突 |
意向锁兼容性
| 当前锁\请求锁 | IS | IX | S 表锁 | X 表锁 |
|---|---|---|---|---|
| IS | ✅ | ✅ | ✅ | ❌ |
| IX | ✅ | ✅ | ❌ | ❌ |
| S 表锁 | ✅ | ❌ | ✅ | ❌ |
| X 表锁 | ❌ | ❌ | ❌ | ❌ |
锁的使用场景与示例
场景 1:防止丢失更新
-- 错误示例:不使用锁,可能导致丢失更新
-- 事务 A
SELECT balance FROM accounts WHERE id = 1; -- 余额 1000
-- 事务 B
SELECT balance FROM accounts WHERE id = 1; -- 余额 1000
-- 事务 A
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 900
-- 事务 B
UPDATE accounts SET balance = balance - 200 WHERE id = 1; -- 800(错误!)
-- 正确示例:使用 FOR UPDATE
-- 事务 A
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 1000
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 900
COMMIT;
-- 事务 B(等待 A 提交后)
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 900
UPDATE accounts SET balance = balance - 200 WHERE id = 1; -- 700
COMMIT;
场景 2:批量更新防止死锁
-- 错误示例:不同顺序更新可能导致死锁
-- 事务 A
UPDATE users SET status = 1 WHERE id IN (1, 2, 3);
-- 事务 B
UPDATE users SET status = 1 WHERE id IN (3, 2, 1); -- 可能死锁
-- 正确示例:固定顺序更新
-- 所有事务都按 id 升序更新
UPDATE users SET status = 1 WHERE id IN (1, 2, 3);
场景 3:范围查询的间隙锁
-- 创建测试表
CREATE TABLE test_locks (
id INT PRIMARY KEY,
value INT
);
INSERT INTO test_locks VALUES (1, 10), (5, 50), (10, 100);
-- 事务 A:范围查询
BEGIN;
SELECT * FROM test_locks WHERE id BETWEEN 5 AND 10 FOR UPDATE;
-- 锁定范围:(5, 10],包含 5 和 10
-- 事务 B:尝试插入(会被阻塞)
INSERT INTO test_locks VALUES (7, 70); -- 等待事务 A 提交
-- 事务 B:尝试更新边界(会被阻塞)
UPDATE test_locks SET value = 55 WHERE id = 5; -- 等待
-- 事务 B:更新范围外(可以执行)
UPDATE test_locks SET value = 15 WHERE id = 1; -- 成功
场景 4:唯一索引与普通索引的锁差异
-- 唯一索引查询(等值):只锁记录
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 只锁 id=1
-- 普通索引查询(等值):锁记录 + 间隙
SELECT * FROM users WHERE name = 'john' FOR UPDATE;
-- 锁定所有 name='john' 的记录 + 间隙(防止插入)
-- 范围查询:临键锁
SELECT * FROM users WHERE id > 10 FOR UPDATE;
-- 锁定 (10, +∞) 区间
锁的查看与诊断
查看锁信息
-- 查看锁等待情况(MySQL 5.7+)
SELECT
request_trx_id,
requested_lock_id,
blocking_trx_id,
blocking_lock_id
FROM performance_schema.data_lock_waits;
-- 查看当前持有的锁
SELECT
engine_transaction_id,
lock_table,
lock_index,
lock_type,
lock_mode,
lock_data
FROM performance_schema.data_locks;
-- 查看锁等待(MySQL 8.0+)
SELECT * FROM sys.innodb_lock_waits;
查看事务信息
-- 查看所有事务
SELECT * FROM information_schema.INNODB_TRX;
-- 查看事务详细信息
SELECT
trx_id,
trx_state,
trx_started,
trx_requested_lock_id,
trx_wait_started,
trx_mysql_thread_id,
trx_query
FROM information_schema.INNODB_TRX
WHERE trx_state = 'LOCK WAIT';
查看锁等待链
-- 完整的锁等待分析
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM information_schema.INNODB_LOCK_WAITS w
INNER JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;
锁优化策略
1. 减少锁粒度
-- 优化前:范围大,锁多
UPDATE orders SET status = 1 WHERE create_time > '2024-01-01';
-- 优化后:分批处理,减少锁持有时间
UPDATE orders SET status = 1
WHERE create_time > '2024-01-01' AND id BETWEEN 1 AND 1000;
UPDATE orders SET status = 1
WHERE create_time > '2024-01-01' AND id BETWEEN 1001 AND 2000;
2. 缩短事务时间
-- 错误示例:长事务持有锁时间长
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 执行大量业务逻辑...
-- 发送 HTTP 请求...
-- 写日志...
COMMIT;
-- 正确示例:快速提交
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 再执行其他业务逻辑
3. 避免死锁的技巧
-- 技巧 1:固定访问顺序
-- 所有事务都按 id 升序访问
UPDATE accounts SET balance = balance - 100 WHERE id IN (1, 2, 3);
-- 技巧 2:使用较低隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 技巧 3:添加超时机制
SET innodb_lock_wait_timeout = 10; -- 10 秒超时
4. 合理使用索引
-- 无索引:全表锁
UPDATE users SET status = 1 WHERE email = 'test@example.com';
-- 添加索引:行锁
CREATE INDEX idx_email ON users(email);
UPDATE users SET status = 1 WHERE email = 'test@example.com';
常见陷阱与注意事项
陷阱 1:隐式类型转换导致锁升级
-- phone 是 VARCHAR 类型
-- 错误:数字不加引号,导致类型转换,锁全表
SELECT * FROM users WHERE phone = 13800138000 FOR UPDATE;
-- 正确:加引号,使用索引
SELECT * FROM users WHERE phone = '13800138000' FOR UPDATE;
陷阱 2:函数导致索引失效
-- 错误:对索引列使用函数,锁全表
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01' FOR UPDATE;
-- 正确:使用范围查询
SELECT * FROM orders
WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02' FOR UPDATE;
陷阱 3:间隙锁导致并发度降低
-- 在可重复读隔离级别下
-- 范围查询会锁住整个区间,影响并发
SELECT * FROM users WHERE id > 100 FOR UPDATE;
-- 解决方案 1:使用读已提交隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 解决方案 2:优化为等值查询
SELECT * FROM users WHERE id = 101 FOR UPDATE;
陷阱 4:自增锁导致的性能问题
-- 批量插入时,自增锁会影响并发
INSERT INTO orders (user_id, amount)
SELECT user_id, amount FROM temp_orders;
-- 优化:使用 innodb_autoinc_lock_mode = 2
SET GLOBAL innodb_autoinc_lock_mode = 2;
总结
本文详细介绍了 MySQL 中的各种锁机制:
核心要点
- 锁的粒度:行锁 > 表锁 > 全局锁(并发度从高到低)
- InnoDB 锁实现:基于索引,无索引会退化为表锁
- 锁类型:
- 记录锁:锁定索引记录
- 间隙锁:锁定索引间隙
- 临键锁:记录锁 + 间隙锁(默认)
- 意向锁:表级锁,表示锁定意图
- 锁兼容性:S 锁与 S 锁兼容,其他组合基本冲突
- 死锁预防:固定访问顺序、缩短事务、合理索引
最佳实践
- 尽量使用索引,避免锁升级
- 缩短事务持有锁的时间
- 固定多表访问顺序,避免死锁
- 根据业务需求选择合适的隔离级别
- 定期监控锁等待情况,及时发现性能瓶颈
参考资料
- MySQL 官方文档 - InnoDB 锁
- MySQL 官方文档 - 锁设置
- 《高性能 MySQL》第 7 章:MySQL 高级特性
- 《MySQL 技术内幕:InnoDB 存储引擎》第 6 章:锁