# 40P01 — deadlock_detected：检测到死锁

> 当事务形成锁等待环时，PostgreSQL 会报告 SQLSTATE 40P01。应回滚受害事务、统一加锁顺序，并且只在业务语义允许时重试完整操作。
---

## 速览 {#at-a-glance}

`40P01` 是 PostgreSQL 类别 40 `transaction_rollback` 中的 `deadlock_detected` 条件。它表示锁管理器发现事务之间形成等待环，因此选择一个受害事务并中止它。

主报文是 `deadlock detected`。服务器可能附带动态构造的等待图 detail、提示查看服务器日志的 hint，以及说明被中断语句的上下文。进程 ID、事务 ID 和具体等待图都随运行变化；应按 SQLSTATE 分支并保存结构化字段，不要匹配这些动态值。

代表性案例先让两个会话分别持有相反的行锁，再用 `threading.Barrier` 放行两个交叉请求，同时由独立观察会话采样 `pg_stat_activity` 的 `wait_event_type=Lock`。观察会话不负责放行请求。一个会话收到 `40P01` 后事务为 `INERROR`；幸存会话完成操作，随后两个会话都回到 `IDLE`。新的事务按顺序取得两把行锁并提交完整重试。

运行 `40P01-manual-boundary-final-20260909` 在 PostgreSQL 18.6 和隔离的 PostgreSQL 10.21 上均通过。逐目标断言和结构化观察保存在[公开证据 JSON](../../data/evidence/40p01.json)中。

<!-- BEGIN SQLSTATE FACTS: generated by scripts/generate.py; do not edit -->

| 字段 | 值 |
| --- | --- |
| SQLSTATE | `40P01` |
| 条件名 | `deadlock_detected` |
| 状态 | `有效` |
| 已知存在于 | `7.4` |
| 锁定快照 | `9.0.23, 9.1.24, 9.2.24, 9.3.25, 9.4.26, 9.5.25, 9.6.24, 10.23, 11.22, 12.22, 13.23, 14.24, 15.19, 16.15, 17.11, 18.6, 19beta3` |
| 宏 | `ERRCODE_T_R_DEADLOCK_DETECTED` |
| 别名 | `—` |

<!-- source facts: data/errcodes/40P01.json -->
<!-- END SQLSTATE FACTS -->

## 含义与触发路径 {#meaning}

死锁需要等待图中出现环。常见形式是会话 A 锁住第 1 行后请求第 2 行，同时会话 B 锁住第 2 行后请求第 1 行。PostgreSQL 的死锁检测器会在配置的 deadlock timeout 到期后检查锁图，`DeadLockReport` 随后报告该条件。

这个错误说明事务协调失败，不表示行数据损坏。受害事务的全部工作都会中止。另一个事务可能在检测器打破环后继续，但它成功完成并不意味着受害事务的预期工作已经应用。

`40P01` 不等于最终成功的锁等待，也不同于由 statement timeout 引起的 `57014`。本案例观察到的 `pg_stat_activity` 锁等待用于证明死锁准备顺序；服务器返回的 `deadlock detected` 才是最终条件。

## 报文与诊断 {#messages}

下面的调度是代表性操作。应在两个连接上分别执行会话 A 和 B；两个会话先各自持有一把行锁，随后由 barrier 放行交叉请求，独立观察会话在请求等待期间采样两个 backend。观察会话不是放行条件。

<!-- BEGIN SQLSTATE SNIPPET: two_session_lock_cycle -->
```sql
CREATE TABLE locks(id integer PRIMARY KEY, marker text NOT NULL);
INSERT INTO locks VALUES (1, 'seed-1'), (2, 'seed-2');

-- 会话 A：开始事务、设置 deadlock_timeout，并锁住 id = 1。
BEGIN;
SET deadlock_timeout = '100ms';
UPDATE locks SET marker = 'first-1' WHERE id = 1;

-- 会话 B：开始事务、设置 deadlock_timeout，并锁住 id = 2。
BEGIN;
SET deadlock_timeout = '100ms';
UPDATE locks SET marker = 'second-2' WHERE id = 2;

-- 明确的 barrier 让两个请求同时进入锁循环。
UPDATE locks SET marker = 'first-2' WHERE id = 2;
UPDATE locks SET marker = 'second-1' WHERE id = 1;

-- 受害事务必须 ROLLBACK；幸存事务可以 COMMIT。
ROLLBACK;
COMMIT;

-- 新的重试连接按同一顺序取得锁，并验证两行。
BEGIN;
UPDATE locks SET marker = 'retry-1' WHERE id = 1;
UPDATE locks SET marker = 'retry-2' WHERE id = 2;
COMMIT;
SELECT id, marker FROM locks ORDER BY id;
```
<!-- END SQLSTATE SNIPPET -->

PostgreSQL 18.6 对受害事务返回的形状为：

```text
SQLSTATE: 40P01
severity: ERROR
message_primary: deadlock detected
message_detail: Process <pid-a> waits for ShareLock on transaction <xid-b>; blocked by process <pid-b>.
Process <pid-b> waits for ShareLock on transaction <xid-a>; blocked by process <pid-a>.
message_hint: See server log for query details.
context: while updating tuple (0,2) in relation "locks"
source: deadlock.c / DeadLockReport / line 1138
```

PG10 运行得到相同的主报文和字段，但源码行号随版本变化。detail 来自实时等待图，因此此处用运行时占位符表示进程和事务标识。hint 并不保证客户端一定收到对应的服务器日志条目，它是在日志已配置时指向调查位置。

## 诊断 {#diagnosis}

记录 SQLSTATE、严重级别、主报文、detail、hint、上下文、失败语句、backend PID 和事务状态。在等待仍存在时检查 `pg_stat_activity` 及相关锁视图。有用的观察应包含 `wait_event_type=Lock`、等待事件、当前查询和参与的 PID；单纯 sleep 不能建立死锁证据。

应根据实际应用代码及所有访问同一行或 advisory lock 的路径整理加锁顺序，并检查触发器、外键、索引或后台 worker 是否取得了额外的锁。`deadlock_timeout` 只控制何时检测，调大它会改变检测延迟，不能消除等待环。

错误发生后，受害连接必须先 `ROLLBACK`，因为此时为 `INERROR`；幸存连接在提交前可能为 `INTRANS`。runner 验证了显式清理后两者都回到 `IDLE`，并在重试前读取了数据。

## 处理与修复 {#response}

回滚受害事务并释放其锁，再选择完整修复：

- 让所有代码路径按一致顺序取得同一组锁，最好显式排序键值。
- 缩短事务，不要在持有数据库锁时等待外部工作。
- 只有在操作可重复且结果具备幂等语义时，才从头重试整个事务，包括读取和加锁。
- 重试后验证已提交的业务结果；幸存事务的部分更新不能证明受害事务的工作成功。

本案例的修复按顺序锁定第 1 行和第 2 行，提交 `retry-1`、`retry-2`，并在连接为 `IDLE` 时读取两行。生产实现仍需有限的重试预算和应用级幂等键；单凭 SQLSTATE 不能判断重复操作是否安全。

## 版本与边界 {#versions}

目录在 PostgreSQL 7.4 的锁定定义中已观察到 `40P01`，并持续到 8.4.22 的 pre-9.0 定义；随后在列出的所有正式快照直到 PostgreSQL 18.6 以及 PostgreSQL 19 Beta 3 预览中存在。这是 definition_only 的存在边界，不是确切实现引入版本或运行时使用断言。9.0 头文件视图到 9.1 文本定义之间记录了类别标题变化；这只是目录观察，不是代码引入日期。扫描范围内条件行没有其他定义变化。

双会话案例在 PostgreSQL 18.6 和 10.21 上均通过。检测时点、锁类型和 detail 取决于工作负载与设置；本页采用 SQLSTATE 及事务回滚要求作为兼容边界，而不是固定的进程 ID detail。

## 相关 {#related}

[`40001` — `serialization_failure`](../40001/) 同样要求从新快照开始重试完整事务。[`57014` — `query_canceled`](../57014/) 表示取消，不表示锁环。[`23503` — `foreign_key_violation`](../23503/) 和 [`23505` — `unique_violation`](../23505/) 是可能出现在锁顺序需要复核的事务中的完整性条件。[`25P02` — `in_failed_sql_transaction`](../25p02/) 是受害事务中止后出现的后续状态。

## 来源 {#sources}

结构化证据记录在[公开证据 JSON](../../data/evidence/40p01.json)中。所有源码记录固定到 PostgreSQL commit `724edf9bde9d356724ad384a2e196edc3c9f80f7`；运行记录保留各目标的精确 ID 和结构化观察。

- `src.errcodes.18.6` — [`errcodes.txt`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/utils/errcodes.txt#L329-L334)
- `src.deadlock.18.6` — [`deadlock.c`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/storage/lmgr/deadlock.c#L1071-L1138)
- `doc.mvcc.18` — [死锁与序列化失败](https://www.postgresql.org/docs/18/mvcc-serialization-failure-handling.html)
- Runtime：latest 与 pg10 均为 `40P01-manual-boundary-final-20260909`，结构化观察见[公开证据 JSON](../../data/evidence/40p01.json)
