欢迎光临
我们一直在努力

MySQL 死锁分析与解决实战

前言

💡 痛点:并发写入时突然报死锁错误?业务日志里频繁出现 Deadlock found when trying to get lock?不知道如何定位是哪两条 SQL 导致的?

🎯 解决方案:掌握 MySQL 死锁分析 — 从日志定位死锁、了解 InnoDB 锁机制、编写死锁安全的代码。

MySQL 死锁排查流程图:

#mermaid-svg-6CakMS1hxe40vqp0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6CakMS1hxe40vqp0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6CakMS1hxe40vqp0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6CakMS1hxe40vqp0 .error-icon{fill:#552222;}#mermaid-svg-6CakMS1hxe40vqp0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6CakMS1hxe40vqp0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6CakMS1hxe40vqp0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6CakMS1hxe40vqp0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6CakMS1hxe40vqp0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6CakMS1hxe40vqp0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6CakMS1hxe40vqp0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6CakMS1hxe40vqp0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6CakMS1hxe40vqp0 .marker.cross{stroke:#333333;}#mermaid-svg-6CakMS1hxe40vqp0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6CakMS1hxe40vqp0 p{margin:0;}#mermaid-svg-6CakMS1hxe40vqp0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-6CakMS1hxe40vqp0 .cluster-label text{fill:#333;}#mermaid-svg-6CakMS1hxe40vqp0 .cluster-label span{color:#333;}#mermaid-svg-6CakMS1hxe40vqp0 .cluster-label span p{background-color:transparent;}#mermaid-svg-6CakMS1hxe40vqp0 .label text,#mermaid-svg-6CakMS1hxe40vqp0 span{fill:#333;color:#333;}#mermaid-svg-6CakMS1hxe40vqp0 .node rect,#mermaid-svg-6CakMS1hxe40vqp0 .node circle,#mermaid-svg-6CakMS1hxe40vqp0 .node ellipse,#mermaid-svg-6CakMS1hxe40vqp0 .node polygon,#mermaid-svg-6CakMS1hxe40vqp0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6CakMS1hxe40vqp0 .rough-node .label text,#mermaid-svg-6CakMS1hxe40vqp0 .node .label text,#mermaid-svg-6CakMS1hxe40vqp0 .image-shape .label,#mermaid-svg-6CakMS1hxe40vqp0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-6CakMS1hxe40vqp0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6CakMS1hxe40vqp0 .rough-node .label,#mermaid-svg-6CakMS1hxe40vqp0 .node .label,#mermaid-svg-6CakMS1hxe40vqp0 .image-shape .label,#mermaid-svg-6CakMS1hxe40vqp0 .icon-shape .label{text-align:center;}#mermaid-svg-6CakMS1hxe40vqp0 .node.clickable{cursor:pointer;}#mermaid-svg-6CakMS1hxe40vqp0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6CakMS1hxe40vqp0 .arrowheadPath{fill:#333333;}#mermaid-svg-6CakMS1hxe40vqp0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6CakMS1hxe40vqp0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6CakMS1hxe40vqp0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6CakMS1hxe40vqp0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6CakMS1hxe40vqp0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6CakMS1hxe40vqp0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6CakMS1hxe40vqp0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6CakMS1hxe40vqp0 .cluster text{fill:#333;}#mermaid-svg-6CakMS1hxe40vqp0 .cluster span{color:#333;}#mermaid-svg-6CakMS1hxe40vqp0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6CakMS1hxe40vqp0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6CakMS1hxe40vqp0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-6CakMS1hxe40vqp0 .icon-shape,#mermaid-svg-6CakMS1hxe40vqp0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6CakMS1hxe40vqp0 .icon-shape p,#mermaid-svg-6CakMS1hxe40vqp0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6CakMS1hxe40vqp0 .icon-shape .label rect,#mermaid-svg-6CakMS1hxe40vqp0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6CakMS1hxe40vqp0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6CakMS1hxe40vqp0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6CakMS1hxe40vqp0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

死锁发生

查看错误日志

分析死锁信息

定位相关表和索引

分析 SQL 执行顺序

定位锁冲突点

优化业务逻辑

避免循环等待

使用低隔离级别

调整索引

死锁 vs 锁等待:

特性死锁锁等待
定义 两个或多个事务互相持有对方需要的锁 事务等待其他事务释放锁
处理方式 MySQL 自动回滚一个事务 等待超时(innodb_lock_wait_timeout)
超时时间 立即检测并处理 默认 50 秒
影响 至少一个事务被回滚 阻塞其他请求

一、InnoDB 锁机制

1.1 锁类型概览

#mermaid-svg-VZQTdSbJzN7VCzmf{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VZQTdSbJzN7VCzmf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VZQTdSbJzN7VCzmf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VZQTdSbJzN7VCzmf .error-icon{fill:#552222;}#mermaid-svg-VZQTdSbJzN7VCzmf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VZQTdSbJzN7VCzmf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VZQTdSbJzN7VCzmf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VZQTdSbJzN7VCzmf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VZQTdSbJzN7VCzmf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VZQTdSbJzN7VCzmf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VZQTdSbJzN7VCzmf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VZQTdSbJzN7VCzmf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VZQTdSbJzN7VCzmf .marker.cross{stroke:#333333;}#mermaid-svg-VZQTdSbJzN7VCzmf svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VZQTdSbJzN7VCzmf p{margin:0;}#mermaid-svg-VZQTdSbJzN7VCzmf .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-VZQTdSbJzN7VCzmf .cluster-label text{fill:#333;}#mermaid-svg-VZQTdSbJzN7VCzmf .cluster-label span{color:#333;}#mermaid-svg-VZQTdSbJzN7VCzmf .cluster-label span p{background-color:transparent;}#mermaid-svg-VZQTdSbJzN7VCzmf .label text,#mermaid-svg-VZQTdSbJzN7VCzmf span{fill:#333;color:#333;}#mermaid-svg-VZQTdSbJzN7VCzmf .node rect,#mermaid-svg-VZQTdSbJzN7VCzmf .node circle,#mermaid-svg-VZQTdSbJzN7VCzmf .node ellipse,#mermaid-svg-VZQTdSbJzN7VCzmf .node polygon,#mermaid-svg-VZQTdSbJzN7VCzmf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VZQTdSbJzN7VCzmf .rough-node .label text,#mermaid-svg-VZQTdSbJzN7VCzmf .node .label text,#mermaid-svg-VZQTdSbJzN7VCzmf .image-shape .label,#mermaid-svg-VZQTdSbJzN7VCzmf .icon-shape .label{text-anchor:middle;}#mermaid-svg-VZQTdSbJzN7VCzmf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VZQTdSbJzN7VCzmf .rough-node .label,#mermaid-svg-VZQTdSbJzN7VCzmf .node .label,#mermaid-svg-VZQTdSbJzN7VCzmf .image-shape .label,#mermaid-svg-VZQTdSbJzN7VCzmf .icon-shape .label{text-align:center;}#mermaid-svg-VZQTdSbJzN7VCzmf .node.clickable{cursor:pointer;}#mermaid-svg-VZQTdSbJzN7VCzmf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VZQTdSbJzN7VCzmf .arrowheadPath{fill:#333333;}#mermaid-svg-VZQTdSbJzN7VCzmf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VZQTdSbJzN7VCzmf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VZQTdSbJzN7VCzmf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VZQTdSbJzN7VCzmf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VZQTdSbJzN7VCzmf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VZQTdSbJzN7VCzmf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VZQTdSbJzN7VCzmf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VZQTdSbJzN7VCzmf .cluster text{fill:#333;}#mermaid-svg-VZQTdSbJzN7VCzmf .cluster span{color:#333;}#mermaid-svg-VZQTdSbJzN7VCzmf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VZQTdSbJzN7VCzmf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VZQTdSbJzN7VCzmf rect.text{fill:none;stroke-width:0;}#mermaid-svg-VZQTdSbJzN7VCzmf .icon-shape,#mermaid-svg-VZQTdSbJzN7VCzmf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VZQTdSbJzN7VCzmf .icon-shape p,#mermaid-svg-VZQTdSbJzN7VCzmf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VZQTdSbJzN7VCzmf .icon-shape .label rect,#mermaid-svg-VZQTdSbJzN7VCzmf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VZQTdSbJzN7VCzmf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VZQTdSbJzN7VCzmf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VZQTdSbJzN7VCzmf :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

InnoDB 锁

共享锁 S

排他锁 X

意向锁

记录锁

间隙锁

临键锁

插入意向锁

允许其他事务读取

不允许写操作

允许一个事务写

不允许其他读写

IS 意向共享

IX 意向排他

1.2 锁的兼容性矩阵

锁类型XSIXIS
X
S
IX
IS

1.3 锁模式详解

— ===== 共享锁 vs 排他锁 =====

— 共享锁(S锁):允许其他事务读取,但不允许写
SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE;

— 排他锁(X锁):不允许任何其他事务读写
SELECT * FROM orders WHERE id = 1 FOR UPDATE;

— INSERT/UPDATE/DELETE 自动加排他锁
UPDATE orders SET status = 'completed' WHERE id = 1;
DELETE FROM orders WHERE id = 1;
INSERT INTO orders (order_no, amount) VALUES ('ORD001', 100);

— ===== 记录锁(Record Lock)=====
— 锁定索引记录,非锁定整行
— 即使 WHERE 条件没有命中索引,也会锁定所有扫描过的行

— ===== 间隙锁(Gap Lock)=====
— 锁定索引之间的间隙,防止幻读
— RR 隔离级别下生效

— ===== 临键锁(Next-Key Lock)=====
— 记录锁 + 间隙锁的组合
— InnoDB 默认的锁算法

— ===== 插入意向锁(Insert Intention Lock)=====
— 插入操作在等待间隙释放时获取


二、死锁日志分析

2.1 开启死锁日志

— ===== 死锁日志配置 =====

— 查看当前配置
SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';

— 开启死锁日志(会输出到 error log)
SET GLOBAL innodb_print_all_deadlocks = ON;

— 查看死锁超时配置
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
— 默认 50 秒

— 设置死锁超时(会话级)
SET innodb_lock_wait_timeout = 10;

— 查看当前锁等待
SHOW ENGINE INNODB STATUS;

— 查看锁信息
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
SELECT * FROM information_schema.INNODB_TRX;

2.2 死锁日志解读

— ===== 创建测试数据 =====

CREATE TABLE accounts (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
balance DECIMAL(12,2) NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO accounts (user_id, balance) VALUES
(1, 1000),
(2, 2000),
(3, 3000);

— ===== 模拟死锁场景 =====

— 事务 A
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; — 锁定 id=1

— 事务 B
BEGIN;
SELECT * FROM accounts WHERE id = 2 FOR UPDATE; — 锁定 id=2

— 事务 A 尝试锁定 id=2(等待)
UPDATE accounts SET balance = balance 100 WHERE id = 2;

— 事务 B 尝试锁定 id=1(死锁!)
UPDATE accounts SET balance = balance + 100 WHERE id = 1;

— 死锁日志示例:
/*
========================
LATEST DETECTED DEADLOCK
————————
20240115 10:30:45 0x7f8a12345678
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 10 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
LOCK HTABLE NO 123
—SQL—
UPDATE accounts SET balance = balance + 100 WHERE id = 1

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 100 page no 3 n bits 72 index PRIMARY of table `test`.`accounts`
lock_mode X locks rec but not gap
Record lock: hrs=1, heap size 1136

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 100 page no 3 n bits 72 index PRIMARY of table `test`.`accounts`
lock_mode X locks rec but not gap waiting

*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 5 sec
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
—SQL—
UPDATE accounts SET balance = balance 100 WHERE id = 2

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 100 page no 3 n bits 72 index PRIMARY of table `test`.`accounts`
lock_mode X locks rec but not gap
Record lock: hrs=2, heap size 1136

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 100 page no 3 n bits 72 index PRIMARY of table `test`.`accounts`
lock_mode X locks rec but not gap waiting

2.3 死锁日志字段解读

— ===== 死锁日志字段详解 =====

— TRANSACTION: 事务信息
— – TRANSACTION ID: 事务 ID(system no)
— – ACTIVE: 活跃时间
— – thread id: 线程 ID

— LOCK WAIT: 锁等待信息
— – lock struct(s): 锁结构数量
— – heap size: 堆大小
— – row lock(s): 行锁数量

— HOLDS THE LOCK(S): 持有的锁
— – lock_mode: 锁模式(X/S, locks rec but not gap/gap/next-key)
— – lock_type: 锁类型(RECORD/BUFFER)
— – space id: 表空间 ID
— – page no: 页号
— – n bits: 位图大小

— WAITING FOR THIS LOCK TO BE GRANTED: 等待的锁
— – waiting: 表示正在等待

— SQL: 正在执行的 SQL


三、常见死锁场景

3.1 场景一:双向更新死锁

#mermaid-svg-R22gMFpTgtZqnw4p{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-R22gMFpTgtZqnw4p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-R22gMFpTgtZqnw4p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-R22gMFpTgtZqnw4p .error-icon{fill:#552222;}#mermaid-svg-R22gMFpTgtZqnw4p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-R22gMFpTgtZqnw4p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-R22gMFpTgtZqnw4p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-R22gMFpTgtZqnw4p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-R22gMFpTgtZqnw4p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-R22gMFpTgtZqnw4p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-R22gMFpTgtZqnw4p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-R22gMFpTgtZqnw4p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-R22gMFpTgtZqnw4p .marker.cross{stroke:#333333;}#mermaid-svg-R22gMFpTgtZqnw4p svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-R22gMFpTgtZqnw4p p{margin:0;}#mermaid-svg-R22gMFpTgtZqnw4p .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-R22gMFpTgtZqnw4p .cluster-label text{fill:#333;}#mermaid-svg-R22gMFpTgtZqnw4p .cluster-label span{color:#333;}#mermaid-svg-R22gMFpTgtZqnw4p .cluster-label span p{background-color:transparent;}#mermaid-svg-R22gMFpTgtZqnw4p .label text,#mermaid-svg-R22gMFpTgtZqnw4p span{fill:#333;color:#333;}#mermaid-svg-R22gMFpTgtZqnw4p .node rect,#mermaid-svg-R22gMFpTgtZqnw4p .node circle,#mermaid-svg-R22gMFpTgtZqnw4p .node ellipse,#mermaid-svg-R22gMFpTgtZqnw4p .node polygon,#mermaid-svg-R22gMFpTgtZqnw4p .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-R22gMFpTgtZqnw4p .rough-node .label text,#mermaid-svg-R22gMFpTgtZqnw4p .node .label text,#mermaid-svg-R22gMFpTgtZqnw4p .image-shape .label,#mermaid-svg-R22gMFpTgtZqnw4p .icon-shape .label{text-anchor:middle;}#mermaid-svg-R22gMFpTgtZqnw4p .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-R22gMFpTgtZqnw4p .rough-node .label,#mermaid-svg-R22gMFpTgtZqnw4p .node .label,#mermaid-svg-R22gMFpTgtZqnw4p .image-shape .label,#mermaid-svg-R22gMFpTgtZqnw4p .icon-shape .label{text-align:center;}#mermaid-svg-R22gMFpTgtZqnw4p .node.clickable{cursor:pointer;}#mermaid-svg-R22gMFpTgtZqnw4p .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-R22gMFpTgtZqnw4p .arrowheadPath{fill:#333333;}#mermaid-svg-R22gMFpTgtZqnw4p .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-R22gMFpTgtZqnw4p .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-R22gMFpTgtZqnw4p .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R22gMFpTgtZqnw4p .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-R22gMFpTgtZqnw4p .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R22gMFpTgtZqnw4p .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-R22gMFpTgtZqnw4p .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-R22gMFpTgtZqnw4p .cluster text{fill:#333;}#mermaid-svg-R22gMFpTgtZqnw4p .cluster span{color:#333;}#mermaid-svg-R22gMFpTgtZqnw4p div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-R22gMFpTgtZqnw4p .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-R22gMFpTgtZqnw4p rect.text{fill:none;stroke-width:0;}#mermaid-svg-R22gMFpTgtZqnw4p .icon-shape,#mermaid-svg-R22gMFpTgtZqnw4p .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R22gMFpTgtZqnw4p .icon-shape p,#mermaid-svg-R22gMFpTgtZqnw4p .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-R22gMFpTgtZqnw4p .icon-shape .label rect,#mermaid-svg-R22gMFpTgtZqnw4p .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R22gMFpTgtZqnw4p .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-R22gMFpTgtZqnw4p .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-R22gMFpTgtZqnw4p :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

事务 A: UPDATE id=1

锁定 id=1

尝试 UPDATE id=2

等待 id=2

事务 B: UPDATE id=2

锁定 id=2

尝试 UPDATE id=1

死锁!


— ===== 场景一:双向更新死锁 =====

— 死锁 SQL
— 事务 A:
BEGIN;
UPDATE accounts SET balance = balance 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

— 事务 B:
BEGIN;
UPDATE accounts SET balance = balance 100 WHERE id = 2; — 锁定 id=2
UPDATE accounts SET balance = balance + 100 WHERE id = 1; — 死锁!
COMMIT;

— ===== 解决方案:统一更新顺序 =====

— 事务 A:
BEGIN;
UPDATE accounts SET balance = balance 100 WHERE id = 1; — 先 id=1
UPDATE accounts SET balance = balance + 100 WHERE id = 2; — 后 id=2
COMMIT;

— 事务 B:
BEGIN;
UPDATE accounts SET balance = balance 100 WHERE id = 1; — 同样先 id=1
UPDATE accounts SET balance = balance + 100 WHERE id = 2; — 同样后 id=2
COMMIT;

3.2 场景二:索引导致的死锁

— ===== 场景二:索引导致的死锁 =====

— 创建带索引的表
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
price DECIMAL(10,2) NOT NULL,
INDEX idx_order_id (order_id),
INDEX idx_product_id (product_id),
INDEX idx_order_product (order_id, product_id)
) ENGINE=InnoDB;

— 死锁场景:不同索引导致不同的锁顺序
— 事务 A:
BEGIN;
— 使用 idx_order_id 索引锁定订单
UPDATE order_items SET quantity = 10 WHERE order_id = 100;

— 事务 B:
BEGIN;
— 使用 idx_product_id 索引锁定商品
UPDATE order_items SET price = 99.9 WHERE product_id = 200;

— 事务 A: 尝试更新 product_id=200 的记录
UPDATE order_items SET quantity = 20 WHERE product_id = 200;

— 事务 B: 尝试更新 order_id=100 的记录
UPDATE order_items SET quantity = 30 WHERE order_id = 100;
— 死锁!

— ===== 解决方案:删除冗余索引 =====
ALTER TABLE order_items DROP INDEX idx_order_id;
ALTER TABLE order_items DROP INDEX idx_product_id;
— 只保留复合索引 idx_order_product

3.3 场景三:主键插入死锁

— ===== 场景三:Gap Lock 导致的插入死锁 =====

— 事务 A:
BEGIN;
— 锁定一个范围(等待插入)
SELECT * FROM accounts WHERE id > 100 FOR UPDATE;

— 事务 B:
BEGIN;
— 尝试在相同范围插入(也需要 gap lock)
INSERT INTO accounts (id, user_id, balance) VALUES (101, 1001, 500);
— 被阻塞

— 事务 A: 尝试在相同范围插入
INSERT INTO accounts (id, user_id, balance) VALUES (102, 1002, 600);
— 死锁!

— ===== 解决方案:使用低隔离级别 =====
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
— 或使用 ROW 模式 binlog
SET SESSION binlog_format = 'ROW';

3.4 场景四:外键死锁

— ===== 场景四:外键索引导致的死锁 =====

CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL UNIQUE,
customer_id BIGINT NOT NULL,
total_amount DECIMAL(12,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_customer_id (customer_id)
) ENGINE=InnoDB;

CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;

— 死锁场景
— 事务 A: 删除订单
BEGIN;
DELETE FROM orders WHERE id = 1;
— InnoDB 会在子表加锁

— 事务 B: 在子表插入记录
BEGIN;
INSERT INTO order_items (order_id, product_id, quantity) VALUES (1, 100, 2);
— 被阻塞

— 事务 A: 在子表插入记录(不同 order_id)
INSERT INTO order_items (order_id, product_id, quantity) VALUES (2, 101, 3);
— 死锁!

— ===== 解决方案:先操作子表,再操作主表 =====
— 事务 A:
BEGIN;
DELETE FROM order_items WHERE order_id = 1;
DELETE FROM orders WHERE id = 1;
COMMIT;

— 或禁用外键检查(不推荐生产环境)
SET foreign_key_checks = OFF;
— 操作完成后重新开启
SET foreign_key_checks = ON;

3.5 场景五:间隙锁死锁

— ===== 场景五:范围更新导致的间隙锁死锁 =====

— 事务 A:
BEGIN;
— 锁定 id 在 1-100 范围内的所有记录
UPDATE accounts SET balance = balance + 100 WHERE id BETWEEN 1 AND 100;

— 事务 B:
BEGIN;
— 锁定 id 在 50-150 范围内的所有记录
UPDATE accounts SET balance = balance 100 WHERE id BETWEEN 50 AND 150;
— 死锁!(间隙重叠)

— ===== 解决方案一:使用主键范围 =====
BEGIN;
— 使用精确的主键范围
UPDATE accounts SET balance = balance + 100 WHERE id >= 1 AND id <= 100;
COMMIT;

BEGIN;
— 同样使用精确范围
UPDATE accounts SET balance = balance 100 WHERE id >= 50 AND id <= 150;
COMMIT;

— ===== 解决方案二:使用记录锁替代间隙锁 =====
— 确保查询命中索引且返回确定记录
— 使用 FOR UPDATE SKIP LOCKED(跳过已锁定行)
SELECT * FROM accounts WHERE id >= 1 AND id <= 100 FOR UPDATE SKIP LOCKED;

— 或使用 NOWAIT(立即失败)
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;


四、死锁诊断工具

4.1 information_schema 查询

— ===== 锁信息查询 =====

— 1. 查看当前所有锁
SELECT
l.lock_id AS lock_id,
l.lock_mode AS lock_mode,
l.lock_type AS lock_type,
l.lock_table AS lock_table,
l.lock_index AS lock_index,
l.lock_space AS lock_space,
l.lock_page AS lock_page,
l.lock_rec AS lock_rec,
p.trx_id,
p.trx_state,
p.trx_started,
p.trx_rows_locked,
p.trx_rows_modified,
p.trx_query AS current_query
FROM information_schema.INNODB_LOCKS l
JOIN information_schema.INNODB_TRX p ON l.lock_trx_id = p.trx_id
ORDER BY p.trx_started;

— 2. 查看锁等待
SELECT
r.requesting_trx_id AS requesting_trx,
r.blocking_trx_id AS blocking_trx,
r.lock_id AS request_lock_id,
r.lock_mode AS request_mode,
r.lock_type AS request_type,
r.lock_table AS request_table,
b.trx_id AS blocking_trx_id,
b.trx_state AS blocking_state,
b.trx_query AS blocking_query
FROM information_schema.INNODB_LOCK_WAITS r
JOIN information_schema.INNODB_TRX b ON r.blocking_trx_id = b.trx_id
ORDER BY r.requesting_trx_id;

— 3. 查看事务详细信息
SELECT
trx_id,
trx_state,
trx_started,
trx_requested_lock_id,
trx_wait_started,
trx_weight,
trx_mysql_thread_id AS thread_id,
trx_query,
trx_rows_locked,
trx_rows_modified,
trx_concurrency_tickets AS concurrency_tickets,
trx_isolation_level,
trx_unique_checks,
trx_foreign_key_checks
FROM information_schema.INNODB_TRX
ORDER BY trx_started;

— 4. 查看锁的等待时间
SELECT
p.trx_mysql_thread_id AS thread_id,
p.trx_state,
TIMESTAMPDIFF(SECOND, p.trx_wait_started, NOW()) AS wait_seconds,
p.trx_query,
l.lock_mode,
l.lock_type,
l.lock_table
FROM information_schema.INNODB_TRX p
JOIN information_schema.INNODB_LOCKS l ON p.trx_id = l.lock_trx_id
WHERE p.trx_state = 'LOCK WAIT';

— 5. 杀掉阻塞的事务
— 先查询阻塞的线程 ID
SELECT
p.trx_mysql_thread_id AS thread_id,
p.trx_query
FROM information_schema.INNODB_TRX p
WHERE EXISTS (
SELECT 1 FROM information_schema.INNODB_LOCK_WAITS w
WHERE w.blocking_trx_id = p.trx_id
);

— 杀掉阻塞线程
— KILL <thread_id>;

4.2 performance_schema 监控

— ===== Performance Schema 锁监控 =====

— 开启锁监控(MySQL 8.0+)
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE 'wait/lock%';

UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME LIKE 'events_locks%';

— 查看锁事件
SELECT
EVENT_NAME,
COUNT_STAR AS total_count,
SUM_TIMER_WAIT / 1000000000000 AS total_seconds,
AVG_TIMER_WAIT / 1000000000000 AS avg_seconds,
MAX_TIMER_WAIT / 1000000000000 AS max_seconds
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE EVENT_NAME LIKE 'wait/lock%'
ORDER BY total_count DESC
LIMIT 20;

— 查看当前锁等待
SELECT
THREAD_ID,
EVENT_ID,
EVENT_NAME,
SOURCE,
TIMER_START,
TIMER_END,
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
OBJECT_TYPE,
LOCK_TYPE,
LOCK_STATUS
FROM performance_schema.events_waits_current
WHERE EVENT_NAME LIKE 'wait/lock%';

— 查看最近的锁事件
SELECT
THREAD_ID,
EVENT_NAME,
OBJECT_SCHEMA,
OBJECT_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_STATUS,
LOCK_DATA
FROM performance_schema.data_locks
ORDER BY TIMESTAMP;

— 查看锁依赖链
SELECT
REQUESTING_THREAD_ID,
REQUESTING_ENGINE_TRANSACTION_ID,
REQUESTING_LOCK_ID,
BLOCKING_THREAD_ID,
BLOCKING_ENGINE_TRANSACTION_ID,
BLOCKING_LOCK_ID
FROM performance_schema.data_lock_waits;

4.3 pt-deadlock-logger 工具

# ===== Percona Toolkit 死锁日志工具 =====

# 安装
# yum install percona-toolkit # CentOS/RHEL
# apt-get install percona-toolkit # Debian/Ubuntu

# 使用 pt-deadlock-logger
pt-deadlock-logger \\
–user=root \\
–password=xxx \\
–host=localhost \\
–engine=InnoDB \\
DSN=d=master,t=deadlocks

# 解析死锁日志
pt-deadlock-logger –print \\
–user=root \\
–password=xxx \\
/var/log/mysql/error.log

# 导出死锁历史
pt-deadlock-logger \\
–user=root \\
–password=xxx \\
–create-dbtable \\
–循环监控 \\
h=localhost,D=deadlock_history,t=deadlocks

# 查询死锁历史
SELECT * FROM deadlock_history.deadlocks
ORDER BY ts DESC LIMIT 10;


五、死锁解决策略

5.1 代码层面解决

— ===== 解决策略一:统一操作顺序 =====

— 错误写法(不同事务不同顺序)
— 事务 A: 先操作账户 A,再操作账户 B
BEGIN;
UPDATE accounts SET balance = balance 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

— 事务 B: 先操作账户 B,再操作账户 A(可能死锁!)
BEGIN;
UPDATE accounts SET balance = balance 100 WHERE id = 2;
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT;

— 正确写法(统一按 ID 升序操作)
— 事务 A:
BEGIN;
UPDATE accounts SET balance = balance 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

— 事务 B:
BEGIN;
UPDATE accounts SET balance = balance 100 WHERE id = 1; — 同样先 id=1
UPDATE accounts SET balance = balance + 100 WHERE id = 2; — 同样后 id=2
COMMIT;

— ===== 解决策略二:减小锁范围 =====

— 错误写法(锁住整个表)
BEGIN;
UPDATE orders SET status = 'shipped' WHERE status = 'paid';
— 锁定大量行,可能超时

— 正确写法(分批处理)
BEGIN;
DECLARE done INT DEFAULT FALSE;
DECLARE cur_id BIGINT;
DECLARE cur CURSOR FOR SELECT id FROM orders WHERE status = 'paid' LIMIT 1000;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

OPEN cur;

read_loop: LOOP
FETCH cur INTO cur_id;
IF done THEN
LEAVE read_loop;
END IF;

UPDATE orders SET status = 'shipped' WHERE id = cur_id;
COMMIT; — 每批提交,释放锁
BEGIN; — 开启新事务
END LOOP;

CLOSE cur;
COMMIT;

— ===== 解决策略三:使用乐观锁 =====

— 添加 version 字段
ALTER TABLE accounts ADD COLUMN version INT NOT NULL DEFAULT 0;

— 乐观锁更新
UPDATE accounts
SET balance = balance 100, version = version + 1
WHERE id = 1 AND version = 5;

— 检查影响行数
— 如果 affected_rows = 0,说明版本不匹配,尝试重新读取重试

5.2 数据库层面解决

— ===== 解决策略四:调整隔离级别 =====

— 查看当前隔离级别
SELECT @@transaction_isolation;

— 设置为 READ COMMITTED(减少 Gap Lock)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

— 或 READ UNCOMMITTED(极少使用)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

— ===== 解决策略五:优化索引 =====

— 查看查询执行计划
EXPLAIN UPDATE accounts SET balance = balance 100 WHERE id = 1;

— 检查是否使用索引
SHOW INDEX FROM accounts;

— 添加合适索引减少锁范围
CREATE INDEX idx_accounts_id ON accounts(id);

— ===== 解决策略六:调整锁超时 =====

— 查看当前超时
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';

— 设置较短超时(快速失败重试)
SET innodb_lock_wait_timeout = 5;

— 永久配置(my.cnf)
— [mysqld]
— innodb_lock_wait_timeout = 5

— ===== 解决策略七:使用行锁而非表锁 =====

— 确保使用行锁(主键/索引查询)
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; — 行锁

— 避免全表扫描(会导致表锁)
SELECT * FROM accounts WHERE balance > 1000 FOR UPDATE; — 可能表锁

5.3 应用层面解决

— ===== 解决策略八:重试机制 =====

— MyBatis 死锁重试示例
@Retryable(
value = {DeadlockTimeoutException.class, ConcurrentUpdateException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2)
)
public void updateAccount(Long id, BigDecimal amount) {
jdbcTemplate.update(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
amount, id
);
}

— Spring Retry 配置
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy();
policy.setInitialInterval(100);
policy.setMultiplier(2.0);
policy.setMaxInterval(3000);

SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);

RetryTemplate template = new RetryTemplate();
template.setBackOffPolicy(policy);
template.setRetryPolicy(retryPolicy);

return template;
}
}

— ===== 解决策略九:分布式锁替代 =====

— 使用 Redis 分布式锁
public boolean transferWithRedisLock(Long fromId, Long toId, BigDecimal amount) {
String lockKey1 = "account:lock:" + Math.min(fromId, toId);
String lockKey2 = "account:lock:" + Math.max(fromId, toId);

String lock1 = redisTemplate.opsForValue().get(lockKey1);
String lock2 = redisTemplate.opsForValue().get(lockKey2);

// 按顺序获取锁
if (acquireLock(lockKey1, 5, TimeUnit.SECONDS) &&
acquireLock(lockKey2, 5, TimeUnit.SECONDS)) {
try {
// 执行转账
return doTransfer(fromId, toId, amount);
} finally {
releaseLock(lockKey1);
releaseLock(lockKey2);
}
}
return false;
}

— ===== 解决策略十:消息队列异步处理 =====

— 将并发写入转为顺序处理
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 发送消息到队列
kafkaTemplate.send("account-transfer", UUID.randomUUID().toString(),
new TransferMessage(fromId, toId, amount));
}

// 消费者顺序处理
@KafkaListener(topics = "account-transfer")
public void handleTransfer(TransferMessage message) {
// 使用单分区确保顺序
accountService.doTransfer(message.getFromId(),
message.getToId(), message.getAmount());
}


六、生产环境案例

6.1 订单扣库存死锁

— ===== 案例:库存扣减死锁 =====

CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
stock INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0,
INDEX idx_name (name)
) ENGINE=InnoDB;

CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL UNIQUE,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
) ENGINE=InnoDB;

— 错误流程(死锁风险)
— 线程 A:创建订单并扣库存
BEGIN;
INSERT INTO orders (order_no) VALUES ('ORD001');
SET @order_id = LAST_INSERT_ID();
INSERT INTO order_items (order_id, product_id, quantity) VALUES (@order_id, 1, 2);
UPDATE products SET stock = stock 2 WHERE id = 1;
COMMIT;

— 线程 B:创建订单并扣库存(并发)
BEGIN;
INSERT INTO orders (order_no) VALUES ('ORD002');
SET @order_id = LAST_INSERT_ID();
INSERT INTO order_items (order_id, product_id, quantity) VALUES (@order_id, 1, 3);
UPDATE products SET stock = stock 3 WHERE id = 1; — 死锁!
COMMIT;

— 死锁原因:
— 1. 两个事务都先锁定 orders 主键
— 2. 然后尝试锁定 products
— 3. InnoDB 外键自动创建索引,导致锁定顺序不同

— ===== 正确流程 =====

— 方法一:先扣库存,再创建订单
BEGIN;
— 乐观锁扣库存
UPDATE products
SET stock = stock 2, version = version + 1
WHERE id = 1 AND version = 0 AND stock >= 2;

— 检查影响行数
— IF affected_rows = 0 THEN ROLLBACK;

— 库存扣减成功后再创建订单
INSERT INTO orders (order_no) VALUES ('ORD001');
SET @order_id = LAST_INSERT_ID();
INSERT INTO order_items (order_id, product_id, quantity) VALUES (@order_id, 1, 2);
COMMIT;

— 方法二:使用单个事务,按固定顺序操作
BEGIN;
— 按 product_id 排序统一操作
UPDATE products SET stock = stock 2 WHERE id = 1;
INSERT INTO orders (order_no) VALUES ('ORD001');
COMMIT;

— 方法三:减少锁定时间
BEGIN;
— 使用 SELECT FOR UPDATE NOWAIT 快速失败
SELECT * FROM products WHERE id = 1 FOR UPDATE NOWAIT;
— 检查是否成功,获取锁失败则直接返回错误

UPDATE products SET stock = stock 2 WHERE id = 1;
— 快速完成订单创建
INSERT INTO orders (order_no) VALUES ('ORD001');
COMMIT;

6.2 余额变动死锁

— ===== 案例:余额变动死锁 =====

— 错误的转账实现
public void transferWrong(Long fromId, Long toId, BigDecimal amount) {
// 事务 A
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 先扣出
jdbcTemplate.update(
"UPDATE accounts SET balance = balance – ? WHERE id = ?",
amount, fromId
);

// 再转入
jdbcTemplate.update(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
amount, toId
);
}
}

— 问题:当 A 向 B 转账,B 向 A 转账时
— A: 锁定 1,尝试锁定 2
— B: 锁定 2,尝试锁定 1
— 死锁!

— ===== 正确实现:统一加锁顺序 =====

public void transferFixed(Long fromId, Long toId, BigDecimal amount) {
// 确保 ID 大的先锁定
Long firstId = fromId < toId ? fromId : toId;
Long secondId = fromId < toId ? toId : fromId;

@Transactional
public void doTransfer(Long firstId, Long secondId, BigDecimal amount) {
// 按 ID 顺序加锁
if (firstId.equals(fromId)) {
jdbcTemplate.update(
"UPDATE accounts SET balance = balance – ? WHERE id = ?",
amount, firstId
);
jdbcTemplate.update(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
amount, secondId
);
} else {
jdbcTemplate.update(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
amount, firstId
);
jdbcTemplate.update(
"UPDATE accounts SET balance = balance – ? WHERE id = ?",
amount, secondId
);
}
}
}

— ===== 最佳实现:乐观锁 + 重试 =====

@Transactional
public boolean transferOptimistic(Long fromId, Long toId, BigDecimal amount, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
try {
// 使用乐观锁扣款
int affected = jdbcTemplate.update("""
UPDATE accounts
SET balance = balance – ?, version = version + 1
WHERE id = ? AND version = ? AND balance >= ?
"""
, amount, fromId, currentVersion, amount);

if (affected == 0) {
// 版本不匹配,版本已被修改
// 重新查询并重试
Account from = jdbcTemplate.queryForObject(
"SELECT * FROM accounts WHERE id = ?",
Account.class, fromId);
// 重新计算版本…
continue;
}

// 转入
jdbcTemplate.update("""
UPDATE accounts
SET balance = balance + ?, version = version + 1
WHERE id = ?
"""
, amount, toId);

return true;
} catch (DataAccessException e) {
if (i == maxRetries 1) {
throw e;
}
// 等待后重试
Thread.sleep(50 * (i + 1));
}
}
return false;
}


七、监控与预警

7.1 死锁监控 SQL

— ===== 死锁监控视图 =====

CREATE OR REPLACE VIEW v_deadlock_monitor AS
SELECT
NOW() AS check_time,
p.trx_id,
p.trx_mysql_thread_id AS thread_id,
p.trx_started,
p.trx_state,
p.trx_query,
p.trx_rows_locked,
p.trx_rows_modified,
TIMESTAMPDIFF(SECOND, p.trx_started, NOW()) AS running_seconds,
l.lock_mode,
l.lock_type,
l.lock_table,
l.lock_index
FROM information_schema.INNODB_TRX p
LEFT JOIN information_schema.INNODB_LOCKS l ON p.trx_id = l.lock_trx_id
ORDER BY p.trx_started;

— 查看当前锁等待
CREATE OR REPLACE VIEW v_lock_waits AS
SELECT
r.requesting_trx_id,
r.blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_state AS blocking_state,
b.trx_query AS blocking_query,
TIMESTAMPDIFF(SECOND, b.trx_wait_started, NOW()) AS wait_seconds,
l.lock_mode,
l.lock_table
FROM information_schema.INNODB_LOCK_WAITS r
JOIN information_schema.INNODB_TRX b ON r.blocking_trx_id = b.trx_id
JOIN information_schema.INNODB_LOCKS l ON b.trx_id = l.lock_trx_id
ORDER BY wait_seconds DESC;

— 查看最近死锁次数
CREATE OR REPLACE VIEW v_deadlock_stats AS
SELECT
VARIABLE_VALUE AS deadlock_count,
VARIABLE_NAME
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME IN ('Innodb_deadlocks', 'Innodb_rows_deleted', 'Innodb_rows_inserted');

— 查看锁内存使用
SELECT
VARIABLE_NAME,
VARIABLE_VALUE
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME LIKE '%lock%memory%'
OR VARIABLE_NAME LIKE '%innodb%lock%';

7.2 Prometheus 监控指标

# ===== Prometheus 监控配置 =====

# mysqld_exporter 配置
# 添加到 prometheus.yml
scrape_configs:
job_name: 'mysql'
static_configs:
targets: ['localhost:9104']
relabel_configs:
source_labels: [__address__]
target_label: instance
regex: '(.+):\\d+'
replacement: '${1}'

# 关键指标
# – mysql_global_status_innodb_deadlocks # 死锁总数
# – mysql_global_status_innodb_lock_waits # 锁等待次数
# – mysql_global_status_innodb_lock_timeouts # 锁超时次数

# ===== Prometheus 查询 =====

# 死锁增长率
rate(mysql_global_status_innodb_deadlocks[5m])

# 锁等待增长率
rate(mysql_global_status_innodb_lock_waits_total[5m])

# 锁超时增长率
rate(mysql_global_status_innodb_lock_timeouts[5m])

# 告警规则
groups:
– name: mysql-lock-alerts
rules:
– alert: MySQLDeadlockDetected
expr: increase(mysql_global_status_innodb_deadlocks[5m]) > 0
for: 0m
labels:
severity: warning
annotations:
summary: "MySQL 死锁检测"
description: "检测到 {{ $value }} 个死锁"

– alert: MySQLLockWaitTimeout
expr: rate(mysql_global_status_innodb_lock_timeouts[5m]) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "MySQL 锁等待超时"
description: "锁等待超时速率异常"

7.3 Grafana 仪表盘

{
"panels": [
{
"title": "死锁次数",
"type": "stat",
"targets": [
{
"expr": "mysql_global_status_innodb_deadlocks",
"legendFormat": "总死锁数"
}
]
},
{
"title": "锁等待次数",
"type": "graph",
"targets": [
{
"expr": "rate(mysql_global_status_innodb_lock_waits_total[5m])",
"legendFormat": "锁等待速率"
}
]
},
{
"title": "锁超时次数",
"type": "graph",
"targets": [
{
"expr": "rate(mysql_global_status_innodb_lock_timeouts[5m])",
"legendFormat": "锁超时速率"
}
]
}
]
}


八、总结

8.1 死锁排查流程

#mermaid-svg-vVe6zDd23Yqh9ptC{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vVe6zDd23Yqh9ptC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vVe6zDd23Yqh9ptC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vVe6zDd23Yqh9ptC .error-icon{fill:#552222;}#mermaid-svg-vVe6zDd23Yqh9ptC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vVe6zDd23Yqh9ptC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vVe6zDd23Yqh9ptC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vVe6zDd23Yqh9ptC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vVe6zDd23Yqh9ptC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vVe6zDd23Yqh9ptC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vVe6zDd23Yqh9ptC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vVe6zDd23Yqh9ptC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vVe6zDd23Yqh9ptC .marker.cross{stroke:#333333;}#mermaid-svg-vVe6zDd23Yqh9ptC svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vVe6zDd23Yqh9ptC p{margin:0;}#mermaid-svg-vVe6zDd23Yqh9ptC .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-vVe6zDd23Yqh9ptC .cluster-label text{fill:#333;}#mermaid-svg-vVe6zDd23Yqh9ptC .cluster-label span{color:#333;}#mermaid-svg-vVe6zDd23Yqh9ptC .cluster-label span p{background-color:transparent;}#mermaid-svg-vVe6zDd23Yqh9ptC .label text,#mermaid-svg-vVe6zDd23Yqh9ptC span{fill:#333;color:#333;}#mermaid-svg-vVe6zDd23Yqh9ptC .node rect,#mermaid-svg-vVe6zDd23Yqh9ptC .node circle,#mermaid-svg-vVe6zDd23Yqh9ptC .node ellipse,#mermaid-svg-vVe6zDd23Yqh9ptC .node polygon,#mermaid-svg-vVe6zDd23Yqh9ptC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vVe6zDd23Yqh9ptC .rough-node .label text,#mermaid-svg-vVe6zDd23Yqh9ptC .node .label text,#mermaid-svg-vVe6zDd23Yqh9ptC .image-shape .label,#mermaid-svg-vVe6zDd23Yqh9ptC .icon-shape .label{text-anchor:middle;}#mermaid-svg-vVe6zDd23Yqh9ptC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vVe6zDd23Yqh9ptC .rough-node .label,#mermaid-svg-vVe6zDd23Yqh9ptC .node .label,#mermaid-svg-vVe6zDd23Yqh9ptC .image-shape .label,#mermaid-svg-vVe6zDd23Yqh9ptC .icon-shape .label{text-align:center;}#mermaid-svg-vVe6zDd23Yqh9ptC .node.clickable{cursor:pointer;}#mermaid-svg-vVe6zDd23Yqh9ptC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vVe6zDd23Yqh9ptC .arrowheadPath{fill:#333333;}#mermaid-svg-vVe6zDd23Yqh9ptC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vVe6zDd23Yqh9ptC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vVe6zDd23Yqh9ptC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vVe6zDd23Yqh9ptC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vVe6zDd23Yqh9ptC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vVe6zDd23Yqh9ptC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vVe6zDd23Yqh9ptC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vVe6zDd23Yqh9ptC .cluster text{fill:#333;}#mermaid-svg-vVe6zDd23Yqh9ptC .cluster span{color:#333;}#mermaid-svg-vVe6zDd23Yqh9ptC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vVe6zDd23Yqh9ptC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vVe6zDd23Yqh9ptC rect.text{fill:none;stroke-width:0;}#mermaid-svg-vVe6zDd23Yqh9ptC .icon-shape,#mermaid-svg-vVe6zDd23Yqh9ptC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vVe6zDd23Yqh9ptC .icon-shape p,#mermaid-svg-vVe6zDd23Yqh9ptC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vVe6zDd23Yqh9ptC .icon-shape .label rect,#mermaid-svg-vVe6zDd23Yqh9ptC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vVe6zDd23Yqh9ptC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vVe6zDd23Yqh9ptC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vVe6zDd23Yqh9ptC :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

死锁发生

查看错误日志

分析死锁信息

定位锁冲突表

分析 SQL 执行顺序

根因

操作顺序不一致

索引选择问题

隔离级别问题

锁范围过大

统一操作顺序

优化索引

调整隔离级别

减小事务范围

回归验证

8.2 死锁解决策略汇总

策略适用场景实现难度
统一操作顺序 批量更新同类资源
减小锁范围 大事务、批量更新 ⭐⭐
乐观锁 并发度低、更新冲突少 ⭐⭐
降低隔离级别 允许脏读/不可重复读
优化索引 减少锁扫描范围 ⭐⭐
重试机制 任何场景 ⭐⭐
分布式锁 高并发场景 ⭐⭐⭐
消息队列 异步处理场景 ⭐⭐⭐

8.3 最佳实践

实践说明
开启死锁日志 innodb_print_all_deadlocks = ON
统一操作顺序 按 ID 排序操作多个资源
减小事务 减少持有锁的时间
合理索引 避免全表扫描
监控告警 死锁次数超过阈值立即告警
重试机制 死锁时自动重试
业务解耦 高并发写入使用消息队列

8.4 学习路径

阶段内容资源
入门 InnoDB 锁机制、死锁日志解读 MySQL 文档
进阶 常见死锁场景、重试机制 博客文章
实战 生产案例、监控告警 GitHub 示例
专家 源码分析、内核原理 书籍

本文基于 MySQL 8.0 编写。如有问题欢迎评论区讨论!

赞(0)
未经允许不得转载:171主机测评 » MySQL 死锁分析与解决实战
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址