Skip to content

MySQL 死锁告警

死锁检测方法

内置检测机制

InnoDB死锁检测

InnoDB存储引擎会自动检测死锁,当检测到死锁时,会选择一个代价较小的事务进行回滚。

死锁检测算法

InnoDB使用等待图(Wait-For Graph)算法来检测死锁:

  1. 为每个事务创建一个节点
  2. 当事务A等待事务B持有的资源时,创建一条从A到B的边
  3. 如果等待图中存在环,则存在死锁

查看死锁信息

方法1:查看错误日志

bash
# 查看错误日志中的死锁信息
grep -i "deadlock" /path/to/error.log

方法2:使用SHOW ENGINE INNODB STATUS

sql
-- 查看InnoDB状态,包含死锁信息
SHOW ENGINE INNODB STATUS\G;

方法3:使用Performance Schema

sql
-- 查看死锁相关事件
SELECT * FROM performance_schema.events_waits_current WHERE event_name LIKE '%lock%';

-- 查看事务等待情况
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;

死锁信息解读

典型死锁信息

LATEST DETECTED DEADLOCK
------------------------
2023-12-01 10:00:00 0x7f1234567890
*** (1) TRANSACTION:
TRANSACTION 123456, ACTIVE 10 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 42, OS thread handle 139709834567890, query id 123456 localhost root updating
UPDATE users SET balance = balance - 100 WHERE id = 1;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 123 page no 4 n bits 72 index PRIMARY of table `test`.`users` trx id 123456 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 00000001e240; asc     @;;
 2: len 7; hex b60000019d0110; asc        ;;  
 3: len 4; hex 800003e8; asc     ;;  

*** (2) TRANSACTION:
TRANSACTION 123457, ACTIVE 8 sec starting index read
mysql tables in use 1, locked 1
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 43, OS thread handle 139709834567891, query id 123457 localhost root updating
UPDATE users SET balance = balance + 100 WHERE id = 2;
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 123 page no 4 n bits 72 index PRIMARY of table `test`.`users` trx id 123457 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;  
 1: len 6; hex 00000001e240; asc     @;;  
 2: len 7; hex b60000019d0110; asc        ;;  
 3: len 4; hex 800003e8; asc     ;;  
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 123 page no 5 n bits 72 index PRIMARY of table `test`.`users` trx id 123457 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;  
 1: len 6; hex 00000001e241; asc     A;;  
 2: len 7; hex b60000019d0110; asc        ;;  
 3: len 4; hex 800003e8; asc     ;;  

*** WE ROLL BACK TRANSACTION (1)

信息解读

  • 事务1:正在执行 UPDATE users SET balance = balance - 100 WHERE id = 1,等待id=1的行锁
  • 事务2:正在执行 UPDATE users SET balance = balance + 100 WHERE id = 2,持有id=1的行锁,等待id=2的行锁
  • 死锁原因:两个事务相互等待对方持有的锁
  • 解决方案:MySQL回滚了事务1

死锁告警配置

监控工具配置

Prometheus + Grafana

配置Prometheus采集

prometheus.yml

yaml
scrape_configs:
  - job_name: 'mysql'
    static_configs:
      - targets: ['localhost:9104']
    scrape_interval: 15s
配置Grafana告警

告警规则

yaml
groups:
- name: mysql_deadlock_alerts
  rules:
  - alert: MySQLDeadlockDetected
    expr: mysql_global_status_innodb_deadlocks > 0
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "MySQL死锁检测"
      description: "实例 {{ $labels.instance }} 在过去5分钟内检测到死锁"

  - alert: MySQLFrequentDeadlocks
    expr: rate(mysql_global_status_innodb_deadlocks[5m]) > 1
    for: 10m
    labels:
      severity: critical
    annotations:
      summary: "MySQL频繁死锁"
      description: "实例 {{ $labels.instance }} 在过去10分钟内频繁发生死锁,速率为 {{ $value }} 次/分钟"

MySQL Enterprise Monitor

配置死锁告警
  1. 登录MySQL Enterprise Monitor
  2. 导航到"Alerts" → "Manage Rules"
  3. 找到"Deadlock Detected"规则
  4. 配置告警阈值和通知方式

开源监控工具

Percona Monitoring and Management (PMM)
  1. 安装PMM客户端
  2. 配置MySQL实例监控
  3. 使用预定义的死锁监控面板
  4. 配置告警规则
Zabbix

配置监控项

# 监控项键值
mysql.status[Innodb_deadlocks]

# 触发器表达式
{Template App MySQL:mysql.status[Innodb_deadlocks].diff(5m)}>0

自定义监控脚本

死锁监控脚本

bash
#!/bin/bash

# MySQL连接信息
HOST="localhost"
PORT="3306"
USER="monitor"
PASSWORD="password"

# 获取死锁数量
DEADLOCKS=$(mysql -h $HOST -P $PORT -u $USER -p$PASSWORD -e "SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';" | grep Innodb_deadlocks | awk '{print $2}')

# 获取死锁详情
DEADLOCK_DETAILS=$(mysql -h $HOST -P $PORT -u $USER -p$PASSWORD -e "SHOW ENGINE INNODB STATUS\G" | grep -A 50 "LATEST DETECTED DEADLOCK")

# 检查死锁
if [ $DEADLOCKS -gt 0 ]; then
    echo "警告: 检测到 $DEADLOCKS 个死锁"
    echo "死锁详情:"
    echo "$DEADLOCK_DETAILS"
    
    # 发送告警(可集成邮件、短信等)
    # mail -s "MySQL死锁告警" admin@example.com <<< "检测到死锁: $DEADLOCKS 个\n$DEADLOCK_DETAILS"
fi

# 输出指标(用于监控系统采集)
echo "mysql_deadlocks $DEADLOCKS"

定时执行

bash
# 添加到crontab
*/5 * * * * /path/to/deadlock_monitor.sh >> /var/log/mysql/deadlock_monitor.log 2>&1

应用层监控

捕获死锁异常

Java应用示例
java
try {
    // 执行事务操作
    statement.executeUpdate("UPDATE users SET balance = balance - 100 WHERE id = 1");
} catch (SQLException e) {
    // 捕获死锁异常
    if (e.getErrorCode() == 1213) { // MySQL死锁错误码
        logger.warn("检测到死锁,正在重试...", e);
        // 实现重试逻辑
        retryTransaction();
    } else {
        throw e;
    }
}
Python应用示例
python
import MySQLdb

def execute_with_retry(connection, query, max_retries=3):
    retries = 0
    while retries < max_retries:
        try:
            cursor = connection.cursor()
            cursor.execute(query)
            connection.commit()
            return True
        except MySQLdb.Error as e:
            if e.args[0] == 1213:  # 死锁错误码
                print(f"检测到死锁,正在重试 ({retries+1}/{max_retries})...")
                retries += 1
                connection.rollback()
                time.sleep(1)  # 等待1秒后重试
            else:
                raise
    return False

死锁处理策略

紧急处理步骤

步骤1:确认死锁发生

sql
-- 查看死锁数量
SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';

-- 查看详细死锁信息
SHOW ENGINE INNODB STATUS\G;

步骤2:分析死锁原因

  1. 查看死锁事务:从InnoDB状态输出中分析涉及的事务
  2. 识别冲突SQL:确定导致死锁的SQL语句
  3. 分析锁类型:了解使用的锁类型(行锁、表锁等)
  4. 确定资源争用:识别冲突的资源(行、索引等)

步骤3:打破死锁

MySQL会自动检测并打破死锁,选择一个代价较小的事务进行回滚。但在某些情况下,可能需要手动干预:

sql
-- 查看当前事务
SHOW PROCESSLIST;

-- 终止导致死锁的事务
KILL [process_id];

根本解决方案

优化SQL语句

  1. 减少事务范围:将大事务拆分为多个小事务
  2. 优化查询条件:确保使用索引,减少锁范围
  3. 统一访问顺序:多个事务按相同顺序访问资源
  4. 避免长事务:尽量缩短事务执行时间
  5. 使用合理的隔离级别:根据业务需求选择合适的隔离级别

示例:优化前的代码

sql
-- 事务1
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 1;
UPDATE users SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- 事务2
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 2;
UPDATE users SET balance = balance + 100 WHERE id = 1;
COMMIT;

示例:优化后的代码

sql
-- 统一访问顺序:先访问id小的记录
START TRANSACTION;
-- 先更新id=1
UPDATE users SET balance = balance - 100 WHERE id = 1;
-- 再更新id=2
UPDATE users SET balance = balance + 100 WHERE id = 2;
COMMIT;

调整InnoDB配置

参数名描述建议值说明
innodb_deadlock_detect启用死锁检测ON通常建议开启
innodb_lock_wait_timeout锁等待超时时间50单位秒,根据业务需求调整
innodb_rollback_on_timeout超时后回滚整个事务OFF建议保持默认值
innodb_autoinc_lock_mode自增锁模式2减少自增锁争用

配置示例

ini
# my.cnf配置
innodb_deadlock_detect = ON
innodb_lock_wait_timeout = 50
innodb_autoinc_lock_mode = 2

应用层优化

  1. 实现重试机制:捕获死锁异常并自动重试
  2. 使用乐观锁:通过版本号或时间戳实现乐观锁
  3. 批量操作拆分:将批量操作拆分为多个小批量操作
  4. 异步处理:将非关键操作改为异步处理
  5. 合理使用索引:确保查询使用索引,减少锁范围

乐观锁示例

sql
-- 表结构添加版本号字段
ALTER TABLE users ADD COLUMN version INT DEFAULT 0;

-- 更新操作
UPDATE users SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = :current_version;

-- 检查更新是否成功
IF affected_rows = 0 THEN
    -- 版本冲突,需要重新读取数据
END IF;

死锁预防措施

开发规范

  1. 统一访问顺序:所有事务按相同顺序访问表和行
  2. 减少事务大小:尽量缩短事务执行时间和范围
  3. 使用合理的隔离级别:根据业务需求选择隔离级别
  4. 避免长时间锁定:避免在事务中执行耗时操作
  5. 合理使用索引:确保查询使用索引,减少锁范围
  6. 避免循环依赖:设计系统时避免循环依赖关系

运维规范

  1. 定期监控:监控死锁数量和频率
  2. 性能优化:定期优化SQL和索引
  3. 配置调优:根据业务需求调整InnoDB配置
  4. 容量规划:确保系统资源充足,避免资源争用
  5. 定期审计:审计应用代码,发现潜在的死锁风险
  6. 培训教育:培训开发人员了解死锁原理和预防措施

监控指标

指标名描述告警阈值监控频率
Innodb_deadlocks死锁数量>05分钟
Innodb_lock_wait_time_avg平均锁等待时间>100ms5分钟
Innodb_lock_waits锁等待次数>105分钟
Innodb_row_lock_time_avg平均行锁等待时间>50ms5分钟
Innodb_row_lock_waits行锁等待次数>505分钟

常见死锁场景与解决方案

场景1:两个事务交叉更新

场景描述

sql
-- 事务1
START TRANSACTION;
UPDATE table1 SET col1 = 1 WHERE id = 1;
UPDATE table2 SET col2 = 1 WHERE id = 1;
COMMIT;

-- 事务2
START TRANSACTION;
UPDATE table2 SET col2 = 1 WHERE id = 1;
UPDATE table1 SET col1 = 1 WHERE id = 1;
COMMIT;

解决方案

  • 统一访问顺序:两个事务按相同顺序访问表
  • 使用分布式锁:对于跨表操作,考虑使用分布式锁

场景2:范围查询与单行更新冲突

场景描述

sql
-- 事务1:范围查询(可能锁定多个行)
START TRANSACTION;
SELECT * FROM users WHERE age > 30 FOR UPDATE;
-- 处理数据
COMMIT;

-- 事务2:单行更新
START TRANSACTION;
UPDATE users SET name = 'New Name' WHERE id = 1;
COMMIT;

解决方案

  • 使用更精确的查询条件:尽量使用主键或唯一索引进行查询
  • 减少锁定范围:只锁定必要的行
  • 使用READ COMMITTED隔离级别:减少锁定范围

场景3:外键约束导致的死锁

场景描述

sql
-- 表结构
CREATE TABLE parent (id INT PRIMARY KEY);
CREATE TABLE child (id INT PRIMARY KEY, parent_id INT, FOREIGN KEY (parent_id) REFERENCES parent(id));

-- 事务1
START TRANSACTION;
INSERT INTO child (id, parent_id) VALUES (1, 1);
-- 等待
COMMIT;

-- 事务2
START TRANSACTION;
DELETE FROM parent WHERE id = 1;
-- 等待
COMMIT;

解决方案

  • 先操作子表:删除时先删除子表数据,再删除父表数据
  • 批量操作拆分:将批量删除拆分为多个小批量操作
  • 使用级联删除:配置外键为级联删除

场景4:自增锁导致的死锁

场景描述

多个事务同时插入带有自增主键的表,可能会因为自增锁争用而导致死锁。

解决方案

  • 调整自增锁模式:设置innodb_autoinc_lock_mode = 2
  • 批量插入:使用批量插入减少自增锁争用
  • 预分配自增ID:对于高并发场景,考虑预分配自增ID

场景5:间隙锁导致的死锁

场景描述

在REPEATABLE READ隔离级别下,InnoDB会使用间隙锁来防止幻读,可能导致死锁。

sql
-- 事务1
START TRANSACTION;
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;
-- 等待
COMMIT;

-- 事务2
START TRANSACTION;
INSERT INTO users (id, age) VALUES (10, 25);
-- 等待
COMMIT;

解决方案

  • 使用READ COMMITTED隔离级别:减少间隙锁的使用
  • 使用更精确的查询条件:尽量使用唯一索引进行查询
  • 避免范围查询加锁:如果可能,避免使用范围查询加锁

死锁分析工具

内置工具

SHOW ENGINE INNODB STATUS

最基本的死锁分析工具,提供详细的死锁信息。

Performance Schema

提供更详细的锁相关信息,包括锁类型、锁定时间等。

sql
-- 查看锁等待情况
SELECT * FROM performance_schema.data_lock_waits;

-- 查看持有的锁
SELECT * FROM performance_schema.data_locks;

-- 查看等待事件
SELECT * FROM performance_schema.events_waits_current WHERE event_name LIKE '%lock%';

第三方工具

Percona Toolkit

pt-deadlock-logger
bash
# 安装Percona Toolkit
sudo apt-get install percona-toolkit

# 使用pt-deadlock-logger监控死锁
pt-deadlock-logger --host=localhost --user=root --password=password
pt-query-digest
bash
# 分析慢查询日志中的死锁相关信息
pt-query-digest /path/to/slow.log | grep -A 20 "Deadlock"

MySQL Enterprise Monitor

提供可视化的死锁监控面板,实时显示死锁情况。

PMM (Percona Monitoring and Management)

开源监控解决方案,包含死锁监控功能。

常见问题(FAQ)

Q1: 如何快速识别死锁问题

A1: 识别死锁的方法:

  • 查看错误日志:错误日志中会记录死锁详细信息
  • 使用SHOW ENGINE INNODB STATUS:查看InnoDB状态输出中的死锁信息
  • 监控指标:监控Innodb_deadlocks状态变量
  • 应用异常:捕获应用中的死锁异常(错误码1213)
  • 性能下降:突然的性能下降可能是死锁导致的

Q2: 死锁发生后应该如何处理

A2: 处理步骤:

  • 保持冷静:MySQL会自动检测并处理死锁
  • 查看详情:分析死锁日志,了解死锁原因
  • 优化SQL:根据死锁原因优化SQL语句
  • 调整代码:统一访问顺序,减少事务范围
  • 监控告警:设置死锁告警,及时发现问题
  • 避免手动干预:除非必要,否则不要手动终止事务

Q3: 如何预防死锁的发生

A3: 预防措施:

  • 统一访问顺序:所有事务按相同顺序访问表和行
  • 减少事务大小:将大事务拆分为多个小事务
  • 使用合理的隔离级别:根据业务需求选择隔离级别
  • 优化查询条件:确保使用索引,减少锁范围
  • 避免长时间锁定:避免在事务中执行耗时操作
  • 使用乐观锁:对于并发冲突较多的场景,使用乐观锁

Q4: 高并发场景下如何优化死锁处理

A4: 高并发优化策略:

  • 实现重试机制:捕获死锁异常并自动重试
  • 使用队列:通过队列削峰,控制并发数量
  • 预热缓存:减少事务中的数据库访问
  • 批量操作拆分:将批量操作拆分为多个小批量操作
  • 调整配置:合理设置innodb_lock_wait_timeoutinnodb_deadlock_detect
  • 监控预警:设置死锁频率预警,及时发现问题

Q5: 死锁与索引有什么关系

A5: 关系分析:

  • 索引减少锁范围:合理的索引可以减少锁定的行数
  • 全表扫描增加死锁风险:全表扫描会锁定更多行,增加死锁概率
  • 索引类型影响:不同类型的索引对锁的影响不同
  • 最佳实践
    • 为经常查询的字段创建索引
    • 避免在索引列上使用函数
    • 使用覆盖索引减少锁竞争
    • 定期优化和重建索引

Q6: 如何监控死锁情况

A6: 监控方法:

  • Prometheus + Grafana:配置死锁监控指标和告警
  • MySQL Enterprise Monitor:使用官方监控工具
  • PMM:Percona Monitoring and Management
  • 自定义脚本:定期检查死锁状态并记录
  • 应用层监控:捕获并记录应用中的死锁异常
  • 告警阈值
    • 轻微:每小时1-5次死锁
    • 中等:每小时5-20次死锁
    • 严重:每小时20+次死锁

Q7: 死锁检测会影响数据库性能吗

A7: 性能影响分析:

  • 死锁检测开销:死锁检测会消耗CPU资源
  • 高并发影响:在数千个并发事务时,死锁检测可能成为瓶颈
  • 优化建议
    • 如果系统几乎不会发生死锁,可考虑关闭死锁检测
    • 设置合理的innodb_lock_wait_timeout作为 fallback
    • 定期分析死锁日志,从根本上解决死锁问题
    • 对于超高并发系统,可考虑应用层设计避免死锁

Q8: 如何分析死锁日志

A8: 日志分析方法:

  • 错误日志:找到"LATEST DETECTED DEADLOCK"部分
  • InnoDB状态:使用SHOW ENGINE INNODB STATUS查看
  • 关键信息
    • 涉及的事务ID
    • 执行的SQL语句
    • 等待的锁类型
    • 持有的锁
    • 回滚的事务
  • 分析工具:使用Percona Toolkit的pt-deadlock-logger
  • 模式识别:识别重复发生的死锁模式,从根本上解决

Q9: 如何在开发中避免死锁

A9: 开发建议:

  • 编码规范:制定统一的数据库访问规范
  • 单元测试:编写并发测试用例
  • 代码审查:审查代码中的并发处理逻辑
  • 培训教育:提高开发人员对死锁的认识
  • 工具支持:使用静态分析工具检测潜在的死锁风险
  • 最佳实践
    • 使用短事务
    • 统一访问顺序
    • 避免在事务中调用外部服务
    • 合理使用锁粒度

Q10: 死锁与事务隔离级别有什么关系

A10: 关系分析:

  • READ UNCOMMITTED:不会发生死锁(但会有其他问题)
  • READ COMMITTED:减少死锁概率(间隙锁使用较少)
  • REPEATABLE READ:默认级别,可能发生死锁
  • SERIALIZABLE:最高隔离级别,死锁概率最高
  • 选择建议
    • 一般场景使用默认的REPEATABLE READ
    • 高并发场景考虑使用READ COMMITTED
    • 数据一致性要求高的场景使用SERIALIZABLE

案例分析

案例1:电商系统库存扣减死锁

背景

  • 高并发电商系统
  • 秒杀场景,大量用户同时抢购商品
  • 库存扣减逻辑:先检查库存,再扣减库存
  • 死锁频率:高峰期每分钟10+次死锁

代码分析

sql
-- 原始代码
START TRANSACTION;
-- 检查库存
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 扣减库存
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;

死锁原因

  • 多个事务同时执行相同的操作序列
  • 检查库存时锁定了行
  • 扣减库存时需要等待其他事务释放锁
  • 形成循环等待

解决方案

  1. 优化SQL:合并检查和更新操作
sql
-- 优化后代码
UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock > 0;
-- 检查更新是否成功
IF affected_rows = 0 THEN
    -- 库存不足
END IF;
  1. 应用层优化

    • 实现重试机制
    • 使用队列削峰
    • 预热库存缓存
  2. 配置优化

    ini
    innodb_autoinc_lock_mode = 2
    innodb_lock_wait_timeout = 30

优化效果

  • 死锁频率降低95%
  • 系统吞吐量提升30%
  • 用户体验显著改善

案例2:金融系统转账死锁

背景

  • 金融转账系统
  • 转账逻辑:从账户A扣款,向账户B存款
  • 并发量:每秒100+笔转账
  • 死锁问题:频繁出现转账死锁

代码分析

sql
-- 事务1:A向B转账
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 账户A
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 账户B
COMMIT;

-- 事务2:B向A转账
START TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 账户B
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 账户A
COMMIT;

死锁原因

  • 两个事务交叉更新账户表
  • 事务1持有账户A的锁,等待账户B的锁
  • 事务2持有账户B的锁,等待账户A的锁
  • 形成循环等待

解决方案

  1. 统一访问顺序
sql
-- 优化后代码:按账户ID顺序更新
START TRANSACTION;
-- 先更新ID较小的账户
IF from_id < to_id THEN
    UPDATE accounts SET balance = balance - amount WHERE id = from_id;
    UPDATE accounts SET balance = balance + amount WHERE id = to_id;
ELSE
    UPDATE accounts SET balance = balance + amount WHERE id = to_id;
    UPDATE accounts SET balance = balance - amount WHERE id = from_id;
END IF;
COMMIT;
  1. 使用存储过程
sql
DELIMITER //
CREATE PROCEDURE transfer(from_id INT, to_id INT, amount DECIMAL(10,2))
BEGIN
    DECLARE done INT DEFAULT 0;
    DECLARE retries INT DEFAULT 0;
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET done = 1;
    
    WHILE retries < 3 AND NOT done DO
        START TRANSACTION;
        
        -- 按ID顺序更新
        IF from_id < to_id THEN
            UPDATE accounts SET balance = balance - amount WHERE id = from_id AND balance >= amount;
            IF ROW_COUNT() = 0 THEN
                ROLLBACK;
                SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient balance';
            END IF;
            UPDATE accounts SET balance = balance + amount WHERE id = to_id;
        ELSE
            UPDATE accounts SET balance = balance + amount WHERE id = to_id;
            UPDATE accounts SET balance = balance - amount WHERE id = from_id AND balance >= amount;
            IF ROW_COUNT() = 0 THEN
                ROLLBACK;
                SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient balance';
            END IF;
        END IF;
        
        COMMIT;
        SET done = 1;
    END WHILE;
    
    IF retries >= 3 THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Transfer failed after multiple attempts';
    END IF;
END //
DELIMITER ;

优化效果

  • 死锁完全消除
  • 转账成功率提升至99.99%
  • 系统稳定性显著提高