MySQL InnoDB行锁与间隙锁机制详解及死锁排查实战

MySQL InnoDB存储引擎的锁机制是保证并发事务隔离性的核心。行锁、间隙锁、临键锁的组合使用实现了不同级别的隔离,但也带来了死锁风险。在生产环境中,死锁和锁等待是导致数据库性能下降的常见原因。本文系统梳理InnoDB各类锁机制的原理,并给出死锁排查的完整流程。

InnoDB行锁类型与加锁规则

InnoDB的锁从粒度上分为行级锁和表级锁。行级锁分为两种:共享锁(S Lock)和排他锁(X Lock)。表级锁包括意向共享锁(IS)和意向排他锁(IX),用于在不扫描全表的情况下判断是否有行级锁存在。

InnoDB的行锁是加在索引上的,而非数据行本身。如果查询未使用索引(全表扫描),InnoDB会对所有行加锁,等同于表锁。这是生产环境锁冲突的常见原因。

-- 加锁行为验证
-- 表结构: CREATE TABLE orders (id INT PRIMARY KEY, order_no VARCHAR(32), amount DECIMAL(10,2), idx_order_no VARCHAR(32), INDEX idx_order_no(order_no));

-- Session 1: 使用索引的更新,加行锁
BEGIN;
UPDATE orders SET amount = 100 WHERE order_no = 'ORD20260815001';
-- 此时仅锁定order_no='ORD20260815001'对应的索引记录

-- Session 2: 更新不同行,正常执行(不阻塞)
BEGIN;
UPDATE orders SET amount = 200 WHERE order_no = 'ORD20260815002';
-- 正常执行,无锁等待

-- Session 2: 更新同一行,阻塞
BEGIN;
UPDATE orders SET amount = 200 WHERE order_no = 'ORD20260815001';
-- 阻塞,等待Session 1释放行锁

间隙锁与临键锁作用机制

在RR(Repeatable Read)隔离级别下,InnoDB使用间隙锁(Gap Lock)防止幻读。间隙锁锁定索引记录之间的间隙,阻止其他事务在该间隙中插入新记录。

临键锁(Next-Key Lock)是行锁和间隙锁的组合,锁定一个左开右闭区间。InnoDB默认使用Next-Key Lock进行范围查询加锁。

-- 间隙锁示例
-- 假设orders表中id值为: 1, 5, 10, 15, 20

-- Session 1: 范围查询并加锁
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 5 AND 15 FOR UPDATE;
-- 加锁范围: (1, 5], (5, 10], (10, 15], (15, 20) 的临键锁
-- 即锁定id > 1 且 id < 20 范围内的所有间隙

-- Session 2: 尝试在间隙中插入
BEGIN;
INSERT INTO orders (id, order_no, amount) VALUES (8, 'ORD008', 50);
-- 阻塞!被间隙锁(5, 10)阻塞

-- Session 2: 尝试在锁范围外插入
INSERT INTO orders (id, order_no, amount) VALUES (25, 'ORD025', 50);
-- 正常执行,不受间隙锁影响

等值查询的唯一索引列上,InnoDB会将Next-Key Lock退化为行锁(仅锁记录本身)或间隙锁(当记录不存在时)。对于非唯一索引,等值查询仍使用Next-Key Lock,并额外对下一个记录加间隙锁。

-- 唯一索引等值查询的锁退化
-- Session 1: 等值查询存在记录
BEGIN;
SELECT * FROM orders WHERE id = 10 FOR UPDATE;
-- 仅对id=10加行锁,不产生间隙锁

-- Session 2: 可以在id=10相邻间隙插入
INSERT INTO orders (id, order_no, amount) VALUES (8, 'ORD008', 50);
-- 正常执行

-- 非唯一索引等值查询
BEGIN;
SELECT * FROM orders WHERE idx_order_no = 'ORD20260815001' FOR UPDATE;
-- 对idx_order_no='ORD20260815001'加Next-Key Lock
-- 同时对下一个索引记录加间隙锁
-- 即锁住 'ORD20260815001' 到下一个值之间的间隙

死锁产生原因与检测机制

死锁是指两个或多个事务互相持有对方需要的锁,导致所有事务都无法继续。InnoDB使用Wait-For Graph算法检测死锁:当检测到图中存在环时,回滚undo量最小的事务。

-- 经典死锁场景:交叉更新
-- Session 1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 锁定id=1

-- Session 2
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 2;  -- 锁定id=2

-- Session 1
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- 等待id=2的锁

-- Session 2
UPDATE accounts SET balance = balance + 100 WHERE id = 1;  -- 等待id=1的锁
-- ERROR 1213 (40001): Deadlock found when trying to get lock;
-- try restarting transaction

另一个常见死锁场景是间隙锁交叉:

-- 间隙锁导致的死锁
-- 表数据: id = 1, 5, 10

-- Session 1: 在(5,10)间隙插入
BEGIN;
INSERT INTO orders (id, order_no, amount) VALUES (7, 'ORD007', 50);
-- 等待间隙锁(5,10)... 假设Session 2已经持有该间隙锁

-- Session 2: 在(5,10)间隙插入
BEGIN;
INSERT INTO orders (id, order_no, amount) VALUES (8, 'ORD008', 50);
-- 等待间隙锁(5,10)... 两个事务互相等待对方的间隙锁
-- InnoDB检测到死锁,回滚其中一个事务

死锁排查流程与日志分析

InnoDB死锁检测默认开启(innodb_deadlock_detect=ON)。检测到死锁后,被回滚的事务会收到1213错误。要获取详细的死锁信息,需开启死锁日志:

-- 查看死锁日志
SHOW ENGINE INNODB STATUS\G

-- 死锁日志关键输出:
-- *** (1) TRANSACTION:
-- TRANSACTION 12345, ACTIVE 5 sec starting index read
-- mysql tables in use 1, locked 1
-- LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
-- MySQL thread id 10, OS thread handle 140123456789, query id 100 localhost root updating
-- UPDATE accounts SET balance = balance - 100 WHERE id = 2
-- *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
-- RECORD LOCKS space id 50 page no 3 n bits 72 index PRIMARY of table `test`.`accounts`
-- trx id 12345 lock_mode X locks rec but not gap waiting
-- Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; ...
--
-- *** (2) TRANSACTION:
-- TRANSACTION 12346, ACTIVE 3 sec starting index read
-- ...
-- UPDATE accounts SET balance = balance + 100 WHERE id = 1
-- *** (2) HOLDS THE LOCK(S):
-- RECORD LOCKS ... index PRIMARY ... lock_mode X locks rec but not gap
-- *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
-- RECORD LOCKS ... index PRIMARY ... lock_mode X locks rec but not gap waiting
--
-- *** WE ROLL BACK TRANSACTION (1)

分析死锁日志应关注以下几个字段:

  • TRANSACTION ID:事务标识,可用于关联general log或binlog定位SQL语句。
  • lock_mode:锁模式。X locks rec but not gap表示行锁,X locks gap before rec表示间隙锁,X表示临键锁。
  • WAITING FOR / HOLDS:WAITING FOR显示事务正在等待的锁,HOLDS显示事务已持有的锁。交叉对比两个事务的HOLDS和WAITING FOR可确定死锁环。
  • index PRIMARY:锁所在的索引。如果看到锁加在PRIMARY上,说明是主键索引加锁;如果是GEN_CLUST_INDEX,说明表没有主键,InnoDB创建了聚簇索引。

开启完整的死锁日志记录(记录到错误日志而非仅内存):

-- my.cnf配置
[mysqld]
innodb_print_all_deadlocks = ON
# 死锁信息将写入MySQL错误日志

-- 查看错误日志位置
SHOW VARIABLES LIKE 'log_error';

死锁预防与锁等待超时配置

预防死锁的实践原则:

  • 统一加锁顺序:所有事务按相同顺序访问表和行。如更新多行时统一按主键升序加锁。
  • 缩短事务范围:事务中不包含耗时操作(如网络调用、文件IO),尽快提交。
  • 使用RC隔离级别:RC隔离级别不使用间隙锁(除了外键和唯一约束检查),减少了间隙锁导致的死锁。在不需要防幻读的场景下推荐使用。
  • 合理使用索引:确保所有更新和删除查询使用索引,避免全表扫描导致的锁升级。
-- 锁等待超时配置
SET GLOBAL innodb_lock_wait_timeout = 30;  -- 锁等待超时30秒
-- 超时后事务不会回滚整条事务,而是该条SQL报错:
-- ERROR 1205 (HY000): Lock wait timeout exceeded

-- 死锁检测配置(高并发场景可能成为瓶颈)
SET GLOBAL innodb_deadlock_detect = ON;  -- 默认开启
-- 如果死锁检测本身消耗大量CPU(可通过SHOW ENGINE INNODB STATUS的SEMAPHORES段确认)
-- 可考虑关闭检测,依赖innodb_lock_wait_timeout超时机制

对于死锁频发的场景,应在应用层实现自动重试机制。重试逻辑需确保收到1213错误码时重试整个事务(而非单条SQL),重试次数建议设为3-5次,每次重试前加入随机退避时间避免再次冲突。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysqlinnodb-xing-suo-yu-jian-xi-suo-ji-zhi-xiang-jie-ji-si/

(0)
小编小编
上一篇 9小时前
下一篇 9小时前

相关推荐