数据库出现死锁该如何处理
数据库死锁的核心成因是 并发事务相互持有对方需要的锁,且等待顺序循环(比如事务 A 锁了资源 1 等资源 2,事务 B 锁了资源 2 等资源 1)。解决死锁的核心思路是:避免循环等待、缩小锁粒度、减少锁持有时间。
【唯一索引、悲观锁、乐观锁】是三种核心解决方案,分别对应不同业务场景(写多读少 / 读多写少、强一致性 / 最终一致性)。下面结合 PostgreSQL 具体说明「原理 + 实现 + 适用场景 + 注意事项」
一、唯一索引:从根源避免 “锁竞争” 和 “幻读”
核心原理
唯一索引(含主键)的核心作用是 强制数据唯一性,同时能:
- 缩小锁粒度:没有索引时,事务会加「表锁」(大范围竞争);有唯一索引时,只会加「行锁」(精准锁定目标行,减少冲突);
- 避免幻读:防止并发事务重复插入相同数据(比如重复创建订单号),从而减少因 “重复插入” 引发的锁竞争;
- 强制锁顺序:基于唯一键(如 ID、订单号)操作时,事务会按唯一键的自然顺序获取锁,避免循环等待(死锁的核心诱因)。
什么是幻读
幻读就是同一事务内,相同查询条件下「结果集行数忽多忽少」的现象,本质是并发事务的 “插入 / 删除” 操作导致的。解决幻读的核心是:要么用唯一索引阻止重复插入,要么用更高的事务隔离级别(如 SERIALIZABLE)强制事务串行执行。
拓展:不可重复读:结果集「内容变化」(行还在,但值变了)
“强制锁顺序” 的真正含义(3 句话讲清)
- 「强制的主体」:是 数据库对 “单个事务内的锁顺序” 强制,不是对 “多个事务之间的操作顺 序” 强制;
- 「强制的规则」:单个事务如果要锁多个唯一键(比如同时锁 user_id=1 和 2)(关键字in),数据库会按唯一键的自然顺序(1<2)加锁,不会按事务写 SQL 的顺序乱加;
- 「不强制的部分」:数据库管不了 “多个事务之间,谁先操作哪个唯一键”—— 这是业务代码决定的,比如事务 A 先操作 1,事务 B 可以先操作 2,这就是 “事务间操作顺序混乱”,进而导致死锁。
适用场景
- 存在 “重复数据插入” 风险的场景(如订单号、用户手机号唯一);
- 并发事务需操作相同资源(如修改同一用户余额),需精准锁定单行。
具体实现(PostgreSQL)
-
创建唯一索引 / 主键(优先主键,主键自带唯一索引):
-- 1. 给表加主键(最常用,比如id列,唯一标识每行) ALTER TABLE your_table ADD PRIMARY KEY (id); -- 2. 给非主键列加唯一索引(如订单号唯一) CREATE UNIQUE INDEX idx_unique_order_no ON your_table (order_no); -- 3. 组合唯一索引(多列组合唯一,如“用户ID+商品ID”避免重复下单) CREATE UNIQUE INDEX idx_unique_user_goods ON your_table (user_id, goods_id); -
基于唯一索引操作数据(避免表锁,精准行锁):
-- 事务1:修改order_no='OD123'的订单(通过唯一索引锁定单行) BEGIN; UPDATE your_table SET status = 'paid' WHERE order_no = 'OD123'; -- 仅锁这一行 COMMIT; -- 尽快提交,释放锁 -- 事务2:同时修改order_no='OD456'的订单(无冲突) BEGIN; UPDATE your_table SET status = 'paid' WHERE order_no = 'OD456'; -- 仅锁这一行 COMMIT;
注意事项
- 唯一索引的列需选择「查询频繁、基数高」的字段(如 ID、订单号),避免索引失效导致表锁;
- 避免 “批量操作无索引的列”(如
UPDATE your_table SET status=1 WHERE create_time < '2025-01-01'无索引时,会锁全表)。
上述的例子是「无冲突场景」—— 因为两个事务操作的是不同的唯一键值(比如不同订单号、不同商品 ID),自然能并行执行,这其实是唯一索引「缩小锁粒度」的优势(行锁 vs 表锁),但没体现出它「解决死锁」的核心作用。
要理解唯一索引如何解决死锁,关键是先看「没有唯一索引时,为什么会产生死锁」,再对比「有唯一索引时如何打破死锁」—— 死锁的核心是「循环等待」,唯一索引的核心作用是强制锁顺序、避免循环等待,同时缩小锁冲突范围。
死锁案例
场景:无唯一索引 → 死锁触发
假设我们有一张 order 表,没有主键 / 唯一索引,只有 user_id(用户 ID)和 order_no(订单号)两列,现在两个并发事务要「交叉修改同一用户的不同订单」:
| 时间线 | 事务 A(操作用户 100 的订单) | 事务 B(操作用户 100 的订单) |
|---|---|---|
| 1 | BEGIN; | BEGIN; |
| 2 | -- 无索引,修改订单 OD123 → 触发「表锁」(因为没索引,PG 只能锁全表) | |
| 3 | UPDATE order SET status='paid' WHERE order_no='OD123'; | |
| 4 | -- 无索引,修改订单 OD456 → 申请表锁,但事务 A 已持有表锁 → 阻塞等待 | |
| 5 | -- 事务 A 想继续修改 OD456 → 自己持有表锁,却要等自己释放?不,实际更常见的是「行锁混乱导致循环等待」(无索引时 PG 可能按物理行号锁,顺序随机) | |
| 6 | UPDATE order SET status='paid' WHERE order_no='OD456'; | |
| 7 | -- 此时事务 A 持有 OD123 的 “伪行锁”,等待 OD456 的锁;事务 B 持有 OD456 的 “伪行锁”,等待表锁 → 循环等待 → 死锁触发! |
为什么会这样?
没有唯一索引时,PostgreSQL 无法精准定位单行,会出现两种问题:
- 要么触发「表锁」(所有事务抢同一把锁,阻塞但可能不死锁,但并发为 0);
- 要么按「物理行号」加锁(行锁),但物理行号是无序的,事务可能「交叉锁定」资源(A 锁行 1 等行 2,B 锁行 2 等行 1),直接触发死锁。
场景:有唯一索引 → 无死锁,有序执行
给 order 表加「唯一索引」(比如 idx_unique_order_no (order_no)),再重复上面的交叉操作:
| 时间线 | 事务 A(操作用户 100 的订单) | 事务 B(操作用户 100 的订单) |
|---|---|---|
| 1 | BEGIN; | BEGIN; |
| 2 | -- 有唯一索引,修改 OD123 → 精准加「行锁」(只锁 OD123 这一行) | |
| 3 | UPDATE order SET status='paid' WHERE order_no='OD123'; | |
| 4 | -- 有唯一索引,修改 OD456 → 精准加「行锁」(只锁 OD456 这一行) | |
| 5 | UPDATE order SET status='paid' WHERE order_no='OD456'; -- 无阻塞,直接执行 | |
| 6 | -- 事务 A 继续修改 OD456 → 申请 OD456 的行锁 → 事务 B 已持有,阻塞等待 | |
| 7 | COMMIT; -- 事务 B 提交,释放 OD456 的锁 | |
| 8 |
-- 事务 A 获取 OD456 的锁,执行修改 → COMMIT; |
为什么没死锁?唯一索引解决了两个核心问题(死锁的两个关键诱因):
- 锁粒度从「表锁」→「行锁」:两个事务操作不同订单号时,锁定的是「各自的行」,不会抢同一把锁,冲突范围大幅缩小;
- 强制「锁顺序」:唯一索引的列(如
order_no)是有序的(OD123 < OD456),事务操作时会按「唯一键升序」获取锁(哪怕是交叉操作,PG 也会按索引顺序加锁),不会出现「A 等 B、B 等 A」的循环等待。
反例:有唯一索引,但锁顺序混乱 → 死锁触发
| 时间点 | 事务 A(用户 1→用户 2 转账) | 事务 A 的锁状态 | 事务 B(用户 2→用户 1 转账) | 事务 B 的锁状态 |
|---|---|---|---|---|
| 0 秒 | BEGIN;(开启事务) | 无任何锁 | BEGIN;(开启事务) | 无任何锁 |
| 1 秒 | -- 第一步:扣减用户 1 的余额(操作 user_id=1)UPDATE user_account SET balance=balance-100 WHERE user_id=1; | 持有「user_id=1 的行锁」(因为 user_id 是唯一主键,精准锁这一行) | - | 无任何锁 |
| 2 秒 | - | 仍持有「user_id=1 的行锁」 | -- 第一步:扣减用户 2 的余额(操作 user_id=2)UPDATE user_account SET balance=balance-100 WHERE user_id=2; | 持有「user_id=2 的行锁」(精准锁这一行) |
| 3 秒 | -- 第二步:给用户 2 加余额(操作 user_id=2)UPDATE user_account SET balance=balance+100 WHERE user_id=2; | 想要获取「user_id=2 的行锁」,但这把锁已被事务 B 持有 → 阻塞等待(事务 A 停在这,不往下走) | - | 仍持有「user_id=2 的行锁」 |
| 4 秒 | - | 阻塞等待(等事务 B 释放 user_id=2 的锁) | -- 第二步:给用户 1 加余额(操作 user_id=1)UPDATE user_account SET balance=balance+100 WHERE user_id=1; | 想要获取「user_id=1 的行锁」,但这把锁已被事务 A 持有 → 阻塞等待(事务 B 也停在这) |
| 5 秒 | 数据库检测到:事务 A 持有「user_id=1 的锁」,等「user_id=2 的锁」;事务 B 持有「user_id=2 的锁」,等「user_id=1 的锁」;循环等待,无法解开! | 死锁触发!数据库自动回滚其中一个事务(比如事务 A),避免无限阻塞 | 同上 | 死锁触发!另一个事务(事务 B)可能继续执行,或也被回滚(看数据库策略) |
-
为什么有唯一索引还会死锁?
唯一索引的作用是「把锁粒度从表锁→行锁」,但它管不了「你先锁哪一行、后锁哪一行」。这里两个事务都只锁自己需要的行(没有锁全表),但因为「锁的顺序相反」,导致「互相持有对方需要的锁,互相等待」—— 这正是死锁的核心条件(循环等待),唯一索引解决不了 “顺序混乱” 的问题。 -
如果没有唯一索引会怎么样?
若user_id没有唯一索引,执行UPDATE WHERE user_id=xxx时,PostgreSQL 会加「表锁」→ 两个事务会争抢同一把表锁,一个事务阻塞等另一个释放,不会死锁,但并发效率为 0(所有操作串行);而有唯一索引时,虽然并发效率高(能同时操作不同行),但如果顺序乱了,反而会触发死锁(比表锁更隐蔽的问题)。 -
死锁发生后会怎么样?
PostgreSQL 会自动检测死锁(默认开启死锁检测),发现后会「回滚其中一个事务」(通常是执行时间更短、修改行数更少的那个),并抛出错误(比如ERROR: deadlock detected),让另一个事务能继续执行,避免系统卡死。
对比:锁顺序一致 → 彻底避免死锁(正确做法)
只要让所有事务「按唯一键的固定顺序加锁」(比如:先锁 user_id 小的行,再锁 user_id 大的行),哪怕有唯一索引,也不会死锁:
| 时间点 | 事务 A(用户 1→用户 2 转账,按 user_id 升序锁) | 事务 A 的锁状态 | 事务 B(用户 2→用户 1 转账,按 user_id 升序锁) | 事务 B 的锁状态 |
|---|---|---|---|---|
| 0 秒 | BEGIN; | 无任何锁 | BEGIN; | 无任何锁 |
| 1 秒 | -- 第一步:先锁 user_id=1(小的),扣减余额UPDATE user_account SET balance=balance-100 WHERE user_id=1; | 持有「user_id=1 的行锁」 | - | 无任何锁 |
| 2 秒 | - | 持有「user_id=1 的行锁」 | -- 第一步:也先锁 user_id=1(小的)UPDATE user_account SET balance=balance-100 WHERE user_id=1; | 想要获取「user_id=1 的行锁」,但被事务 A 持有 → 阻塞等待 |
| 3 秒 | -- 第二步:再锁 user_id=2(大的),加余额UPDATE user_account SET balance=balance+100 WHERE user_id=2; | 持有「user_id=1+2 的行锁」 | - | 继续阻塞等待 |
| 4 秒 | COMMIT;(事务结束,释放所有锁) | 无任何锁 | - | 获得「user_id=1 的行锁」,执行扣减余额 |
| 5 秒 | - | - | -- 第二步:再锁 user_id=2(大的),加余额UPDATE user_account SET balance=balance+100 WHERE user_id=2; | 持有「user_id=1+2 的行锁」 |
| 6 秒 | - | - | COMMIT;(事务结束,释放所有锁) | 无任何锁 |
结果:没有循环等待,两个事务有序执行,完全不会死锁!
核心总结(一句话打通逻辑)
1、唯一索引解决的是「锁粒度太大(表锁)导致的无意义冲突」,但解决不了「锁顺序混乱导致的循环等待」;
2、死锁的核心是「循环等待」,哪怕有唯一索引,只要多个事务交叉锁定同一组资源(比如两行数据)且顺序相反,就一定会死锁;
3、解决办法只有一个:所有事务都按「唯一键升序(或固定顺序)」加锁,打破循环等待。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)