mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4002 字
11 分钟
MySQL 锁机制与死锁
2024-06-10

事务原理与隔离级别 讲隔离级别时,反复提到 Next-Key Lock 防幻读、当前读加排他锁,但锁的细节全部推迟到了这里。MVCC 解决了读写不互相阻塞的问题,但当前读(SELECT ... FOR UPDATEUPDATEDELETE)和写操作仍然需要锁来保证正确性。

本文从两阶段锁(2PL)理论出发,拆解 InnoDB 的锁体系:表级锁(意向锁、MDL、AUTO_INC、全局锁)、行级锁(Record、Gap、Next-Key、Insert Intention Lock)、锁兼容性矩阵,以及 RR 下加锁规则的四个场景。然后是死锁的产生条件、Wait-For Graph 检测、死锁案例与排查 SOP,以及间隙锁误锁这个常见踩坑。MVCC 版本链与可见性判断见 MySQL MVCC 原理

前置知识#

Important

一、两阶段锁理论#

1.1 2PL 原理#

两阶段锁(Two-Phase Locking,2PL)是保证可串行化的经典并发控制协议。其规则是:每个事务的锁操作分为两个阶段,增长阶段只加锁不释放,收缩阶段只释放不新增。

graph LR subgraph G["增长阶段"] G1["加锁1"] --> G2["加锁2"] --> G3["加锁3"] end subgraph S["收缩阶段"] S1["释放锁1"] --> S2["释放锁2"] --> S3["释放锁3"] end G -->|"到达锁定点\n获取所有锁"| S

2PL 为什么能保证可串行化?如果所有事务都遵守 2PL,事务的加锁顺序构成一个偏序关系,这个偏序关系就是可串行化顺序。形式化证明基于冲突可串行化理论,2PL 保证了冲突操作的串行顺序与加锁顺序一致。

1.2 严格两阶段锁(S2PL)#

2PL 的问题是:收缩阶段释放锁后,其他事务可能读到该事务尚未提交的修改,这恰好是脏读。解决方案是严格两阶段锁(Strict 2PL,S2PL):所有锁在事务提交或回滚时才统一释放。S2PL 是大多数数据库 SERIALIZABLE 隔离级别的实现方式。

InnoDB 的 REPEATABLE READ 和 READ COMMITTED 在实际执行中采用了 S2PL 的变体:行级排他锁在事务提交时才释放,但共享锁的行为因隔离级别而异。RC 下共享锁在语句执行完就释放,RR 下共享锁持有到事务结束。

二、InnoDB 锁类型#

2.1 锁体系总览#

InnoDB 的锁体系从粒度到类型层层递进:

graph TB LOCK["InnoDB 锁"] LOCK --> TABLE["表级锁"] LOCK --> ROW["行级锁"] TABLE --> TABLE_MDL["元数据锁 MDL\nDDL 防护"] TABLE --> TABLE_INT["意向锁 IS / IX"] TABLE --> TABLE_AUTO["AUTO_INC 锁\n自增主键分配"] ROW --> RECORD["Record Lock\n锁定索引记录"] ROW --> GAP["Gap Lock\n锁定记录间间隙"] ROW --> NEXTKEY["Next-Key Lock\nRecord + Gap"] ROW --> INSERT_INT["Insert Intention Lock\n插入意向锁"] style LOCK fill:#e3f2fd,stroke:#1565c0 style ROW fill:#fff3e0,stroke:#e65100 style RECORD fill:#e8f5e9,stroke:#2e7d32 style GAP fill:#fce4ec,stroke:#c62828 style NEXTKEY fill:#f3e5f5,stroke:#6a1b9a

2.2 表级锁详解#

InnoDB 的表级锁不止意向锁一类。总览图里出现的 MDL、AUTO_INC,以及更外层的全局锁,都属于表级或表级粒度以上的锁。它们触发机制各异,生产中踩坑的点也不同。这一节按”谁会触发、触发什么后果、生产怎么遇到”的顺序逐一展开。

2.2.1 元数据锁 MDL#

元数据锁(Metadata Lock,MDL)是 Server 层加的锁,不是存储引擎层的锁,目的是防止 DDL 和 DML 并发时结构不一致。任何事务对表执行 SELECTINSERTUPDATEDELETE 时,会自动在表上加 MDL 共享锁;执行 ALTERDROPCREATE 等 DDL 时,加 MDL 排他锁。

操作加的 MDL持有时间
普通 SELECTMDL 共享读锁(MDL_SHARED_READ事务结束释放
INSERT / UPDATE / DELETEMDL 共享写锁(MDL_SHARED_WRITE事务结束释放
ALTER / DROP / TRUNCATE 等 DDLMDL 排他写锁拿到后执行,执行完释放

MDL 共享读锁与共享写锁都属于共享类,彼此兼容,所以多个事务可以同时读写。但 MDL 排他锁与所有 MDL 锁冲突,DDL 必须等表上所有事务的 MDL 释放后才能拿到。

生产中最经典的坑就在这里。一个长事务(比如一个跑了半小时的报表查询)持有表的 MDL 共享锁,此时 DBA 执行 ALTER TABLE 加字段。DDL 申请 MDL 排他锁,被长事务挡住,进入等待队列。问题在于 MDL 排他锁是排队的:排在 DDL 后面新来的 DML 也会被 DDL 的等待挡住,因为它们申请的共享锁与 DDL 要的排他锁冲突。于是长事务阻塞了 DDL,DDL 又反过来阻塞了所有后续查询,整张表瞬间不可读写,业务雪崩。

-- 长事务持有 MDL 读锁
BEGIN;
SELECT * FROM orders WHERE ...; -- 此事务未提交,持有 orders 的 MDL 共享锁
-- 另一个会话尝试 DDL,被阻塞
ALTER TABLE orders ADD COLUMN remark VARCHAR(100);
-- 此时第三个会话的普通 SELECT 也会被阻塞
SELECT * FROM orders LIMIT 1; -- 卡住,因为排在 DDL 后面

排查 MDL 等待看 performance_schema.metadata_lockssys.schema_table_lock_waits

-- 查看谁持有、谁等待 MDL
SELECT * FROM performance_schema.metadata_locks
WHERE OBJECT_SCHEMA = 'testdb' AND OBJECT_NAME = 'orders';
-- sys 视图直接给出阻塞源
SELECT * FROM sys.schema_table_lock_waits\G

预防手段是 DDL 前先确认无长事务,或用 ALTER TABLE ... ALGORITHM=INSTANT(MySQL 8.0 部分场景支持秒级加列,不长时间持锁)。把 lock_wait_timeout 设小一点(默认 31536000 秒即一年,几乎等于不超时),可以让 DDL 等不到锁时快速失败而非无限期挂着。

2.2.2 AUTO_INC 锁#

AUTO_INC 锁用于自增主键的分配。当 INSERT 语句需要给自增列分配新值时,InnoDB 要保证并发插入时自增值不重复、不跳号。根据 innodb_autoinc_lock_mode 的取值,有三种模式:

模式取值行为适用
传统模式0全表级 AUTO_INC 锁,整条 INSERT 语句期间持有,串行兼容旧版本,并发差
连续模式1批量插入用表级锁,单条插入用轻量锁(互斥量,拿完即释放)兼顾语义与并发,MySQL 8.0 前的默认值
交叉模式2全程用轻量锁,不锁表,多事务可并发分配并发最高,MySQL 8.0 起的默认值

生产中最常见的纠结是模式 2 的跳号。模式 2 下批量 INSERT ... VALUES (...), (...), ... 时,InnoDB 一次性预分配一段自增值,若语句中途失败或回滚,预分配但未用的值就丢了,自增列出现空洞。对要求自增列严格连续的场景(比如生成对外展示的订单号),不能用模式 2,要用模式 1。对只把自增列当主键、不关心是否连续的场景,模式 2 并发最高,是 8.0 的默认值。

-- 查看当前模式
SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';
-- 8.0 默认为 2(交叉模式),5.7 默认为 1(连续模式)
-- 跳号场景:模式 2 下批量插入中途失败
-- 假设 orders(id INT AUTO_INCREMENT PRIMARY KEY, amt INT)
INSERT INTO orders(amt) VALUES (1),(2),(3),(4),(5); -- InnoDB 预分配 id=1..5
-- 若语句失败,下次插入从 id=6 开始,预分配但未用的值丢弃,造成跳号

2.2.3 全局锁#

全局锁作用于整个数据库实例,效果是让整个库只能读不能写。MySQL 的全局锁是 FLUSH TABLES WITH READ LOCK(FTWRL):

FLUSH TABLES WITH READ LOCK;
-- 此后全库所有表只能 SELECT,任何 INSERT/UPDATE/DELETE/DDL 都被阻塞
-- 直到执行 UNLOCK TABLES

FTWRL 的典型用途是全库逻辑备份。备份工具(如 mysqldump 的非事务一致性选项、或 Percona XtraBackup 的某些模式)在开始备份前执行 FTWRL,拿到一个全库一致的快照点,保证备份出来的多张表数据在同一个逻辑时刻一致。这是它唯一仍合理的生产用途。

但 FTWRL 代价极高。它阻塞所有写操作,业务停摆,且持有期间任何写请求堆积。更麻烦的是它可能和长事务互锁:如果某个长事务持有锁未释放,FTWRL 会等它,等的过程中全库写请求已经堆积。生产中能用一致性快照备份(mysqldump --single-transaction,依赖 MVCC 不加表锁)就别用 FTWRL,只在引擎不支持事务或需要跨引擎一致性时才用。

MySQL 8.0 后,需要”全库只读”语义时更推荐 SET GLOBAL read_only = ONsuper_read_only = ON。它们让实例只读但不锁表,写请求被拒而非阻塞,且不会像 FTWRL 那样持有期间死等已有事务。两者的区别:read_only 允许有 SUPER 权限的用户写,super_read_onlySUPER 用户也不让写。

2.2.4 意向锁#

意向锁是表级锁,但目的是为行级锁服务。如果事务要加表级锁,必须检查表中是否有行级锁。没有意向锁时需要逐行扫描,有了意向锁只需检查表级意向锁即可。意向锁是一个快速判断表内是否存在行级锁的标记。

锁类型符号兼容性用途
共享锁(S Lock)SS-S 兼容,S-X 互斥读操作(SELECT ... LOCK IN SHARE MODE
排他锁(X Lock)X与所有锁互斥写操作(INSERT / UPDATE / DELETE / FOR UPDATE
意向共享锁(IS)ISIS-IS / IS-IX / IS-S 兼容表级锁,表示打算加行级 S 锁
意向排他锁(IX)IXIX-IX / IX-IS 兼容表级锁,表示打算加行级 X 锁
-- 意向锁的工作方式
-- 事务 T1:给 id=1 的行加 X 锁(自动加 IX 表锁)
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- InnoDB 自动在 accounts 表上加 IX 锁
-- 事务 T2:尝试给整个表加 S 锁
LOCK TABLES accounts READ;
-- 检查:表上有 IX 锁,与 S 锁冲突 → 阻塞等待

2.3 锁兼容性矩阵#

ISIXSX
IS兼容兼容兼容互斥
IX兼容兼容互斥互斥
S兼容互斥兼容互斥
X互斥互斥互斥互斥

意向锁之间互相兼容(IS-IS、IS-IX、IX-IX),因为它们只是标记”打算加行锁”,不真正锁住行。只有表级 S/X 锁与意向锁冲突时才阻塞。

2.4 行级锁:Record Lock#

Record Lock 锁定索引记录本身,而非数据行。如果表没有索引,InnoDB 会使用隐藏的聚簇索引进行锁定。

-- Record Lock 示例
-- 表:users(id INT PRIMARY KEY, name VARCHAR(50), age INT, INDEX idx_age(age))
BEGIN;
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 锁定:id=5 的 Record Lock(在聚簇索引上)
SELECT * FROM users WHERE age = 25 FOR UPDATE;
-- 锁定:age=25 的 Record Lock(在 idx_age 索引上)
-- + 对应主键的 Record Lock(回表后在聚簇索引上)

2.5 行级锁:Gap Lock#

Gap Lock 锁定索引记录之间的间隙,防止其他事务在间隙中插入新记录。Gap Lock 是 InnoDB 在 REPEATABLE READ 下防止幻读的关键机制。

-- Gap Lock 示例
-- 假设 users 表中 age 列有值:10, 20, 30, 40
BEGIN;
SELECT * FROM users WHERE age = 20 FOR UPDATE;
-- 在 RR 隔离级别下,对 age=20 命中的记录加 Next-Key Lock (10, 20]
-- 并向右遍历到第一条不满足条件的 age=30,退化为纯 Gap Lock (20, 30)
-- 即 idx_age 上:(10, 20] 的 Next-Key Lock + (20, 30) 的 Gap Lock(不含 age=30 本身)
-- 另一个事务尝试插入
INSERT INTO users (name, age) VALUES ('New', 25);
-- 阻塞!因为 25 落在 (20, 30) 间隙内
Warning

Gap Lock 在 READ COMMITTED 下不会启用。如果应用从 RR 切换到 RC,间隙锁消失,幻读防护也会消失。有些团队为了减少锁冲突把隔离级别改成 RC,需要同步评估幻读风险。

2.6 行级锁:Next-Key Lock#

Next-Key Lock = Record Lock + Gap Lock,锁定一条记录及其前面的间隙。这是 InnoDB 在 RR 隔离级别下的默认加锁方式。

-- Next-Key Lock 的范围表示
-- 假设索引值为:5, 10, 15, 20
-- Next-Key Lock 的可能范围:
-- (-∞, 5] → 间隙(-∞,5) + 记录5
-- (5, 10] → 间隙(5,10) + 记录10
-- (10, 15] → 间隙(10,15) + 记录15
-- (15, 20] → 间隙(15,20) + 记录20
-- (20, +∞) → 间隙(20,+∞)

2.7 行级锁:Insert Intention Lock#

Insert Intention Lock 是 Gap Lock 的特殊形式,表示意图在间隙中插入。多个事务在同一间隙中插入不同位置时不会互相阻塞。事务 A 持有 (10, 20) 的 Gap Lock 时,事务 B(插入 age=15)和事务 C(插入 age=12)都阻塞;A 释放后,B 和 C 并行插入,因为它们插入的位置不同,互不冲突。

三、加锁规则#

3.1 加锁规则#

InnoDB 的加锁遵循以下规则(适用于 RR 隔离级别):

  1. 加锁的基本单位是 Next-Key Lock
  2. 查找过程中访问到的对象才会加锁
  3. 等值查询:唯一索引命中记录,退化为 Record Lock;未命中,退化为 Gap Lock
  4. 等值查询:普通索引命中记录,向右遍历到不满足条件的第一条记录,该记录的 Gap Lock 不包含
  5. 范围查询:对扫描到的每个索引记录加 Next-Key Lock

3.2 加锁规则实战分析#

-- 加锁规则实战分析
-- 表:t(id INT PRIMARY KEY, c INT, INDEX idx_c(c))
-- 数据:id=5,c=5 | id=10,c=10 | id=15,c=15 | id=20,c=20
-- 场景1:等值查询,唯一索引命中
SELECT * FROM t WHERE id = 10 FOR UPDATE;
-- 加锁:id=10 的 Record Lock(退化)
-- 场景2:等值查询,唯一索引未命中
SELECT * FROM t WHERE id = 12 FOR UPDATE;
-- 加锁:(10, 15) 的 Gap Lock(退化)
-- 场景3:等值查询,普通索引命中
SELECT * FROM t WHERE c = 10 FOR UPDATE;
-- 加锁:idx_c 上 (5, 10] 的 Next-Key Lock
-- idx_c 上 (10, 15) 的 Gap Lock(向右到不满足条件的第一条)
-- 聚簇索引上 id=10 的 Record Lock(回表)
-- 场景4:范围查询
SELECT * FROM t WHERE c >= 10 AND c < 15 FOR UPDATE;
-- 加锁:idx_c 上 (5, 10] 的 Next-Key Lock
-- idx_c 上 (10, 15] 的 Next-Key Lock
-- 聚簇索引上 id=10, id=15 的 Record Lock

场景 3 最容易出错。等值查询命中普通索引时,InnoDB 会向右遍历直到遇到不满足条件的第一条记录(c=15),但这条记录的 Gap Lock 不包含,即 (10, 15) 是 Gap Lock 而非 Next-Key Lock,不锁 c=15 这条记录本身。这个设计是为了减少锁范围,避免不必要的阻塞。

四、间隙锁误锁踩坑#

间隙锁的目的是防止幻读,但在实际开发中经常导致意外的锁等待。典型场景是范围查询锁住了不存在的行,导致后续插入阻塞。

-- 间隙锁误锁场景
-- 表:orders(id INT PRIMARY KEY, status VARCHAR(20), INDEX idx_status(status))
-- 数据:status 列有 'paid', 'shipped',没有 'pending'(字母序 paid < pending < shipped)
-- 事务 A:查询某个不存在的状态
BEGIN;
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE;
-- 没有命中记录,但加了 Gap Lock
-- 锁定了 ('paid', 'shipped') 之间的间隙(按字母序)
-- 事务 B:尝试插入
INSERT INTO orders (id, status) VALUES (100, 'processing');
-- 阻塞!'processing' 字母序也落在 ('paid', 'shipped') 间隙内
-- 虽然事务 A 查的是 'pending',但 Gap Lock 覆盖整个间隙,无关的插入也被挡住

事务 A 查询的 status = 'pending' 没有命中记录,InnoDB 退化为 Gap Lock,锁住的是索引中 'pending' 应该所在位置的前后间隙。'pending' 的字母序落在 'paid''shipped' 之间,所以锁住的是 ('paid', 'shipped') 这段间隙,任何插入到这个间隙的记录都会被阻塞,包括与 'pending' 毫无关系的 'processing'(它同样满足 paid < processing < shipped)。这里的关键是:Gap Lock 锁的是一段区间,不是某个具体值,只要插入值落进区间就阻塞,和事务 A 查的那个值是否相同无关。

-- 排查方法:查看当前锁等待(MySQL 8.0 用 performance_schema)
SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM performance_schema.data_locks;
SELECT * FROM information_schema.INNODB_TRX;
-- 解决方法:
-- 1. 缩小查询范围,加主键条件:WHERE id = ? AND status = 'pending'
-- 2. 降低隔离级别到 READ COMMITTED(Gap Lock 不启用)
-- 3. 如果业务允许,用普通 SELECT 代替 SELECT FOR UPDATE
Warning

间隙锁误锁是线上锁等待的常见原因。排查时如果发现 data_locks 中有 lock_modeGAP 的记录,且等待的事务是 INSERT,大概率是间隙锁误锁。解决方案优先考虑缩小查询范围或用普通 SELECT,轻易不要降隔离级别,那会引入幻读风险。

五、死锁#

5.1 死锁产生条件#

死锁需要同时满足四个条件(Coffman 条件):

  1. 互斥:资源同一时刻只能被一个事务持有
  2. 持有并等待:事务持有至少一个资源,同时等待其他资源
  3. 不可抢占:已持有的资源不能被强制剥夺
  4. 循环等待:事务之间形成环形等待链

5.2 死锁检测:Wait-For Graph#

数据库通过等待图(Wait-For Graph)检测死锁:如果等待图中存在环,则存在死锁。

graph LR T1["事务T1<br/>持有: 行A的X锁<br/>等待: 行B的X锁"] T2["事务T2<br/>持有: 行B的X锁<br/>等待: 行C的X锁"] T3["事务T3<br/>持有: 行C的X锁<br/>等待: 行A的X锁"] T1 -->|"等待"| T2 T2 -->|"等待"| T3 T3 -->|"等待"| T1 style T1 fill:#ffcdd2,stroke:#c62828 style T2 fill:#fff9c4,stroke:#f9a825 style T3 fill:#c8e6c9,stroke:#2e7d32

检测到环 T1→T2→T3→T1 后,数据库选择一个牺牲者(通常是修改数据量最少的事务)回滚,打破循环。InnoDB 默认开启死锁检测(innodb_deadlock_detect=ON),在持有锁和等待锁的事务之间构建 Wait-For Graph,发现环就回滚代价较小的一方。

5.3 死锁案例#

-- 死锁案例:交叉更新
-- 事务 A -- 事务 B
BEGIN; BEGIN;
UPDATE t SET c=100
WHERE id=5;
-- 持有:id=5 的 Record Lock
UPDATE t SET c=200
WHERE id=10;
-- 持有:id=10 的 Record Lock
UPDATE t SET c=100
WHERE id=10;
-- 等待:id=10 的 Record Lock(被 B 持有)
UPDATE t SET c=200
WHERE id=5;
-- 等待:id=5 的 Record Lock(被 A 持有)
-- 死锁!InnoDB 检测到并回滚 B

InnoDB 检测到死锁后,回滚其中一个事务并返回错误:

-- 被回滚的事务收到:
-- ERROR 1213 (40001): Deadlock found when trying to get lock;
-- try restarting transaction
-- 查看最近的死锁信息
SHOW ENGINE INNODB STATUS\G
-- 关注 "LATEST DETECTED DEADLOCK" 段,里面有死锁时刻两个事务的锁信息

5.4 死锁预防策略#

策略原理优点缺点
超时机制等待超过阈值则回滚实现简单阈值难设定,可能误杀长事务
按固定顺序访问所有事务按相同顺序加锁从根本上消除循环等待需要业务层约束
Wait-Die老事务等待新事务,新事务回滚不会饿死老事务新事务可能反复回滚
Wound-Wait老事务抢占新事务的资源老事务不会被阻塞新事务可能反复被抢占
Tip

预防死锁最实用的方法是按固定顺序访问资源。如果 T1 和 T2 都按 id 升序加锁(先 id=5 再 id=10),就不会形成循环等待。这是最简单也最有效的死锁预防策略。

六、锁排查运维 SOP#

6.1 锁等待参数#

InnoDB 的锁等待超时由 innodb_lock_wait_timeout 控制:

50Optionalinteger

行锁等待超时时间(秒),默认 50。超过后 MySQL 返回 ERROR 1205 并回滚当前语句(不是整个事务)。

ONOptionalboolean

死锁检测开关,默认开启。关闭后完全依赖超时机制处理死锁,高并发热点行场景下可减少检测开销。

6.2 查锁 SOP#

线上出现锁等待时,排查的标准流程如下:

-- 第一步:查看当前所有事务
SELECT trx_id, trx_state, trx_started, trx_requested_lock_id, trx_wait_started
FROM information_schema.INNODB_TRX;
-- 第二步:查看当前锁信息(MySQL 8.0 用 performance_schema)
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
-- MySQL 5.7 用 information_schema(INNODB_LOCKS 在 8.0 已删,5.7 才可用):
-- SELECT * FROM information_schema.INNODB_LOCKS;
-- SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 第三步:找到阻塞源头
-- trx_state = LOCK WAIT 的事务是被阻塞的
-- MySQL 8.0 的锁信息在 performance_schema,与 INNODB_TRX 不在同一模型,
-- 直接查 sys.innodb_lock_waits 视图(封装了 data_lock_waits + PROCESSLIST)更直观:
SELECT * FROM sys.innodb_lock_waits\G
-- MySQL 5.7 的写法(INNODB_LOCK_WAITS 在 8.0 已删,5.7 才可用):
-- 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
-- FROM information_schema.INNODB_LOCK_WAITS w
-- JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id
-- JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;
-- 第四步:查看锁详情
SHOW ENGINE INNODB STATUS\G
-- 关注 "TRANSACTIONS" 段和 "LATEST DETECTED DEADLOCK" 段

6.3 死锁日志解读#

SHOW ENGINE INNODB STATUS 输出的 LATEST DETECTED DEADLOCK 段是死锁排查的关键。它记录了死锁发生时两个事务的状态:

*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 2 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 t SET c=100 WHERE id=10
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 3 n bits 72 index PRIMARY of table `test`.`t`
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 1 sec starting index read
UPDATE t SET c=200 WHERE id=5
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 50 page no 3 n bits 72 index PRIMARY of table `test`.`t`
trx id 12346 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
...
Record lock, heap no 2 ...
*** WE ROLL BACK TRANSACTION (2)

解读要点:

  • TRANSACTION 12345TRANSACTION 12346 是死锁的两个事务
  • WAITING FOR THIS LOCK TO BE GRANTED 显示事务正在等待什么锁
  • HOLDS THE LOCK(S) 显示事务当前持有什么锁
  • lock_mode X locks rec but not gap 是 Record Lock,lock_mode X 是 Next-Key Lock
  • 最后一行 WE ROLL BACK TRANSACTION (2) 说明 InnoDB 选择回滚事务 2(通常是因为它修改的数据量更少)
Note

MySQL 8.0 将锁信息从 information_schema 迁移到了 performance_schema.data_locksdata_lock_waits(旧的 INNODB_LOCKS / INNODB_LOCK_WAITS 视图已删除,5.7 升级时需替换)。排查阻塞关系时,8.0 可直接用 sys.innodb_lock_waits 视图,省去手写 JOIN。如需更详细的锁状态,可用 SET GLOBAL innodb_status_output=ON 开启输出。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

MySQL 锁机制与死锁
https://blog.souloss.cn/posts/middleware/db/mysql-lock-and-deadlock/
作者
Souloss
发布于
2024-06-10
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时