文章目录
-
- [一、死锁的基本原理与 PostgreSQL 的处理机制](#一、死锁的基本原理与 PostgreSQL 的处理机制)
-
- [1. 什么是死锁?](#1. 什么是死锁?)
- [2. PostgreSQL 如何检测死锁?](#2. PostgreSQL 如何检测死锁?)
- [3. 死锁 vs 锁等待](#3. 死锁 vs 锁等待)
- 二、死锁的常见场景与根本原因
-
- [1. 典型场景](#1. 典型场景)
- [2. 根本原因总结](#2. 根本原因总结)
- 三、方法一:通过日志识别死锁(被动发现)
-
- [1. 确保日志配置正确](#1. 确保日志配置正确)
- [2. 死锁日志示例](#2. 死锁日志示例)
- [3. 日志分析要点](#3. 日志分析要点)
- 四、方法二:实时监控与主动发现死锁
-
- [1. 查询 `pg_stat_database` 获取死锁计数](#1. 查询
pg_stat_database获取死锁计数) - [2. 实时查看锁等待与阻塞链](#2. 实时查看锁等待与阻塞链)
- [3. 使用 `pg_locks` 深入分析锁状态](#3. 使用
pg_locks深入分析锁状态)
- [1. 查询 `pg_stat_database` 获取死锁计数](#1. 查询
- 五、方法三:启用详细锁日志(高级诊断)
-
- [1. 动态调整参数(无需重启)](#1. 动态调整参数(无需重启))
- [2. 日志内容增强](#2. 日志内容增强)
- 六、死锁根因分析四步法
-
- [步骤 1:还原事务逻辑](#步骤 1:还原事务逻辑)
- [步骤 2:检查操作顺序一致性](#步骤 2:检查操作顺序一致性)
- [步骤 3:评估事务粒度](#步骤 3:评估事务粒度)
- [步骤 4:检查索引覆盖](#步骤 4:检查索引覆盖)
- 七、死锁预防与优化策略
-
- [1. 应用层规范](#1. 应用层规范)
- [2. 数据库设计优化](#2. 数据库设计优化)
- [3. 参数调优(谨慎)](#3. 参数调优(谨慎))
- 八、自动化监控与告警
-
- [1. Prometheus + Grafana 监控](#1. Prometheus + Grafana 监控)
- [2. 自定义脚本巡检](#2. 自定义脚本巡检)
- [3. APM 工具集成](#3. APM 工具集成)
- [九、死锁排查 SOP(标准操作流程)](#九、死锁排查 SOP(标准操作流程))
在高并发的 PostgreSQL 数据库环境中,死锁(Deadlock)是一种典型的事务并发问题。当两个或多个事务相互等待对方持有的锁,形成循环依赖时,系统无法继续推进任何一方,导致事务永久阻塞。若不及时检测和处理,死锁会迅速耗尽连接池、引发应用超时,甚至造成服务雪崩。
PostgreSQL 内置了死锁检测机制 ,能自动发现并终止其中一个事务以打破僵局。然而,仅依赖自动处理远远不够------DBA 必须能够主动发现、精确定位、分析根因并预防复发。本文将系统性地阐述 PostgreSQL 死锁的排查全流程,涵盖原理、监控、日志分析、实时诊断及优化策略。
一、死锁的基本原理与 PostgreSQL 的处理机制
1. 什么是死锁?
假设有两个事务 T1 和 T2:
- T1 持有行 A 的锁,请求行 B 的锁;
- T2 持有行 B 的锁,请求行 A 的锁。
此时,T1 等待 T2,T2 等待 T1,形成循环等待,即死锁。
2. PostgreSQL 如何检测死锁?
-
PostgreSQL 使用 "等待图"(Wait-for Graph) 进行死锁检测。
-
当一个事务因锁冲突进入等待状态时,后台进程会检查是否存在环路。
-
若检测到死锁,选择一个"代价最小"的事务作为牺牲者(victim) ,回滚其当前语句(或整个事务),并抛出错误:
ERROR: deadlock detected DETAIL: Process 12345 waits for ShareLock on transaction 67890; blocked by process 54321.
注意:PostgreSQL 的死锁检测是语句级 的。若在事务中某条语句引发死锁,仅该语句被回滚,事务可继续执行后续语句(除非使用
SERIALIZABLE隔离级别)。
3. 死锁 vs 锁等待
- 锁等待(Lock Wait):单向等待,最终会获得锁(只要持有者释放);
- 死锁(Deadlock):循环等待,必须由系统干预才能解除。
二、死锁的常见场景与根本原因
1. 典型场景
(1)更新顺序不一致
sql
-- 会话1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 会话2(同时执行)
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
若两个会话交叉执行第一条 UPDATE,则各自持有对方需要的锁,形成死锁。
(2)外键约束引发的隐式锁
- 插入子表记录时,需对父表主键加
SHARE ROW EXCLUSIVE锁; - 若多个事务以不同顺序操作父子表,易引发死锁。
(3)索引维护锁冲突
CREATE INDEX CONCURRENTLY分多阶段执行,期间可能与其他 DML 冲突;- 大量并发
INSERT/UPDATE可能与索引构建事务死锁。
(4)显式锁使用不当
- 应用层使用
SELECT FOR UPDATE但未按固定顺序访问行; - 手动加表锁(
LOCK TABLE)顺序不一致。
2. 根本原因总结
- 事务内操作顺序不一致(最主要原因);
- 事务粒度过大(长时间持有锁);
- 缺乏索引(导致锁升级为表级或大量行锁);
- 高并发写入热点数据(如自增 ID 表、计数器表)。
三、方法一:通过日志识别死锁(被动发现)
PostgreSQL 在检测到死锁时,会向日志输出详细信息(前提是日志级别足够)。
1. 确保日志配置正确
在 postgresql.conf 中设置:
conf
log_min_messages = warning # 至少为 warning
log_min_error_statement = error # 记录导致错误的 SQL
log_lock_waits = on # 记录长时间锁等待(辅助分析)
deadlock_timeout = 1s # 死锁检测触发时间(默认 1 秒)
重载配置:
bash
pg_ctl reload -D $PGDATA
2. 死锁日志示例
2026-02-08 11:00:00 UTC [12345]: [1-1] user=app,db=prod ERROR: deadlock detected
2026-02-08 11:00:00 UTC [12345]: [2-1] user=app,db=prod DETAIL: Process 12345 waits for ShareLock on transaction 67890; blocked by process 54321.
Process 54321 waits for ShareLock on transaction 12345; blocked by process 12345.
Process 12345: UPDATE accounts SET balance = balance + 100 WHERE id = 2;
Process 54321: UPDATE accounts SET balance = balance + 50 WHERE id = 1;
2026-02-08 11:00:00 UTC [12345]: [3-1] user=app,db=prod HINT: See server log for query details.
2026-02-08 11:00:00 UTC [12345]: [4-1] user=app,db=prod CONTEXT: while updating tuple (12,3) in relation "accounts"
2026-02-08 11:00:00 UTC [12345]: [5-1] user=app,db=prod STATEMENT: UPDATE accounts SET balance = balance + 100 WHERE id = 2;
3. 日志分析要点
- 被终止的事务:日志所属进程(Process 12345)即为牺牲者;
- 参与死锁的进程:列出所有相互阻塞的进程 ID;
- 具体 SQL 语句:显示每个进程正在执行的语句;
- 涉及的关系(表)和元组 :如
relation "accounts",tuple (12,3)。
⚠️ 注意:日志仅记录已被检测并处理的死锁,无法反映历史死锁频率。
四、方法二:实时监控与主动发现死锁
1. 查询 pg_stat_database 获取死锁计数
sql
SELECT
datname,
deadlocks,
xact_commit,
xact_rollback
FROM pg_stat_database
WHERE datname = 'your_db_name';
deadlocks:自上次统计重置以来的死锁次数;- 可定期采样,计算死锁发生率(如每小时增加 10 次)。
重置统计:
SELECT pg_stat_reset();(谨慎使用,影响全局监控)
2. 实时查看锁等待与阻塞链
死锁发生前通常存在锁等待。通过以下查询可提前发现风险:
sql
SELECT
blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocked.wait_event AS blocked_wait,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query,
blocking.state AS blocking_state
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event IS NOT NULL;
若发现双向阻塞(A 阻塞 B,B 也阻塞 A),则极可能即将发生死锁。
3. 使用 pg_locks 深入分析锁状态
sql
SELECT
l.pid,
a.query,
l.locktype,
l.database,
l.relation::regclass,
l.page,
l.tuple,
l.virtualxid,
l.transactionid,
l.mode,
l.GRANTED
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.pid IN (
SELECT pid FROM pg_stat_activity WHERE wait_event IS NOT NULL
)
ORDER BY l.pid;
GRANTED = f表示该锁正在等待;- 结合
transactionid可追踪事务间依赖。
五、方法三:启用详细锁日志(高级诊断)
若需深入分析死锁模式,可临时开启更详细的锁日志。
1. 动态调整参数(无需重启)
sql
-- 会话级(仅当前会话)
SET log_lock_waits = on;
SET deadlock_timeout = '500ms'; -- 缩短检测时间(仅测试环境!)
-- 全局(需 superuser)
ALTER SYSTEM SET log_lock_waits = on;
SELECT pg_reload_conf();
2. 日志内容增强
当锁等待超过 deadlock_timeout(默认 1 秒),即使未死锁,也会记录:
LOG: process 12345 still waiting for ShareLock on transaction 67890 after 1000.123 ms
DETAIL: Process holding the lock: 54321. Wait queue: 12345.
STATEMENT: UPDATE accounts SET ...
这有助于在死锁发生前识别长等待事务。
⚠️ 警告:生产环境不要随意缩短
deadlock_timeout,可能导致误杀正常长事务。
六、死锁根因分析四步法
一旦发现死锁,按以下流程定位根本原因:
步骤 1:还原事务逻辑
- 从日志或应用代码中,找到参与死锁的完整事务 SQL 序列;
- 确认各事务的操作顺序、涉及表、WHERE 条件。
步骤 2:检查操作顺序一致性
- 是否所有事务都按相同顺序访问表或行?
- 例如:始终先更新
accounts(id=1),再更新accounts(id=2)。
步骤 3:评估事务粒度
- 事务是否过大?是否包含非数据库操作(如 HTTP 调用、sleep)?
- 建议:保持事务尽可能短小,减少锁持有时间。
步骤 4:检查索引覆盖
- UPDATE/DELETE 的 WHERE 条件是否有索引?
- 若无索引,PostgreSQL 可能扫描全表并锁定大量无关行,增加冲突概率。
七、死锁预防与优化策略
1. 应用层规范
-
强制统一访问顺序:
-
对同一组资源(如账户 ID 列表),按 ID 升序排序后再操作;
-
示例(伪代码):
pythonaccount_ids = sorted([id1, id2]) for aid in account_ids: execute("UPDATE accounts SET ... WHERE id = %s", aid)
-
-
避免在事务中混入非 DB 操作:
- 不要在 BEGIN...COMMIT 中调用外部 API 或 sleep。
-
使用重试机制:
- 捕获
deadlock detected错误,自动重试事务(通常 1~3 次即可成功)。
- 捕获
2. 数据库设计优化
- 为高频 UPDATE/DELETE 字段添加索引;
- 拆分热点表(如分库分表、使用序列 ID 避免自增瓶颈);
- 使用乐观锁替代悲观锁(如 version 字段 + CAS 更新)。
3. 参数调优(谨慎)
| 参数 | 默认值 | 建议 |
|---|---|---|
deadlock_timeout |
1s | 一般无需修改;OLTP 可略降(如 500ms),但需测试 |
max_connections |
100 | 限制连接数,减少并发冲突 |
不推荐通过增大
deadlock_timeout来"避免"死锁------这只会延长阻塞时间,恶化用户体验。
八、自动化监控与告警
1. Prometheus + Grafana 监控
- 通过
postgres_exporter采集pg_stat_database.deadlocks; - 设置告警规则:
rate(pg_stat_database_deadlocks[5m]) > 0.1(每 5 分钟 ≥1 次死锁)。
2. 自定义脚本巡检
定期运行 SQL 检查死锁增长:
sql
-- 记录当前死锁数
SELECT deadlocks INTO current_deadlocks FROM pg_stat_database WHERE datname = 'prod';
-- 与上次记录比较,若增加则告警
3. APM 工具集成
- New Relic、Datadog 等可捕获 PostgreSQL 错误日志;
- 配置告警:当
error.message包含 "deadlock detected" 时触发。
九、死锁排查 SOP(标准操作流程)
-
发现:
- 应用报错 "deadlock detected";
- 监控显示
pg_stat_database.deadlocks增加; - 日志出现死锁记录。
-
定位:
- 从日志提取参与死锁的 SQL 语句和进程 ID;
- 查询
pg_stat_activity确认事务上下文。
-
分析:
- 还原完整事务逻辑;
- 检查操作顺序、索引、事务大小。
-
解决:
- 临时:确保应用有重试机制;
- 长期:统一访问顺序、加索引、缩小事务。
-
验证:
- 压测模拟高并发场景,确认死锁消失;
- 监控死锁计数是否归零。
-
预防:
- 将死锁检查纳入 CI/CD;
- 建立数据库开发规范。
结语:死锁虽不可避免,但完全可控。PostgreSQL 提供了完善的检测与日志机制,关键在于主动监控、快速响应、根因治理。通过本文所述方法,DBA 和开发者可将死锁从"灾难性故障"转变为"可管理的偶发事件"。
记住:最好的死锁处理,是让它从未发生。坚持"小事务、固定顺序、充分索引"三大原则,可消除 95% 以上的死锁风险。