什么是数据库死锁,怎么解决?
2025年12月29日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,核心是想考察你是否:
- 理解并发场景下的数据一致性问题:面试官不仅想知道“死锁”是什么,更是想知道你是否理解在多个事务并发操作时,数据库为了保证 ACID 特性而引入的锁机制会带来哪些副作用。
- 具备实际的问题诊断和解决能力:你是否遇到过死锁,是否知道如何从数据库日志或监控中发现死锁,以及最重要的,如何从应用代码层面去预防它。
- 掌握数据库事务与锁的基本原理:死锁是事务和锁机制下的一个典型现象。回答此题能反映出你对事务隔离级别、锁的类型(行锁、间隙锁等)以及资源竞争的基本理解。
- 具有系统性的解决方案思路:解决方案是否涵盖从紧急处理到根治预防,从数据库层面到应用架构层面的完整维度。
核心答案
数据库死锁是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,它们都将无法推进下去。
解决思路分为两步:
- 检测与恢复(治标):大部分数据库(如 InnoDB)具备死锁检测与自动回滚机制。一旦检测到死锁,会选择一个“代价最小”的事务进行回滚(作为“牺牲者”),从而打破死锁,让其他事务得以继续。
- 避免与预防(治本):这是开发者的核心职责。主要策略包括:
- 保持一致的访问顺序:在应用代码中,约定所有事务对多个资源的访问(如多行数据、多张表)都遵循相同的全局顺序。
- 大事务拆小:缩小事务范围,减少锁的持有时间和范围,降低冲突概率。
- 使用低隔离级别或乐观锁:在业务允许的情况下,使用
READ COMMITTED隔离级别或基于版本号的乐观锁机制,减少阻塞。 - 添加合理的索引:通过索引进行精准锁定,避免锁住不必要的行或升级为表锁。
技术深度解析
原理/机制:
- 死锁的必要条件:互斥、占有且等待、不可剥夺、循环等待。数据库死锁是典型的“循环等待”。
- InnoDB 的死锁处理:引擎内部有一个等待图数据结构。当新的事务请求锁需要等待时,它会检查等待图中是否存在环。如果存在环,则判定为死锁,并立即触发回滚(而不是等待锁超时)。
代码示例:一个典型的死锁场景是交叉更新。
// ❌ 错误示例:两个线程/事务以相反顺序更新同两行数据,极易引发死锁。
// 事务1执行:
update account set balance = balance - 100 where id = 1;
update account set balance = balance + 100 where id = 2;
// 事务2同时执行:
update account set balance = balance - 200 where id = 2; // 被事务1阻塞
update account set balance = balance + 200 where id = 1; // 被事务2阻塞 -> 死锁!
// ✅ 解决:约定所有事务按固定顺序(如按id升序)访问资源。
public void transferMoney(Transaction tx, int fromId, int toId, BigDecimal amount) {
int firstId = Math.min(fromId, toId);
int secondId = Math.max(fromId, toId);
// 先锁定id小的账户
executeUpdate(tx, "UPDATE account SET balance = balance - ? WHERE id = ?", amount, firstId);
// 再锁定id大的账户
executeUpdate(tx, "UPDATE account SET balance = balance + ? WHERE id = ?", amount, secondId);
}
// 无论从1转给2,还是从2转给1,都变成“先锁id=1,再锁id=2”,消除了循环等待。
最佳实践与常见误区
- 最佳实践:
- 在应用层做排序:如上例所示,这是最有效且低成本的预防手段。
- 设置合理的事务超时时间:通过
SET innodb_lock_wait_timeout设置单个锁等待的超时时间(默认为50秒),避免线程长时间挂起。但这不是解决死锁,而是让应用能“失败重试”。 - 监控与告警:开启数据库的死锁日志(
SHOW ENGINE INNODB STATUS或查看error log),并配置监控,当死锁频率异常升高时能及时报警。
- 常见误区:
- 认为死锁完全由数据库自动解决即可:数据库的回滚只是兜底,会造成事务失败,影响用户体验。根本责任在于开发者在设计时预防。
- 认为重试能解决所有死锁:在死锁发生时,简单地加入应用层重试逻辑是可行的,但重试次数和间隔需要谨慎设计,并且必须确保业务操作的幂等性,否则可能导致数据重复或状态错误。
- 忽略索引导致锁升级:如果
WHERE条件没有命中索引,InnoDB 会退化为锁住整张表或大量间隙,这会极大增加死锁概率。
总结
数据库死锁是并发事务争夺资源的恶性循环,其根治关键在于应用层通过统一资源访问顺序来打破 “循环等待”,而数据库的自动检测与回滚机制则是防止系统瘫痪的最后一道保险。