MySQL InnoDB的锁机制是高并发场景下数据一致性的基础保障。InnoDB支持行级锁,但在不同隔离级别和查询条件下,锁的行为差异显著。间隙锁(Gap Lock)和临键锁(Next-Key Lock)是RR隔离级别特有的机制,用于防止幻读,但也是死锁高发区。理解锁的类型、加锁规则和排查方法是数据库运维和开发的核心能力。
InnoDB锁类型与加锁规则
InnoDB的行锁分为两种:
– 共享锁(S Lock):SELECT … LOCK IN SHARE MODE
– 排他锁(X Lock):SELECT … FOR UPDATE, INSERT, UPDATE, DELETE
此外还有表级意向锁(IS/IX),用于快速判断表中是否有行锁,不阻塞行级操作。
加锁的基本规则取决于隔离级别和查询条件:
-- 准备测试数据
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
order_no VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
status VARCHAR(20),
created_at DATETIME
);
CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_status ON orders(status);
INSERT INTO orders VALUES
(1, 'ORD001', 1001, 99.00, 'paid', '2026-08-01 10:00:00'),
(5, 'ORD005', 1002, 199.00, 'paid', '2026-08-02 10:00:00'),
(10, 'ORD010', 1003, 299.00, 'pending', '2026-08-03 10:00:00'),
(15, 'ORD015', 1004, 399.00, 'shipped', '2026-08-04 10:00:00'),
(20, 'ORD020', 1005, 499.00, 'cancelled', '2026-08-05 10:00:00');
RR隔离级别下的临键锁与间隙锁
InnoDB默认隔离级别是REPEATABLE READ,使用Next-Key Lock(临键锁)防止幻读。Next-Key Lock是行锁+间隙锁的组合,锁定一个左开右闭区间。
以idx_user_id索引为例,当前数据为1001, 1002, 1003, 1004, 1005:
-- 事务A
BEGIN;
SELECT * FROM orders WHERE user_id = 1003 FOR UPDATE;
-- 锁定的是idx_user_id上的Next-Key Lock: (1002, 1003]
等值查询命中记录时,临键锁退化为行锁(Record Lock),只锁定命中的那条记录。但如果查询条件是唯一索引且命中,则只加记录锁不加间隙锁。
范围查询的加锁规则:
-- 事务A
BEGIN;
SELECT * FROM orders WHERE user_id > 1002 AND user_id < 1005 FOR UPDATE;
-- 锁定区间: (1002, 1003], (1003, 1004], (1004, 1005)
-- 即: user_id在(1002, 1005)范围内的所有间隙和记录
这个锁定阻止其他事务在user_id (1002, 1005)之间插入任何新记录。
不同索引类型对锁范围的影响
索引类型直接影响锁范围,这是死锁分析的关键:
-- 场景1:通过主键等值查询
SELECT * FROM orders WHERE id = 10 FOR UPDATE;
-- 主键是唯一索引,命中记录只加Record Lock: {10}
-- 场景2:通过主键范围查询
SELECT * FROM orders WHERE id > 5 AND id < 15 FOR UPDATE;
-- 加Next-Key Lock: (5, 10], (10, 15]
-- 场景3:通过非唯一索引等值查询
SELECT * FROM orders WHERE user_id = 1003 FOR UPDATE;
-- 非唯一索引,加Next-Key Lock: (1002, 1003]
-- 同时加下一个间隙锁: (1003, 1004)
-- 因为非唯一索引可能有重复值,需要锁定两侧间隙
-- 场景4:无索引查询
SELECT * FROM orders WHERE amount = 199.00 FOR UPDATE;
-- amount列无索引,全表扫描
-- 对所有记录加Next-Key Lock,效果等同于锁表
场景4是生产环境最危险的锁行为。无索引查询的FOR UPDATE会锁定全表所有记录和间隙,导致所有写操作阻塞。务必确保WHERE条件命中索引。
典型死锁场景与复现
间隙锁导致的死锁是最常见类型。以下场景在并发插入时高频出现:
-- 初始数据: user_id有1001, 1003, 1005三条记录
-- 事务A
BEGIN;
SELECT * FROM orders WHERE user_id = 1003 FOR UPDATE;
-- 加锁: idx_user_id (1001, 1003] + (1003, 1005)
-- 事务B(并发)
BEGIN;
SELECT * FROM orders WHERE user_id = 1005 FOR UPDATE;
-- 加锁: idx_user_id (1003, 1005] + (1005, +∞)
-- 成功获取锁
-- 事务A继续
INSERT INTO orders (id, order_no, user_id, amount, status, created_at)
VALUES (8, 'ORD008', 1004, 150.00, 'pending', NOW());
-- 尝试插入user_id=1004,落在间隙(1003, 1005)
-- 被事务B的间隙锁(1003, 1005)阻塞 → 等待
-- 事务B继续
INSERT INTO orders (id, order_no, user_id, amount, status, created_at)
VALUES (7, 'ORD007', 1004, 160.00, 'pending', NOW());
-- 尝试插入user_id=1004,落在间隙(1003, 1005)
-- 被事务A的间隙锁(1003, 1005)阻塞 → 等待
-- 死锁!A等B释放间隙锁,B等A释放间隙锁
-- InnoDB检测到死锁,回滚undo量较小的事务
这类死锁的本质是两个事务互相持有对方需要的间隙锁。间隙锁之间不冲突(多个事务可同时持有同一间隙锁),但间隙锁阻塞INSERT。
死锁排查与诊断工具
开启死锁日志记录:
-- 查看死锁检测是否开启
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
-- 默认ON,死锁时自动回滚一个事务
-- 查看死锁日志输出方式
SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';
-- OFF: 只在SHOW ENGINE INNODB STATUS中显示最后一个死锁
-- ON: 所有死锁记录到error log
-- 临时开启
SET GLOBAL innodb_print_all_deadlocks = ON;
查看最近一次死锁信息:
SHOW ENGINE INNODB STATUS\G
死锁日志的关键部分:
========================
LATEST DETECTED DEADLOCK
========================
2026-08-17 10:30:45 0x7f8a3c2b7000
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 3 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 10, OS thread handle 1402345678, query id 100
UPDATE orders SET status = 'paid' WHERE user_id = 1003
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 3 n bits 72 index idx_user_id of table `test`.`orders`
trx id 12345 lock_mode X locks gap before rec
Insert intention waiting
*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 2 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 11, OS thread handle 1402345679, query id 101
INSERT INTO orders (id, order_no, user_id, ...) VALUES (8, 'ORD008', 1004, ...)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 50 page no 3 n bits 72 index idx_user_id of table `test`.`orders`
trx id 12346 lock_mode X
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 3 n bits 72 index idx_user_id of table `test`.`orders`
trx id 12346 lock_mode X locks gap before rec
Insert intention waiting
*** WE ROLL BACK TRANSACTION (2)
日志解读:
– lock_mode X locks gap before rec:间隙锁
– Insert intention waiting:插入意向锁等待,表明INSERT被间隙锁阻塞
– HOLDS THE LOCK(S):该事务持有的锁
– WAITING FOR THIS LOCK TO BE GRANTED:该事务等待的锁
– WE ROLL BACK TRANSACTION (2):回滚了事务2(undo log较小的一方)
通过performance_schema分析当前锁状态
MySQL 8.0使用performance_schema查看锁信息:
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query,
TIMEDIFF(NOW(), b.trx_started) AS blocking_duration
FROM performance_schema.data_lock_waits w
INNER JOIN information_schema.innodb_trx b
ON b.trx_id = w.blocking_engine_transaction_id
INNER JOIN information_schema.innodb_trx r
ON r.trx_id = w.requesting_engine_transaction_id;
查看具体锁详情:
SELECT
ENGINE_LOCK_ID,
ENGINE_TRANSACTION_ID,
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_STATUS,
LOCK_DATA
FROM performance_schema.data_locks
WHERE OBJECT_NAME = 'orders'
ORDER BY ENGINE_TRANSACTION_ID;
LOCK_DATA字段显示锁定的具体值或间隙范围。LOCK_MODE为X表示排他记录锁,X,GAP表示间隙锁,X,REC_NOT_GAP表示纯记录锁。
死锁预防与优化策略
1. 降低隔离级别为READ COMMITTED(推荐):
-- RC级别下不存在间隙锁,只加记录锁
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
RC级别下FOR UPDATE只锁定命中记录,不锁间隙。上述死锁场景在RC下不会发生。代价是放弃了RR的防幻读能力,但大多数业务可接受。
2. 固定加锁顺序,避免交叉等待:
-- 不推荐:不同事务按不同条件加锁
-- 事务A: WHERE user_id = 1003 FOR UPDATE → WHERE user_id = 1005 FOR UPDATE
-- 事务B: WHERE user_id = 1005 FOR UPDATE → WHERE user_id = 1003 FOR UPDATE
-- 死锁风险高
-- 推荐:所有事务按id升序加锁
-- 事务A和B都: WHERE id IN (10, 15) FOR UPDATE ORDER BY id
3. 使用INSERT … ON DUPLICATE KEY UPDATE替代先查后插:
-- 不推荐:先SELECT再INSERT,间隙锁风险
SELECT * FROM orders WHERE order_no = 'ORD008';
-- 如果不存在则INSERT
-- 推荐:原子操作
INSERT INTO orders (id, order_no, user_id, amount, status, created_at)
VALUES (8, 'ORD008', 1004, 150.00, 'pending', NOW())
ON DUPLICATE KEY UPDATE amount = VALUES(amount);
4. 大事务拆分为小事务,缩短锁持有时间:
-- 不推荐:一个事务中处理多个订单
BEGIN;
UPDATE orders SET status = 'processing' WHERE id IN (1, 5, 10, 15, 20);
-- 执行其他耗时操作...
UPDATE inventory SET stock = stock - 1 WHERE product_id = 100;
COMMIT;
-- 推荐:拆分为多个短事务
BEGIN;
UPDATE orders SET status = 'processing' WHERE id = 1;
COMMIT;
BEGIN;
UPDATE orders SET status = 'processing' WHERE id = 5;
COMMIT;
5. 合理使用索引,避免全表扫描加锁:
-- 确保WHERE条件的所有列都有索引
-- 特别注意复合索引的最左前缀原则
-- 无索引的FOR UPDATE等于锁表
6. 设置锁超时时间,快速失败而非长时间等待:
SET SESSION innodb_lock_wait_timeout = 5;
SET GLOBAL innodb_lock_wait_timeout = 5;
默认50秒过长,生产环境建议设为3-5秒。锁等待超时后事务回滚,应用层重试,比长时间阻塞更优。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysqlinnodb-xing-suo-ji-zhi-yu-jian-xi-suo-si-suo-pai-cha/