锁等待与超时
上一节讲了死锁——两个事务互相等待,由数据库自动挑一个杀掉。但线上更常见、更磨人的其实是另一类问题:一个 SQL 卡在锁等待上,既不报错也不返回,一直挂着。如果持有锁的事务是个 idle in transaction 的连接,它可能挂上几分钟甚至几小时,把后面的请求全部拖死。
PostgreSQL 为此提供了一整套超时(Timeout)参数:lock_timeout、statement_timeout、deadlock_timeout、idle_in_transaction_session_timeout、idle_session_timeout、transaction_timeout。它们的名字相似、作用却各不相同,本页把这六个参数一次讲清,并给出线上 DDL 加锁超时的完整实战方案。
前置知识
阅读本节前,建议先了解:页级锁与死锁、表级锁(ACCESS SHARE / EXCLUSIVE)
基础概念
锁等待到底在等什么
当一个语句请求的锁与别人已持有的锁冲突时,它不会立刻失败,而是进入等待队列(Wait Queue)。等待时该后端进程处于「睡眠」状态,pg_stat_activity.wait_event_type 会显示为 Lock:
SELECT pid, wait_event_type, wait_event, state, left(query, 40) AS query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'; pid | wait_event_type | wait_event | state | query
------+-----------------+---------------+--------+----------------------
9301 | Lock | transactionid | active | UPDATE accounts SET ...
9302 | Lock | relation | active | ALTER TABLE orders ...wait_event 的取值说明了在等哪一类锁:
wait_event | 在等的对象 | 常见来源 |
|---|---|---|
transactionid | 某个事务 ID 的行锁 | UPDATE / DELETE / SELECT ... FOR UPDATE 撞上同一行 |
relation | 某个表上的表级锁 | LOCK TABLE、ALTER TABLE、VACUUM FULL |
tuple | 某个具体的元组锁 | 少见,通常是外键检查 |
extend | 等待扩展关系文件 | 大量并发插入同一张表 |
virtualxid | 等待虚拟事务 ID | 少见 |
object / userlock / advisory | 其他对象锁 | pg_advisory_lock 等 |
六个超时参数总览
| 参数 | 默认值 | 作用 | 超时后行为 | 错误码 | 可设置范围 |
|---|---|---|---|---|---|
deadlock_timeout | 1s | 等待超过此时长后检查死锁 | 若检测到死锁则中止一个事务 | 40P01 | 超级用户 |
lock_timeout | 0(禁用) | 等待锁的最长时间 | 中止当前语句 | 55P03 | 任意用户 |
statement_timeout | 0(禁用) | 单条语句总执行时间上限 | 中止当前语句 | 57014 | 任意用户 |
idle_in_transaction_session_timeout | 0(禁用) | 事务中空闲多久后终止会话 | 终止整个会话(FATAL) | — | 任意用户 |
idle_session_timeout | 0(禁用) | 不在事务中空闲多久后终止会话 | 终止整个会话(FATAL) | — | 任意用户 |
transaction_timeout | 0(禁用) | 单个事务总时长上限(PG17+) | 终止整个会话(FATAL) | — | 任意用户 |
别把三者搞混
lock_timeout 只管等锁的时间;statement_timeout 管整条语句的时间(包含执行、也包含等锁);deadlock_timeout 不中止任何东西,它只是「什么时候去检查有没有死锁」的阈值。三者可以同时生效,谁先到谁生效。
超时、死锁与长等待三者关系
时间轴(一个语句开始等待锁)
│
├─ 阻塞发生
│
├─ 0s ─────────────────────────────────────────────────►
│
├─ lock_timeout 到点 ──► 报 55P03 lock_not_available,本语句失败
│
├─ deadlock_timeout 到点 ──► 检查等待图
│ ├─ 有环 → 挑一个事务中止,报 40P01 deadlock_detected
│ └─ 无环 → 继续等待(什么都不发生)
│
├─ statement_timeout 到点 ──► 报 57014 query_canceled,本语句失败
│
└─ 一直等到持有者提交/回滚 ──► 拿到锁,语句继续执行关键点:deadlock_timeout 不是「死锁的超时时间」,它不会解掉一个「单向长等待」。想给长等待兜底,必须用 lock_timeout 或 statement_timeout。
语法与用法
设置与查看超时参数
-- 查看当前会话生效值
SHOW <参数名>;
-- 会话级 / 事务级设置(SET LOCAL 仅当前事务有效,事务结束自动还原)
SET <参数名> = '<值>';
SET LOCAL <参数名> = '<值>';
-- 数据库级 / 角色级(对之后连接该库或该角色登录的会话生效)
ALTER DATABASE <dbname> SET <参数名> = '<值>';
ALTER ROLE <rolename> SET <参数名> = '<值>';
-- 集群级(写入 postgresql.conf,需重载或重启)
ALTER SYSTEM SET <参数名> = '<值>';
SELECT pg_reload_conf();-- 查看六个参数当前值
SELECT name, setting, unit, context
FROM pg_settings
WHERE name IN (
'deadlock_timeout', 'lock_timeout', 'statement_timeout',
'idle_in_transaction_session_timeout', 'idle_session_timeout',
'transaction_timeout'
)
ORDER BY name; name | setting | unit | context
-------------------------------------+---------+------+-----------
deadlock_timeout | 1000 | ms | superuser
idle_in_transaction_session_timeout | 0 | ms | user
idle_session_timeout | 0 | ms | user
lock_timeout | 0 | ms | user
statement_timeout | 0 | ms | user
transaction_timeout | 0 | ms | usercontext 列很重要
context = 'user' 表示普通用户即可 SET;superuser 表示只有超级用户能改;sighup 表示改配置后 pg_reload_conf() 即可生效;postmaster 表示必须重启。用这一列可以快速判断某个参数该怎么调。
用 SET LOCAL 给单条语句设限
SET LOCAL 是事务作用域的,事务一结束就自动还原。这是给「危险的 DDL」加保险的标准写法:
BEGIN;
-- 只在本事务内限制等锁 3 秒
SET LOCAL lock_timeout = '3s';
-- 整条语句最多跑 10 秒
SET LOCAL statement_timeout = '10s';
ALTER TABLE orders ADD COLUMN remark text;
COMMIT;ERROR: canceling statement due to lock timeout
CONTEXT: while updating tuple (0,5) in relation "orders"超时后事务已中止,此时必须 ROLLBACK(或直接断开),不能继续 COMMIT:
ROLLBACK;详细说明
等待队列的 FIFO 公平性
PostgreSQL 的锁等待队列是先进先出(FIFO)的。这一点看似细节,实则有重要后果:一个请求不会因为「刚好没和别人冲突」就插队拿到锁。
举个例子:表 T 上,会话 A 持有 ACCESS EXCLUSIVE;会话 B 在等 ACCESS SHARE;此时会话 C 来了,它想拿 ACCESS SHARE——虽然 ACCESS SHARE 之间不冲突,但 C 仍然要排在 B 后面。
-- 会话 B(先到)
BEGIN; LOCK TABLE orders IN ACCESS SHARE MODE; -- 排队等待中
-- 会话 C(后到)
SELECT count(*) FROM orders; -- 同样要排在 B 后面-- 在第三个会话观察排队顺序
SELECT pid, locktype, relation::regclass AS rel, mode, granted
FROM pg_locks
WHERE relation = 'orders'::regclass
ORDER BY pid; pid | locktype | rel | mode | granted
------+----------+--------+---------------------+---------
9401 | relation | orders | AccessExclusiveLock | t ← A 持有
9402 | relation | orders | AccessShareLock | f ← B 在等(先到)
9403 | relation | orders | AccessShareLock | f ← C 在等(后到)排队也会「堆积」
正因为有 FIFO,一个被卡住的 ACCESS EXCLUSIVE 请求(比如 ALTER TABLE)会把后面所有请求都堵住——哪怕它们彼此并不冲突。这就是「DDL 会引发雪崩」的机制:一条 DDL 卡住,后面的查询全排到它后面,连接池迅速耗尽。
lock_timeout:只对等锁生效
lock_timeout 是最精准的超时参数:它只在「尝试获取锁并等待」时计时,一旦拿到锁就停止计时,不会因为语句本身跑得久而杀它。
-- 只限制等锁 1 秒,语句拿到锁后可以继续跑很久
SET lock_timeout = '1s';
UPDATE orders SET status = 'done' WHERE id = 42;ERROR: canceling statement due to lock timeout超时对应 SQLSTATE 55P03(lock_not_available),在应用层可以精确识别:
-- 用 PL/pgSQL 的异常处理捕获 lock_timeout(55P03)
DO $$
BEGIN
SET LOCAL lock_timeout = '500ms';
UPDATE orders SET status = 'done' WHERE id = 42;
EXCEPTION
WHEN lock_not_available THEN -- SQLSTATE 55P03
RAISE NOTICE '锁等待超时,稍后重试';
END $$;NOTICE: 锁等待超时,稍后重试
DO单位可以用字符串
lock_timeout 的值可以写成毫秒数(1000)也可以写成带单位的字符串('1s'、'500ms')。用字符串可读性更好,推荐使用。
statement_timeout:整条语句的总时限
statement_timeout 管的是从语句到达服务器到执行完成的全过程,包括解析、等锁、执行、以及结果发送前的处理。它不关心时间花在哪,只要超了就直接中止。
SET statement_timeout = '2s';
-- 一个慢查询(含等锁)
SELECT count(*) FROM big_table;ERROR: canceling statement due to statement timeout对应 SQLSTATE 57014(query_canceled)——注意这个错误码并不是 statement_timeout 专属的,任何「用户主动取消语句」的行为(包括 Ctrl+C、pg_cancel_backend())都会返回 57014。
-- 两种超时在同一语句上叠加,谁先到谁生效
SET lock_timeout = '1s';
SET statement_timeout = '5s';
BEGIN;
UPDATE orders SET status = 'x' WHERE id = 42; -- 若等锁超 1 秒 → 55P03
COMMIT;ERROR: canceling statement due to lock timeoutstatement_timeout 不覆盖事务
statement_timeout 是逐语句计时的。一个事务里有 100 条语句,每条都不能超时,但事务整体仍可能跑很久——它不会因为「事务开了 1 小时」而中止。要限制事务总时长,得用 transaction_timeout(PG17+,见下文)。
deadlock_timeout:死锁检测的触发阈值
deadlock_timeout 是六个参数里唯一不负责「中止」的。它的含义是:「一个会话等锁超过这么久还没等到,就去做一次死锁检查」。
SHOW deadlock_timeout;
-- 默认 1s;调大能减少无谓检测开销,调小能更快发现死锁
ALTER SYSTEM SET deadlock_timeout = '2s';
SELECT pg_reload_conf(); deadlock_timeout
------------------
1s为什么默认要等 1 秒才检查?因为检测本身有成本:需要获取锁管理器的内部锁、遍历等待图。而绝大多数锁等待都会在毫秒级内解决,如果每次都检测,纯属浪费。
-- 配合 log_lock_waits,让所有超过 deadlock_timeout 的等待都留痕
ALTER SYSTEM SET log_lock_waits = on;
SELECT pg_reload_conf();
SELECT name, setting FROM pg_settings WHERE name = 'log_lock_waits'; name | setting
----------------+---------
log_lock_waits | on调大 deadlock_timeout 的副作用
如果把它调得过大(比如 10 秒),死锁被发现的延迟就会变长——期间两个事务一直互相等着,占用连接、持有锁、放大阻塞面。除非有明确的性能理由,不建议把它调离默认值太远。
idle_in_transaction_session_timeout:事务中空闲
这是最先应该在生产环境打开的参数。很多「锁等待」事故的根因就是:应用开了事务、拿了锁,然后去做别的事(调用下游 HTTP、读文件、等待用户输入),事务一直不提交。
idle_in_transaction_session_timeout 会在会话处于事务中但空闲(idle in transaction)超过指定时长后,直接终止整个会话(返回 FATAL,不是普通的 ERROR):
-- 把「事务中空闲」限制在 30 秒
ALTER DATABASE appdb SET idle_in_transaction_session_timeout = '30s';
-- 或者在会话级设置
SET idle_in_transaction_session_timeout = '30s';
-- 观察当前有多少会话卡在 idle in transaction
SELECT pid,
now() - xact_start AS xact_age,
now() - state_change AS idle_age,
left(query, 40) AS last_query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start; pid | xact_age | idle_age | last_query
------+------------------+--------------+-------------------------------------
9501 | 00:04:12.331 | 00:04:05.109 | UPDATE accounts SET balance = ...
-- 超时后,该会话在日志中会看到:
FATAL: terminating connection due to idle-in-transaction timeout终止的是会话,不是语句
被 idle_in_transaction_session_timeout 杀掉的是整个连接,客户端会收到连接断开。应用必须有重连机制,否则会直接报错。这也是为什么该参数往往要配合连接池的重试策略一起上线。
idle_session_timeout:不在事务中的空闲连接
idle_session_timeout(PostgreSQL 14+ 引入)管的是另一类空闲:会话不在事务中、也没有在跑语句,纯粹挂在那里不动。
-- 空闲连接(非事务中)超过 10 分钟就断开
SET idle_session_timeout = '10min';
-- 区分两类空闲
SELECT state, count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY state; state | count
------------------------+-------
active | 3
idle | 47 ← idle_session_timeout 管这些
idle in transaction | 2 ← idle_in_transaction_session_timeout 管这些
-- 超时后的日志:
FATAL: terminating connection due to idle-session timeout两者配合使用,可以把「僵尸连接」清理得比较干净:
# postgresql.conf 推荐基线
idle_in_transaction_session_timeout = '30s' # 事务中空闲 30 秒就断
idle_session_timeout = '10min' # 普通空闲连接 10 分钟就断连接池存在时的取舍
如果用了 PgBouncer / 应用连接池,池里通常会保留一批空闲连接。此时 idle_session_timeout 要和池的 idle_timeout / max_lifetime 协调,避免池以为是「可用连接」而数据库已经把它断开(会导致「连接被服务器关闭」类报错)。
transaction_timeout:事务总时长(PostgreSQL 17+)
前面几个参数都只覆盖事务生命周期的一部分:statement_timeout 管单条语句,idle_in_transaction_session_timeout 管空闲。但一个持续执行的长事务(不断有语句在跑)却没有任何参数能拦住。
PostgreSQL 17 引入了 transaction_timeout,专门解决这个问题:它限制一个事务从开始到结束的总时长,超时后终止整个会话。
-- 单个事务最多存活 5 分钟
SET transaction_timeout = '5min';
BEGIN;
SELECT pg_sleep(1); -- 事务开始计时
-- ... 反复执行语句,只要总时长超 5 分钟就会被终止
COMMIT;FATAL: terminating connection due to transaction timeout| 参数 | 覆盖的场景 |
|---|---|
statement_timeout | 单条语句太长 |
idle_in_transaction_session_timeout | 事务开着但不动 |
transaction_timeout(PG17+) | 事务一直在动,但总时长太长 |
-- 完整基线:三层时间保护一起上
SET statement_timeout = '30s'; -- 单语句
SET idle_in_transaction_session_timeout = '60s'; -- 事务内空闲
SET transaction_timeout = '10min'; -- 事务总时长版本兼容性
transaction_timeout 是 PostgreSQL 17 新增的参数。在 16 及更早版本上执行 SET transaction_timeout = ... 会直接报错「unrecognized configuration parameter」。写迁移脚本或跨版本部署时要先判断版本。
参数作用域与优先级
同一个参数可以在多个层级设置,生效顺序(从低到高):
内建默认值
↓
postgresql.conf / ALTER SYSTEM
↓
ALTER DATABASE ... SET
↓
ALTER ROLE ... SET (或 ALTER ROLE ... IN DATABASE ... SET)
↓
会话启动时的命令行参数 / 连接字符串参数
↓
SET(会话级)
↓
SET LOCAL(事务级,最高)-- 验证优先级:数据库级设 5s,会话级改 2s,事务级再覆盖为 500ms
ALTER DATABASE appdb SET lock_timeout = '5s';
SHOW lock_timeout; -- 5s(继承自数据库级)
SET lock_timeout = '2s';
SHOW lock_timeout; -- 2s(会话级覆盖)
BEGIN;
SET LOCAL lock_timeout = '500ms';
SHOW lock_timeout; -- 500ms(事务级最高)
COMMIT;
SHOW lock_timeout; -- 2s(SET LOCAL 已随事务结束还原) lock_timeout
--------------
500ms
COMMIT
lock_timeout
--------------
2s还可以直接查看某个来源到底设了什么,以及某个参数当前值的最终来源:
-- 数据库级/角色级的参数覆盖
SELECT datname, unnest(setconfig) AS setting
FROM pg_db_role_setting s
LEFT JOIN pg_database d ON d.oid = s.setdatabase;
-- 参数当前值的最终来源(database / role / session / default)
SELECT name, setting, source
FROM pg_settings
WHERE name IN ('lock_timeout', 'statement_timeout'); datname | setting
---------+----------------------------
appdb | lock_timeout=5s
| statement_timeout=30s
name | setting | source
-----------------+---------+------------
lock_timeout | 2000 | session
statement_timeout| 30000 | database排查「为什么参数没生效」
pg_settings.source 会直接告诉你当前值的最终来源,比逐层猜测要快得多;sourcefile 还能指出配置来自哪个文件。
超时错误的捕获与重试
lock_timeout(55P03)与 statement_timeout(57014)的区别在应用层非常重要,因为它们的重试策略完全不同:
| 错误码 | 含义 | 是否适合立即重试 |
|---|---|---|
55P03 | lock_not_available(lock_timeout 触发) | 适合——冲突通常短暂,退避后重试往往成功 |
57014 | query_canceled(statement_timeout / 手动取消) | 要谨慎——说明语句本身可能就慢,盲目重试只会重复慢 |
40P01 | deadlock_detected | 适合——立即重试,但需幂等 |
40001 | serialization_failure(SERIALIZABLE 冲突) | 适合——立即重试,但需幂等 |
-- 在 PL/pgSQL 里对 lock_timeout 做有界重试
DO $$
DECLARE
attempts int := 0;
BEGIN
LOOP
attempts := attempts + 1;
BEGIN
SET LOCAL lock_timeout = '500ms';
UPDATE products SET stock = stock - 1 WHERE id = 7 AND stock >= 1;
EXIT; -- 成功,跳出循环
EXCEPTION
WHEN lock_not_available THEN -- 55P03
IF attempts >= 3 THEN
RAISE; -- 重试 3 次仍失败就抛出
END IF;
PERFORM pg_sleep(0.1 * attempts); -- 退避
END;
END LOOP;
RAISE NOTICE '第 % 次尝试成功', attempts;
END $$;NOTICE: 第 2 次尝试成功
DO一个更实用的「锁等待重排」技巧:把 lock_timeout 设得很短(如 100ms),失败就退避重试。这样能避免长时间排队,把「等锁」转化为「快速失败 + 重试」:
SET lock_timeout = '100ms';
UPDATE products SET stock = stock - 1 WHERE id = 7 AND stock >= 1;ERROR: canceling statement due to lock timeout重试必须幂等
重试的前提是操作幂等(Idempotent)。像 UPDATE ... SET n = n + 1 这类语句,重试时必须确认「上次是否已经提交」——如果盲目重试,可能把 +1 做了两次。稳妥做法是用条件更新(带上期望值)或唯一约束保证幂等。
用 pg_blocking_pids 与 pg_locks.granted 定位阻塞者
超时发生后,最关键的问题是「到底是谁在阻塞我」。三个工具配合使用:
-- 工具 1:pg_blocking_pids() 一步拿到阻塞者
SELECT pid,
pg_blocking_pids(pid) AS blocked_by,
now() - query_start AS waited,
left(query, 50) AS query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0; pid | blocked_by | waited | query
------+------------+--------------+-----------------------------------
9601 | {9602} | 00:00:42.117 | ALTER TABLE orders ADD COLUMN ...-- 工具 2:pg_locks.granted 精确区分「持有」与「等待」
SELECT pid, locktype, relation::regclass AS rel, mode, granted, waitstart
FROM pg_locks
WHERE NOT granted
ORDER BY waitstart; pid | locktype | rel | mode | granted | waitstart
------+----------+--------+---------------------+---------+----------------------------
9601 | relation | orders | AccessExclusiveLock | f | 2026-09-28 10:31:02.114+08-- 工具 3:顺着阻塞者往上查它是什么状态
SELECT pid, state, now() - xact_start AS xact_age, left(query, 50) AS query
FROM pg_stat_activity
WHERE pid IN (SELECT unnest(pg_blocking_pids(9601))); pid | state | xact_age | query
------+----------------------+----------------+--------------------------
9602 | idle in transaction | 00:03:41.882 | SELECT * FROM orders ...结论一目了然:9601 的 ALTER TABLE 等了 42 秒,阻塞者是 9602,而 9602 处于 idle in transaction 状态已经 3 分 41 秒——典型的「应用开事务后去干别的」事故。
pg_cancel_backend vs pg_terminate_backend
pg_cancel_backend(pid) 只是取消当前语句(对应 57014),事务仍在;pg_terminate_backend(pid) 会杀掉整个连接。面对 idle in transaction 的阻塞者,只有 pg_terminate_backend() 有用,因为 cancel 一个空闲会话没有任何效果。
实战示例
下面模拟一次典型的「线上 DDL 被长事务阻塞」事故,并给出加了超时保护的完整解决方案。
第 1 步:准备数据与环境
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer text NOT NULL,
amount numeric(10,2) NOT NULL,
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO orders (customer, amount, status)
SELECT 'cust-' || g, (random() * 1000)::numeric(10,2),
CASE WHEN g % 3 = 0 THEN 'done' ELSE 'pending' END
FROM generate_series(1, 200000) AS g;
ANALYZE orders;CREATE TABLE
INSERT 0 200000
ANALYZE第 2 步:会话 A 开一个长事务并持有表锁
-- 会话 A:故意不提交,保持在事务中
BEGIN;
SELECT count(*) FROM orders WHERE status = 'pending'; -- 持 ACCESS SHARE 表锁 count
--------
133333
(1 row)第 3 步:会话 B 执行 DDL,观察无限等待
-- 会话 B(不加任何超时,会一直等下去)
ALTER TABLE orders ADD COLUMN remark text;在第三个会话里观察阻塞情况:
SELECT pid,
pg_blocking_pids(pid) AS blocked_by,
wait_event_type,
wait_event,
now() - query_start AS waited,
left(query, 40) AS query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0; pid | blocked_by | wait_event_type | wait_event | waited | query
------+------------+-----------------+------------+--------------+-------------------------
9710 | {9709} | Lock | relation | 00:02:18.440 | ALTER TABLE orders A...ALTER TABLE 已等了 2 分 18 秒,而且它会一直等下去。
第 4 步:用 SET LOCAL 给它加超时保护
先把会话 B 中断(Ctrl+C 或 pg_cancel_backend),然后换成带超时的写法:
-- 会话 B(改进版)
BEGIN;
SET LOCAL lock_timeout = '3s';
SET LOCAL statement_timeout = '30s';
ALTER TABLE orders ADD COLUMN remark text;
COMMIT;ERROR: canceling statement due to lock timeout3 秒后立刻失败,不再无限期占用连接。会话 B 收到错误后回滚(ROLLBACK),接着排查阻塞者:
-- 查看会话 A 的状态:如果它是空闲事务,就必须人为清理
SELECT pid, state, now() - xact_start AS xact_age
FROM pg_stat_activity
WHERE pid = 9709;
-- 确认是空闲事务后终止它
SELECT pg_terminate_backend(9709); pid | state | xact_age
------+---------------------+---------------
9709 | idle in transaction | 00:05:41.227
pg_terminate_backend
----------------------
t第 5 步:清理阻塞者并重试 DDL
会话 B 重试 DDL,这次成功:
BEGIN;
SET LOCAL lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN remark text;
COMMIT;ALTER TABLE
COMMIT第 6 步:把防线固化到数据库级配置
一次性治标之后,治本要把基线参数落地,避免同类事故复发:
-- 给业务库设置超时基线
ALTER DATABASE appdb SET lock_timeout = '5s';
ALTER DATABASE appdb SET statement_timeout = '30s';
ALTER DATABASE appdb SET idle_in_transaction_session_timeout = '60s';
ALTER DATABASE appdb SET idle_session_timeout = '10min';ALTER DATABASE
ALTER DATABASE
ALTER DATABASE
ALTER DATABASE# postgresql.conf:集群级兜底
lock_timeout = '5s'
statement_timeout = '30s'
idle_in_transaction_session_timeout = '60s'
idle_session_timeout = '10min'
log_lock_waits = on
deadlock_timeout = '1s'第 7 步:建立超时监控
-- 一个可直接放进巡检脚本的「锁等待与超时体检」查询
SELECT
count(*) FILTER (WHERE wait_event_type = 'Lock') AS lock_waiters,
count(*) FILTER (WHERE wait_event_type = 'Lock'
AND now() - query_start > interval '5 seconds') AS long_lock_waiters,
count(*) FILTER (WHERE state = 'idle in transaction'
AND now() - state_change > interval '1 minute') AS stuck_idle_xacts,
count(*) FILTER (WHERE now() - xact_start > interval '10 minutes') AS long_xacts
FROM pg_stat_activity
WHERE backend_type = 'client backend'; lock_waiters | long_lock_waiters | stuck_idle_xacts | long_xacts
--------------+-------------------+------------------+------------
2 | 0 | 1 | 1注意事项
错误!以为 deadlock_timeout 能解掉长锁等待。
-- 错误认知:调小 deadlock_timeout 就能让卡住的语句早点失败
SET deadlock_timeout = '100ms';
UPDATE orders SET status = 'x' WHERE id = 42;
-- 事实:若没有等待环,它会一直等下去,deadlock_timeout 不起任何中止作用
-- 正确做法:要「早点失败」必须用 lock_timeout
SET lock_timeout = '100ms';
UPDATE orders SET status = 'x' WHERE id = 42;错误!在生产环境全局设置极短的 statement_timeout,却不考虑长报表。
# 错误!全局 1 秒会让月末报表、批量任务全部失败
statement_timeout = '1s'
# 正确做法:给一个宽松的全局基线,再让特殊业务按需放宽
statement_timeout = '30s'-- 报表/批处理会话自行放宽
SET statement_timeout = '30min';错误!给 DDL 加了 lock_timeout 却不处理失败后的状态。
-- 错误!DDL 超时后事务已中止,直接 COMMIT 实际会回滚,且容易被忽视
BEGIN;
SET LOCAL lock_timeout = '2s';
ALTER TABLE orders ADD COLUMN remark text; -- 超时失败(55P03)
COMMIT;
-- 正确做法:捕获错误后显式 ROLLBACK,再由应用决定是否重试错误!在连接池里给每条语句都设 lock_timeout,但池本身持有长事务。 超时保护的是服务端等锁的语句,如果连接池自己开着事务不放(例如「先 BEGIN,再调用外部服务,再提交」的写法),任何服务端参数都救不了——问题的根在应用代码。
不推荐:直接把 idle_in_transaction_session_timeout 设为极小值(如 5s)。 部分 ORM 会在事务内做多次往返(先 SELECT 校验、再 INSERT),如果两次往返之间因为网络抖动超过 5 秒,连接就会被服务器断开,表现为随机的「连接已关闭」错误,非常难排查。建议从 30~60 秒起步。
最佳实践
- 生产环境必开
idle_in_transaction_session_timeout,因为它是「锁等待事故」最大的根因。
ALTER DATABASE appdb SET idle_in_transaction_session_timeout = '60s';- 线上 DDL 一律套用
SET LOCAL lock_timeout,并把「失败就重试」写进发布脚本。
BEGIN;
SET LOCAL lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN remark text;
COMMIT;- 开启
log_lock_waits,让所有超过deadlock_timeout的等待在日志里可回溯。
log_lock_waits = on- 用
pg_settings.source排查参数为何没生效,它直接给出当前值的最终来源,比逐层猜测高效得多。
SELECT name, setting, source, sourcefile
FROM pg_settings
WHERE name = 'lock_timeout';- 区分对待
55P03与57014:前者退避重试,后者先优化语句。
55P03 lock_not_available → 短暂冲突,退避重试
57014 query_canceled → 语句本身慢/被取消,先分析再决定
40P01 deadlock_detected → 立即重试(需幂等)
40001 serialization_failure → 立即重试(需幂等)- 不要用「设短超时」代替「修代码」。超时是安全网,不是解决方案;根因通常是长事务、更新顺序不一致或缺少索引。
-- 先看根因:有多少长事务出自应用层而不该存在
SELECT pid, usename, application_name, state,
now() - xact_start AS xact_age, left(query, 40) AS query
FROM pg_stat_activity
WHERE now() - xact_start > interval '5 minutes';- 给不同业务配置不同基线,不要「一刀切」:OLTP 连接池要短超时快速失败,报表批处理则要宽松。
-- OLTP:短超时,快速失败
ALTER ROLE app_oltp SET statement_timeout = '10s';
ALTER ROLE app_oltp SET lock_timeout = '1s';
-- 报表:宽松超时
ALTER ROLE app_report SET statement_timeout = '15min';
ALTER ROLE app_report SET lock_timeout = '30s';下一节
继续学习:锁冲突矩阵