如何避免MySQL死锁?资深DBA的9条黄金法则

大家好,我是大华!

死锁是数据库里很常见的问题:两个或多个事务互相等待对方释放锁,结果谁也动不了。

MySQL的InnoDB引擎会自己自动检测死锁,并且回滚其中一个事务来解决,但这种情况如果经常遇到的话,会很影响性能和用户体验。

其实,只要注意一些设计细节,就能大大减少甚至避免死锁。

下面是几个最实用的方法:


1. 事务要短,动作要快

事务越长,锁住数据的时间就越久,别人就越容易"撞上"你。

正确做法:只在事务里做必要的数据库操作,别把业务逻辑(比如调接口、算数据)塞进去。

sql 复制代码
-- 不推荐:事务中混杂业务逻辑
START TRANSACTION;
SELECT * FROM users WHERE id = 1;
-- 假设此处有耗时的业务处理...
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE orders SET status = 'paid' WHERE user_id = 1;
COMMIT;

-- 推荐:事务只包含必要数据库操作
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE orders SET status = 'paid' WHERE user_id = 1;
COMMIT;

2. 所有事务按同一个顺序操作表

这是避免死锁最有效的一招!

比如:如果多个事务都要改 usersorders 表,那就统一先改 users,再改 orders。不要有的先改 users,有的先改 orders。

sql 复制代码
-- 所有地方都这样写:
UPDATE users SET ... WHERE id = 1;
UPDATE orders SET ... WHERE user_id = 1;

只要顺序一致,就不会出现"A等B、B等A"的循环等待。


3. 给表加合适的索引

InnoDB 的行锁是靠索引来实现的。如果查询没用到索引,MySQL 就可能锁住整张表(或很多无关的行),大大增加死锁风险。

建议

  • 经常用来查或更新的字段(比如 user_id)要建索引。
  • EXPLAIN 看看 SQL 是否命中索引。
sql 复制代码
CREATE INDEX idx_user_id ON orders(user_id);

4. 别用太高的隔离级别(除非必要)

MySQL 默认是 REPEATABLE READ,它会加"间隙锁",防止幻读,但也更容易死锁。

如果你的业务能接受"读已提交"(比如允许看到别人刚提交的数据),可以改成:

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

这样锁的范围更小,死锁概率更低。


5. 显式加锁时要小心

如果你要用 SELECT ... FOR UPDATE 锁行,一定要确保:

  • 条件能命中索引;
  • 锁的行尽量少;
  • 事务尽快结束。
sql 复制代码
-- 安全:通过主键或索引锁定一行
SELECT * FROM accounts WHERE user_id = 1 FOR UPDATE;

如果 user_id 没索引,这条语句可能锁住成千上万行!


6. 应用层要有重试机制

死锁偶尔还是会发生。这时候,应用应该:

  • 捕获死锁错误(MySQL 错误码 1213 或 SQLSTATE '40001');
  • 自动重试几次(比如最多 2~3 次);
  • 每次重试前等一小会儿(比如 100ms、200ms...)。
java 复制代码
// 伪代码示例
for (int i = 0; i < 3; i++) {
    try {
        doDatabaseUpdate();
        break; // 成功就退出
    } catch (DeadlockException e) {
        sleep(100 * (i + 1)); // 等一下再试
    }
}

7. 大批量更新要分批做

一次更新几万行?这很容易锁住大量数据,引发死锁或卡顿。

正确做法:每次只改 500~1000 行,改完提交,再继续。

sql 复制代码
-- 分批更新
UPDATE large_table SET status = 'done'
WHERE create_time < '2023-01-01' AND status != 'done'
LIMIT 1000;
-- 循环执行,直到没有数据可更新

8. 避免热点数据被频繁修改

比如一个全局计数器,所有请求都去 UPDATE counter SET value = value + 1,那这一行就成了堵点。

解决办法:用分桶计数。

sql 复制代码
-- 把计数分散到 10 个桶里
UPDATE counter_buckets SET value = value + 1 
WHERE name = 'views' AND bucket = FLOOR(RAND() * 10);

-- 查总数时再加起来
SELECT SUM(value) FROM counter_buckets WHERE name = 'views';

9. 出问题了怎么查?

看最近一次死锁详情:

sql 复制代码
SHOW ENGINE INNODB STATUS;

LATEST DETECTED DEADLOCK部分。

查当前正在运行的事务(MySQL 8.0+):

sql 复制代码
SELECT * FROM performance_schema.data_locks;
SELECT * FROM information_schema.INNODB_TRX;

总结

1.事务要短 :别拖着不提交。 2.顺序要一致 :所有人按相同顺序改表。 3.索引要到位 :避免锁太多无关数据。 4.出错要重试 :应用层兜底处理死锁。 5.大批量要分批:别一次锁太多行。

死锁没法完全杜绝,但只要做好这些,基本就不会再被它困扰了!

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《async/await 到底要不要加 try-catch?异步错误处理最佳实践》

《如何查看 SpringBoot 当前线程数?3 种方法亲测有效》

《Java 开发必看:什么时候用 for,什么时候用 Stream?》

《别再乱 new ArrayList!8 大 Java 容器选型案例,一篇看懂》

相关推荐
@老蝴2 小时前
MySQL数据库 - 约束和联合查询
android·数据库·mysql
程序猿20232 小时前
MySQL索引使用--最左前缀法则
数据库·mysql
老华带你飞2 小时前
列车售票|基于springboot 列车售票系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习·spring
青韵3 小时前
Claude 高级工具使用解析:从上下文优化到程序化调用的工程实践
后端
汝生淮南吾在北3 小时前
SpringBoot+Vue在线考试系统
vue.js·spring boot·后端·毕业设计·毕设
树獭叔叔3 小时前
Langgraph: Human-in-the-Loop 实现机制
后端·langchain·aigc
李拾叁的摸鱼日常3 小时前
Spring Boot中OncePerRequestFilter原理与Filter单次调用控制全解析
java·后端
script.boy3 小时前
基于spring boot校园二手交易平台的设计与实现
java·spring boot·后端
用户47949283569153 小时前
XSS、CSRF、CSP、HttpOnly 全扫盲:前端安全不只是后端的事
前端·后端·面试