数据库死锁
1. 什么是死锁?(经典转账场景举例)
假设数据库里有两条记录:A账户(100元) 和 B账户(100元)。
事务1(小明) 要做:给A减10元,给B加10元。
事务2(小红) 要做:给B减10元,给A加10元。
时间线执行流程:
- 事务1 拿到 A账户 的锁(准备扣A的钱)。
- 事务2 拿到 B账户 的锁(准备扣B的钱)。
- 事务1 想拿 B账户 的锁,发现被事务2占着,于是等待。
- 事务2 想拿 A账户 的锁,发现被事务1占着,于是等待。
结果:事务1等事务2释放B,事务2等事务1释放A。双方谁也不松手,永远僵持,这就是死锁(Deadlock)。
2. 死锁产生的4个必要条件(缺一不可)
数据库死锁必须同时满足以下4个条件,只要打破任意一个,死锁就不会产生:
- 互斥:同一时间,一把锁只能被一个事务持有(A账户不能同时被两个人修改)。
- 占有并等待:事务1拿着A锁,还在等着拿B锁,不释放已持有的锁。
- 不可抢占:事务1不主动释放A锁,事务2无法强行抢走锁资源。
- 循环等待:事务1等事务2,事务2等事务1,形成资源等待环路。
3. 数据库如何处理死锁?
以MySQL、Oracle为例,数据库不会放任事务无限阻塞,内置死锁检测机制:
- 数据库会评估两个事务执行代价(修改数据量、耗时等),选出牺牲品(Victim);
- 强制回滚(Rollback)牺牲品事务,释放它占用的全部锁资源;
- 剩余事务获取全部所需锁,正常执行完成。
被回滚的事务会抛出报错:
Deadlock found when trying to get lock; try restarting transaction
核心结论:死锁不会造成数据库崩溃,是数据库主动“劝架”,业务侧只需重试事务即可。
4. 死锁 VS 锁等待(Lock Wait)区别
- 锁等待:单方向排队等待,事务2等待事务1释放锁,事务1执行完毕一定会释放锁,属于正常并发排队现象。
- 死锁:双向循环互相等待,双方永远无法主动释放锁,必须依靠数据库强制回滚其中一个事务才能解除阻塞。
5. 开发中如何预防死锁?(优先预防,优于事后处理)
数据库虽能自动处理死锁,但频繁死锁会大幅降低数据库并发性能,推荐以下预防方案:
- 固定资源访问顺序:所有事务统一按「先A后B」固定顺序更新数据,杜绝交叉等待环路。
- 缩短事务执行时长:事务内部禁止耗时网络请求、大量计算逻辑,锁持有时间越短,锁冲突概率越低。
- 使用更低隔离级别:使用「读已提交(Read Committed)」替代默认「可重复读(Repeatable Read)」,缩小间隙锁锁定范围。
- 添加合理业务索引:更新、修改语句必须命中索引;无索引会触发表锁,极大提升死锁发生概率。
6. 对比总结:内存溢出(OOM) vs 数据库死锁(Deadlock)
| 对比维度 | 内存溢出 (OOM) | 数据库死锁 (Deadlock) |
|---|---|---|
| 本质 | 内存资源耗尽,资源不足 | 锁资源互相抢占,循环等待 |
| 发生位置 | 应用服务器内存 / JVM堆 | InnoDB等数据库存储引擎 |
| 业务后果 | 进程崩溃,OOM杀手终止进程 | 单个事务回滚,应用抛出异常,服务进程正常存活 |
| 解决方案 | 扩容内存、修复内存泄漏、优化代码 | 事务重试、统一SQL更新顺序、补充索引 |
7. 面试/实战小技巧
Spring框架捕获死锁异常:DeadlockLoserDataAccessException
最佳实践:不修改核心业务逻辑,增加事务重试机制(例如重试3次),数据库已完成锁释放,重试通常能执行成功。

