如何避免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 容器选型案例,一篇看懂》

相关推荐
野犬寒鸦27 分钟前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
霖霖总总30 分钟前
[小技巧66]当自增主键耗尽:MySQL 主键溢出问题深度解析与雪花算法替代方案
mysql·算法
逍遥德1 小时前
如何学编程之01.理论篇.如何通过阅读代码来提高自己的编程能力?
前端·后端·程序人生·重构·软件构建·代码规范
MX_93592 小时前
Spring的bean工厂后处理器和Bean后处理器
java·后端·spring
海奥华22 小时前
mysql索引
数据库·mysql
程序员泠零澪回家种桔子3 小时前
Spring AI框架全方位详解
java·人工智能·后端·spring·ai·架构
javachen__3 小时前
mysql新老项目版本选择
数据库·mysql
Dxy12393102163 小时前
MySQL如何高效查询表数据量:从基础到进阶的优化指南
数据库·mysql
Dying.Light3 小时前
MySQL相关问题
数据库·mysql
源代码•宸3 小时前
大厂技术岗面试之谈薪资
经验分享·后端·面试·职场和发展·golang·大厂·职级水平的薪资