大家好,我是大华!
死锁是数据库里很常见的问题:两个或多个事务互相等待对方释放锁,结果谁也动不了。
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. 所有事务按同一个顺序操作表
这是避免死锁最有效的一招!
比如:如果多个事务都要改 users 和 orders 表,那就统一先改 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 种方法亲测有效》