大家好!今天我要和各位分享一个在 MySQL 项目中经常让开发者头疼的问题——InnoDB 的死锁问题。相信不少朋友都遇到过这样的情况:一个好好运行的系统突然报错,日志里冒出"Deadlock found when trying to get lock; try restarting transaction",然后你就开始了漫长的排查之旅...

别担心,这篇文章会用真实案例带你从现象到根源,彻底掌握死锁的排查技巧和解决方法。无论你是数据库管理员还是后端开发,这些内容都能帮你在实际工作中少走弯路。

InnoDB 锁机制基础知识

在深入案例前,我们先用通俗的话聊聊 InnoDB 的锁机制。

InnoDB 有几种主要的锁类型:

  • 共享锁(S 锁):大家一起看,不能改(SELECT ... LOCK IN SHARE MODE)
  • 排他锁(X 锁):我改数据时谁都别动(SELECT ... FOR UPDATE, UPDATE, DELETE)
  • 意向锁(IS/IX 锁):打个招呼说"我要在这张表的某些行上加锁了"
  • 记录锁:锁住单条记录
  • 间隙锁:锁住一个范围,但不包含记录本身
  • 临键锁(Next-Key Lock):记录锁+间隙锁的组合,防止幻读的重要机制
  • 插入意向锁:一种特殊的间隙锁,表示插入操作的意向,多个事务可以在同一间隙中设置插入意向锁而不冲突,但会与间隙锁冲突

需要特别强调的是:间隙锁和临键锁只在 REPEATABLE READ 隔离级别下默认生效。在 READ COMMITTED 隔离级别下,InnoDB 不使用间隙锁,临键锁会退化为记录锁,这是减少死锁的重要知识点。

想象一下,锁就像是在图书馆看书。共享锁就像大家一起看同一本书但不能写批注,排他锁就像你借走了这本书,别人就看不了了。

什么是死锁?

死锁就像两个人互相给对方让路,结果谁都动不了的尴尬局面。在数据库中,当两个或多个事务互相等待对方释放锁,导致所有事务都无法继续执行时,就形成了死锁。

举个简单例子:小明要拿筷子和碗才能吃饭,小红也一样。现在小明拿了筷子,小红拿了碗,两人都在等对方放下手中的东西,结果谁都吃不了饭。

graph LR
    A[事务1] -->|持有| B[资源A]
    A -->|请求| C[资源B]
    D[事务2] -->|持有| C
    D -->|请求| B

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style D fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#bbf,stroke:#333,stroke-width:2px

案例一:经典的行锁更新死锁

场景描述

假设我们有一个订单系统,有一张orders表,结构如下:

CREATE TABLE `orders` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `product_id` int NOT NULL,
  `status` int NOT NULL,
  `amount` decimal(10,2) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB;

系统中存在两个并发执行的事务,导致了死锁:

  • 事务 A:更新用户 1 的所有订单状态
  • 事务 B:更新特定产品相关的所有订单金额

问题重现

以下是导致死锁的操作序列:

这里需要注意一个重要点:InnoDB 的行锁是通过索引实现的。在这个案例中,事务 A 使用user_id索引,事务 B 使用product_id索引,导致锁定的记录顺序不同,增加了死锁风险。如果查询条件未命中索引,情况会更糟,可能导致表锁或大范围的临键锁。

死锁日志分析

当死锁发生时,MySQL 会在错误日志中记录详细信息。使用SHOW ENGINE INNODB STATUS可以查看最近一次死锁的详情:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-04-17 14:32:51 0x7f9a1c3a2700
*** (1) TRANSACTION:
TRANSACTION 10795, ACTIVE 2 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 155, OS thread handle 140301189614336, query id 9697 localhost root updating
UPDATE orders SET status=2 WHERE user_id=1 AND id>2

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 24 page no 4 n bits 72 index PRIMARY of table `test`.`orders` trx id 10795 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000003; asc     ;;
 1: len 6; hex 000000002a25; asc     *%;;
 2: len 7; hex 81000000110137; asc       7;;
 3: len 4; hex 80000002; asc     ;;
 4: len 4; hex 80000065; asc    e;;
 5: len 4; hex 80000001; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 10794, ACTIVE 5 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 156, OS thread handle 140301235861248, query id 9696 localhost root updating
UPDATE orders SET amount=amount*1.1 WHERE product_id=101 LIMIT 1 OFFSET 1

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 24 page no 4 n bits 72 index PRIMARY of table `test`.`orders` trx id 10794 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000003; asc     ;;
 1: len 6; hex 000000002a25; asc     *%;;
 2: len 7; hex 81000000110137; asc       7;;
 3: len 4; hex 80000002; asc     ;;
 4: len 4; hex 80000065; asc    e;;
 5: len 4; hex 80000001; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 24 page no 4 n bits 72 index PRIMARY of table `test`.`orders` trx id 10794 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000002a23; asc     *#;;
 2: len 7; hex 81000000110135; asc       5;;
 3: len 4; hex 80000001; asc     ;;
 4: len 4; hex 80000065; asc    e;;
 5: len 4; hex 80000001; asc     ;;

*** WE ROLL BACK TRANSACTION (1)

从日志中我们可以看到:

  1. 事务 A(TRANSACTION 10795)想获取 id=3 的行锁,但被事务 B 持有
  2. 同时事务 B(TRANSACTION 10794)想获取 id=1 的行锁,但被事务 A 持有
  3. 形成循环等待,MySQL 检测到死锁并回滚了事务 A

日志中一些专业术语解释:

  • lock_mode X:代表排他锁(X 锁)
  • locks rec but not gap:表示只锁记录,不锁间隙(记录锁)
  • heap no:表示记录在索引页中的位置

解决方案

对于这种情况,以下是按实用性排序的解决方法:

  1. 统一访问顺序(最有效):确保所有事务按相同的顺序访问记录,例如总是按主键顺序
-- 事务A
BEGIN;
UPDATE orders SET status=2 WHERE user_id=1 ORDER BY id;
COMMIT;

-- 事务B
BEGIN;
UPDATE orders SET amount=amount*1.1 WHERE product_id=101 ORDER BY id;
COMMIT;
  1. 减小事务范围:不要在一个大事务中做太多事情,拆分为多个小事务
-- 原来的大事务
BEGIN;
UPDATE orders SET status=2 WHERE user_id=1;
-- 其他操作...
COMMIT;

-- 拆分后
BEGIN;
UPDATE orders SET status=2 WHERE user_id=1 AND id BETWEEN 1 AND 100;
COMMIT;

BEGIN;
UPDATE orders SET status=2 WHERE user_id=1 AND id BETWEEN 101 AND 200;
COMMIT;
  1. 添加适当的锁超时设置
SET innodb_lock_wait_timeout = 50; -- 设置锁等待超时
  1. 使用乐观锁替代悲观锁(适合读多写少场景):
-- 使用版本号控制
UPDATE orders SET amount=amount*1.1, version=version+1
WHERE product_id=101 AND version=当前版本;

需要注意的是,乐观锁在并发冲突频繁的场景下可能导致大量重试,反而降低性能。此时应考虑悲观锁加合理的锁超时设置。

案例二:Gap 锁导致的死锁

场景描述

在 REPEATABLE READ 隔离级别下,InnoDB 会使用间隙锁(Gap Lock)来防止幻读。这种锁可能导致一些不直观的死锁。

假设有一个用户积分表:

CREATE TABLE `user_points` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `points` int NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB;

表中已有数据:

id | user_id | points | created_at
1  | 1       | 100    | 2023-04-15 10:00:00
3  | 3       | 150    | 2023-04-15 11:00:00
5  | 5       | 200    | 2023-04-15 12:00:00

注意这里 user_id 为 2 和 4 的记录不存在。

问题重现

graph LR
    A[事务A] -->|持有| C["Gap锁(1,3)"]
    A -->|请求| E["插入意向锁(4)"]

    B[事务B] -->|持有| D["Gap锁(3,5)"]
    B -->|请求| F["插入意向锁(2)"]

    C -.->|阻塞| F
    D -.->|阻塞| E

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#f9f,stroke:#333,stroke-width:2px
    style C fill:#bbf,stroke:#333,stroke-width:2px
    style D fill:#bbf,stroke:#333,stroke-width:2px
    style E fill:#bfb,stroke:#333,stroke-width:2px
    style F fill:#bfb,stroke:#333,stroke-width:2px

这里的关键是理解:在 RR 隔离级别下,即使查询的记录不存在,FOR UPDATE也会在该位置获取间隙锁或临键锁。当执行 INSERT 操作时,事务会先请求一个插入意向锁(Insert Intention Lock),这是一种特殊的间隙锁。虽然多个事务可以在同一个间隙内持有不同的插入意向锁(允许并发插入不同位置),但插入意向锁会与普通间隙锁冲突,导致等待。

当两个事务交叉持有间隙锁并尝试插入时,就会形成死锁。

临键锁案例补充

为更好理解临键锁,考虑以下场景:

-- 在REPEATABLE READ隔离级别下
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 5 AND 15 FOR UPDATE;
-- 其他操作...
COMMIT;

这个 SELECT 语句会做什么?它会:

  1. 对 id 值为 5 到 15 的记录加记录锁(Record Lock)
  2. 对 id 值范围(15, "下一个索引值")加间隙锁(Gap Lock)
  3. 这两种锁合起来形成临键锁(Next-Key Lock)

这种锁定策略可以防止其他事务在锁定范围内插入新记录(防幻读),同时允许对锁定范围之外的记录进行修改。

死锁日志分析

------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-04-17 15:12:45 0x7f9a1c3a2700
*** (1) TRANSACTION:
TRANSACTION 10801, ACTIVE 6 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 157, OS thread handle 140301189614336, query id 9712 localhost root update
INSERT INTO user_points(user_id,points,created_at) VALUES(4,120,NOW())

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 25 page no 4 n bits 72 index idx_user_id of table `test`.`user_points` trx id 10801 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000005; asc     ;;
 1: len 4; hex 80000005; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 10802, ACTIVE 3 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 158, OS thread handle 140301235861248, query id 9713 localhost root update
INSERT INTO user_points(user_id,points,created_at) VALUES(2,110,NOW())

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 25 page no 4 n bits 72 index idx_user_id of table `test`.`user_points` trx id 10802 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000003; asc     ;;
 1: len 4; hex 80000003; asc     ;;

*** WE ROLL BACK TRANSACTION (2)

日志中的关键信息:

  • lock_mode X locks gap before rec insert intention waiting:表示事务尝试获取插入意向锁,但被另一个事务的间隙锁阻止
  • 两个事务分别持有对方需要的间隙锁,形成死锁

解决方案

按照有效性排序:

  1. 降低隔离级别(最直接有效):将隔离级别从 REPEATABLE READ 降低到 READ COMMITTED,这样就不会使用 Gap 锁
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

注意:隔离级别调整需谨慎评估业务一致性需求,避免幻读问题对应用造成影响。

  1. 使用 INSERT ON DUPLICATE KEY UPDATE:替代先 SELECT 再 INSERT 的模式
INSERT INTO user_points(user_id,points,created_at)
VALUES(2,110,NOW())
ON DUPLICATE KEY UPDATE points=110, created_at=NOW();
  1. 拆分事务:不要在同一个事务中先查询再插入
-- 原来的模式
BEGIN;
SELECT * FROM user_points WHERE user_id=2 FOR UPDATE;
-- 如果不存在则插入
INSERT INTO user_points(user_id,points,created_at) VALUES(2,110,NOW());
COMMIT;

-- 改进后
-- 查询阶段(可以使用共享锁或不加锁)
SELECT * FROM user_points WHERE user_id=2;

-- 插入阶段(单独事务)
BEGIN;
INSERT INTO user_points(user_id,points,created_at) VALUES(2,110,NOW())
ON DUPLICATE KEY UPDATE points=110, created_at=NOW();
COMMIT;
  1. 索引优化:确保查询条件有合适的索引

案例三:外键约束导致的死锁

场景描述

外键约束也是死锁的常见原因。考虑以下两个表:

CREATE TABLE `departments` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

CREATE TABLE `employees` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `dept_id` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_dept_id` (`dept_id`),
  CONSTRAINT `fk_dept_id` FOREIGN KEY (`dept_id`) REFERENCES `departments` (`id`)
) ENGINE=InnoDB;

问题重现

这里的关键点:外键约束会导致额外的锁请求。当我们修改引用字段时,InnoDB 需要验证引用的完整性:

  1. 如果更新子表的外键值(如 dept_id),InnoDB 会检查父表(departments)中对应的值是否存在,需要在父表相应记录上添加共享锁(S 锁),而非排他锁
  2. 如果更新或删除父表中被引用的记录,InnoDB 会检查子表是否有依赖,可能添加父子表间的额外锁

这些额外的锁请求大大增加了死锁的可能性。

死锁日志分析

------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-04-17 16:03:11 0x7f9a1c3a2700
*** (1) TRANSACTION:
TRANSACTION 10809, ACTIVE 8 sec updating or deleting
mysql tables in use 2, locked 2
LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
MySQL thread id 159, OS thread handle 140301189614336, query id 9725 localhost root updating
UPDATE employees SET dept_id=2 WHERE id=1

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `test`.`employees` trx id 10809 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000002a3d; asc     *=;;
 2: len 7; hex 81000000110110; asc       ;;
 3: len 3; hex 426f62; asc Bob;;
 4: len 4; hex 80000001; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 10810, ACTIVE 4 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 160, OS thread handle 140301235861248, query id 9726 localhost root update
INSERT INTO departments(id,name) VALUES(3,'HR')

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 4 n bits 72 index PRIMARY of table `test`.`departments` trx id 10810 lock_mode X locks rec but not gap waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** WE ROLL BACK TRANSACTION (2)

解决方案

按照有效性排序:

  1. 调整事务顺序(最有效):按照固定顺序访问相关表
-- 统一先操作父表,再操作子表
BEGIN;
UPDATE departments SET name='IT' WHERE id=1;
UPDATE employees SET dept_id=2 WHERE id=1;
COMMIT;
  1. 设置外键约束时使用RESTRICT而非CASCADE
ALTER TABLE employees DROP FOREIGN KEY fk_dept_id;
ALTER TABLE employees ADD CONSTRAINT fk_dept_id FOREIGN KEY (dept_id)
REFERENCES departments(id) ON DELETE RESTRICT ON UPDATE RESTRICT;
  1. 分割事务:避免在一个事务中同时操作多个相关表
-- 分两个事务操作
-- 事务1:更新父表
BEGIN;
UPDATE departments SET name='IT' WHERE id=1;
COMMIT;

-- 事务2:更新子表
BEGIN;
UPDATE employees SET dept_id=2 WHERE id=1;
COMMIT;
  1. 减少外键使用(谨慎考虑):在高并发系统中考虑减少外键约束,由应用程序保证数据一致性

案例四:自增锁死锁

场景描述

自增锁是一种特殊的锁,用于处理 AUTO_INCREMENT 列的值生成。在高并发场景下,自增锁争用也可能导致死锁。

MySQL 通过innodb_autoinc_lock_mode参数控制自增锁行为:

  • innodb_autoinc_lock_mode=0:传统模式,所有插入语句都需要获取表级锁
  • innodb_autoinc_lock_mode=1(默认值):混合模式,其特点是:

    • 对于行数已知的插入(如INSERT ... VALUES),使用轻量级互斥锁
    • 仅对于行数未知的插入(如INSERT ... SELECT),才使用表级锁
  • innodb_autoinc_lock_mode=2:交叉模式,所有插入使用轻量级互斥锁,不保证自增值连续性

问题重现

考虑以下场景:

CREATE TABLE `order_items` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_id` int NOT NULL,
  `product_id` int NOT NULL,
  `quantity` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB;

这里的关键点是理解:在innodb_autoinc_lock_mode=1(默认)模式下,INSERT ... SELECT等行数不确定的语句会获取表级自增锁,而INSERT ... VALUES等行数确定的语句仅获取轻量级互斥锁。当两种类型的插入混合使用时,容易导致锁冲突和死锁。

解决方案

  1. 调整自增锁模式(最有效):
-- 全局设置,需要重启MySQL生效
SET GLOBAL innodb_autoinc_lock_mode = 2;

-- 或在配置文件中设置
[mysqld]
innodb_autoinc_lock_mode = 2

注意:mode=2不保证自增值连续性,但能显著减少锁冲突。

  1. 批量插入优化:尽量在一个语句中插入多行数据,明确指定字段顺序
-- 替代多次单行插入
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (101, 1, 2), (101, 2, 1), (101, 3, 5);
  1. 使用应用生成的 ID:对于高并发系统,考虑使用应用层生成唯一 ID
-- 使用应用生成的UUID或其他唯一ID
INSERT INTO order_items (id, order_id, product_id, quantity)
VALUES (GENERATED_ID, 101, 1, 2);

死锁排查工具与技巧

1. 使用 SHOW ENGINE INNODB STATUS 查看死锁信息

SHOW ENGINE INNODB STATUS\G

关注输出中的"LATEST DETECTED DEADLOCK"部分。

2. 开启死锁日志记录

SET GLOBAL innodb_print_all_deadlocks = 1;

这会将所有死锁信息记录到 MySQL 错误日志中。

3. 使用 performance_schema 监控锁等待

-- 启用performance_schema
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE 'wait/lock/metadata/%' OR NAME LIKE 'wait/lock/innodb/%';

-- 查询当前锁等待
SELECT * FROM performance_schema.events_waits_current
WHERE EVENT_NAME LIKE 'wait/lock%';

-- 查询锁等待历史
SELECT * FROM performance_schema.events_waits_history
WHERE EVENT_NAME LIKE 'wait/lock%';

4. 使用工具分析死锁

  • pt-deadlock-logger(Percona 工具集)
  • MySQL 企业版监控工具

5. 死锁监控与预警

graph TD
    A[设置死锁监控] --> B[开启死锁日志]
    B --> C[编写脚本定期分析日志]
    C --> D[设置告警阈值]
    D --> E[超过阈值发送告警]
    E --> F[立即排查问题]

其他常见死锁类型简述

元数据锁死锁

当 DDL 操作(如 ALTER TABLE)与 DML 操作(如 INSERT、UPDATE)并发执行时,可能出现元数据锁死锁。

解决方案

  • 将 DDL 操作安排在低峰期
  • 使用在线 DDL 工具(如 pt-online-schema-change):这些工具通过创建临时表、复制数据和表结构交换来避免长时间锁表
  • MySQL 5.6+的ALGORITHM=INPLACE或 8.0+的ALGORITHM=INSTANT参数可实现某些 DDL 操作的在线执行
  • 避免长事务与 DDL 并发

预防死锁的实用建议

  1. 控制事务大小和持续时间

    • 保持事务短小、快速完成
    • 只在必要时使用事务
  2. 合理设计数据访问顺序

    • 按照主键或索引顺序访问数据
    • 使用 ORDER BY 确保访问顺序一致
  3. 选择合适的隔离级别

    • 不需要可重复读的场景使用 READ COMMITTED
    • 了解每个隔离级别的锁行为
  4. 优化索引设计

    • 确保查询条件有合适的索引
    • 避免使用不必要的锁定查询
  5. 谨慎使用外键

    • 高并发系统可考虑减少外键约束
    • 使用 RESTRICT 代替 CASCADE
  6. 应用层重试机制

    • 捕获死锁异常并实现重试逻辑
int retries = 3;
boolean success = false;
while (retries > 0 && !success) {
    Connection conn = null;
    try {
        conn = dataSource.getConnection();
        conn.setAutoCommit(false);  // 开启事务

        // 数据库操作
        PreparedStatement ps = conn.prepareStatement("UPDATE...");
        ps.executeUpdate();

        conn.commit();  // 提交事务
        success = true;
    } catch (SQLException e) {
        if (conn != null) {
            try {
                conn.rollback();  // 回滚事务
            } catch (SQLException ex) {
                // 处理回滚异常
            }
        }

        if (e.getErrorCode() == 1213 && retries > 1) { // MySQL死锁错误码1213
            retries--;
            Thread.sleep(100); // 短暂延迟后重试
        } else {
            throw e; // 重试失败或其他错误,继续抛出
        }
    } finally {
        if (conn != null) {
            conn.close();
        }
    }
}
  1. 使用乐观锁替代悲观锁

    • 适合读多写少的场景
    • 使用版本号或时间戳实现
    • 注意:高冲突场景下乐观锁可能导致频繁重试

死锁排查流程

flowchart TD
    A[发现死锁错误] --> B{是否能重现?}
    B -->|能| C[分析复现步骤]
    B -->|不能| D[查看死锁日志]
    C --> E[识别锁类型与资源]
    D --> E
    E --> F[分析事务访问顺序]
    F --> G[确认死锁原因]
    G --> H{原因类型?}
    H -->|访问顺序| I[统一访问顺序]
    H -->|锁范围| J[调整隔离级别/锁范围]
    H -->|事务设计| K[优化事务设计]
    H -->|外键约束| L[调整外键策略]
    I --> M[验证解决方案]
    J --> M
    K --> M
    L --> M
    M --> N[监控并预防]

总结

死锁类型典型特征解决方案开发成本适用场景
行锁更新冲突多个事务更新相同或相关行数据统一访问顺序、减小事务范围、使用乐观锁高并发多表更新业务
Gap 锁冲突REPEATABLE READ 隔离级别下插入操作死锁降低隔离级别、使用 ON DUPLICATE KEY UPDATE需要频繁插入查询的应用
外键约束死锁父子表并发操作导致锁冲突减少外键使用、调整事务顺序、使用 RESTRICT 约束中高具有复杂关系模型的系统
元数据锁死锁DDL 和 DML 语句混合执行将 DDL 操作放在低峰期、使用在线 DDL 工具需要频繁架构变更的应用
自增锁死锁多事务同时插入自增列调整 innodb_autoinc_lock_mode、批量插入高并发写入场景

通过本文的案例和分析,相信你已经对 InnoDB 死锁有了更深入的理解。记住,死锁不可完全避免,但可以通过合理的设计和实践大大减少其发生频率和影响。当死锁发生时,保持冷静,按照本文提供的排查流程,你一定能够找到问题所在并解决它。


感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!

如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~


异常君
1 声望1 粉丝

在 Java 的世界里,永远有下一座技术高峰等着你。我愿做你登山路上的同频伙伴,陪你从看懂代码到写出让自己骄傲的代码。咱们,代码里见!