隐式与显式事务
在 PostgreSQL 里,「有没有事务」这件事并不总是像 BEGIN / COMMIT 那样显眼:一条单独的 UPDATE 会被自动包进一个事务,一个 COPY 也是一个事务,而 PL/pgSQL 函数的调用整体也在事务里。理解隐式事务(implicit transaction)与显式事务(explicit transaction)的边界,是避免「长事务拖垮数据库」和「跨请求共享事务」这两类生产事故的前提。
前置知识
阅读本节前,建议先了解:BEGIN / COMMIT / ROLLBACK、事务自动提交、MVCC 概述
基础概念
「隐式」与「显式」的区别只在于——事务边界是数据库替你划的,还是你亲手划的:
| 维度 | 隐式事务 | 显式事务 |
|---|---|---|
| 边界 | 服务端按单条语句自动划定 | 由 BEGIN / COMMIT 划定 |
| 覆盖范围 | 一条语句(含其中的所有行与子语句) | 从 BEGIN 到 COMMIT / ROLLBACK 之间的全部语句 |
| 原子性粒度 | 语句级 | 多语句级 |
| 生命周期 | 语句结束即结束,通常毫秒级 | 由应用控制,可能持续数秒到数小时 |
| 典型风险 | 几乎没有(天然短) | 长事务、悬挂事务、锁堆积 |
| 是否与连接绑定 | 是(事务属于某个后端连接) | 是 |
| 能否跨连接 / 跨请求 | 不能 | 不能 |
一个必须反复强调的事实:事务是连接(session / backend)级的,不是应用级、不是线程级、不是请求级。 两个 HTTP 请求若使用同一连接池,它们的语句可能落在同一个事务里;反过来,同一次业务如果用了两个连接,就不是一个事务。
事务与连接的关系:
应用进程
├── 连接 1(后端 PID 8421) ← 事务 A 只存在于这条连接上
├── 连接 2(后端 PID 8422) ← 事务 B 只存在于这条连接上
└── 连接 3(后端 PID 8423) ← 事务 C 只存在于这条连接上
「同一事务」的唯一含义是:语句发往同一条连接,且该连接上事务尚未结束。语法与用法
显式事务只有三种开启方式与两种结束方式:
-- 开启显式事务块
BEGIN [ WORK | TRANSACTION ] [ transaction_mode [, ...] ];
START TRANSACTION [ transaction_mode [, ...] ];
-- 结束
COMMIT [ WORK | TRANSACTION ]; -- 也可写成 END
ROLLBACK [ WORK | TRANSACTION ]; -- ABORT 是 ROLLBACK 的别名
-- transaction_mode 可包含:
-- ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }
-- READ WRITE | READ ONLY
-- [ NOT ] DEFERRABLE隐式事务没有对应语法——它就是「不发 BEGIN 直接执行语句」。最小对照示例:
-- 隐式事务:一条语句一个事务
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 100.00 WHERE id = 2;UPDATE 1
UPDATE 1-- 显式事务:两条语句一个事务
BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 100.00 WHERE id = 2;
COMMIT;BEGIN
UPDATE 1
UPDATE 1
COMMIT-- 查看自己是否处在事务中:xact_start 有值时即为「事务进行中」
SELECT state, xact_start, now() - xact_start AS xact_age, query
FROM pg_stat_activity
WHERE pid = pg_backend_pid(); state | xact_start | xact_age | query
--------+-------------------------------+----------+---------------------
active | 2026-09-28 10:00:00.123456+08 | 00:00:00 | SELECT state, ...
(1 row)详细说明
隐式事务的形态:不只是「自动提交的单条语句」
很多操作都自带隐式事务,只是表现不明显:
| 操作 | 隐式事务边界 | 说明 |
|---|---|---|
单条 DML(INSERT / UPDATE / DELETE) | 该语句 | 语句级原子性 |
单条 DDL(CREATE / ALTER / DROP) | 该语句 | PostgreSQL 的 DDL 可回滚 |
COPY | 整次 COPY | 整批数据要么全进要么全不进 |
TRUNCATE | 该语句 | 全表清空是原子操作 |
SELECT | 该语句 | 只读,不需要 XID |
| 多语句简单查询串 | 整串 | 通过一次协议消息发送的多条语句会组成一个隐式事务 |
| PL/pgSQL 函数调用 | 调用该函数的那条语句(若函数内无 COMMIT) | 函数里的异常处理用子事务 |
其中最反直觉的是「一次发送的多条语句会组成一个隐式事务」:
# psql -c 里的多个分号语句,会作为一次简单查询发送 → 一个隐式事务
psql -d mydb -c "INSERT INTO t VALUES (1); INSERT INTO t VALUES (1); \echo done"-- 两条语句在同一隐式事务中:第一条也一并回滚
ERROR: duplicate key value violates unique constraint "t_pkey"-- 验证:第一条 INSERT 并没有生效(整串被回滚)
SELECT count(*) FROM t WHERE id = 1; count
-------
0
(1 row)这一点在写脚本时极重要:-c "A; B;" 与 -c A -c B 的语义完全不同——前者是一个事务,后者是两个。
显式事务的边界必须由你在正确的位置划
显式事务的语法很简单,难的是「在哪里划」。三条原则:
- 一个业务动作 = 一个事务:一次转账、一次下单、一次库存扣减。
- 事务内只做数据库操作:网络调用、文件 IO、消息发送都应在事务外。
- 事务要短:以毫秒到秒级为目标,绝不让它跨越用户思考时间。
-- 推荐:事务内纯数据库操作,短小紧凑
BEGIN;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;
INSERT INTO orders (sku, qty) VALUES ('A100', 1);
COMMIT;
-- 事务提交后再发消息/写缓存BEGIN
UPDATE 1
INSERT 0 1
COMMIT-- 不推荐:把外部调用和「等待」放进事务
BEGIN;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100';
-- 错误!这里调用第三方支付接口,可能耗时数秒到数十秒
-- http_client.post('https://pay.example.com/charge', ...)
INSERT INTO orders (sku, qty) VALUES ('A100', 1);
COMMIT; -- 整个期间持有行锁后者的直接后果是:锁被长时间持有,其它事务排队等待,idle in transaction 堆积,VACUUM 无法回收死元组。
SELECT FOR UPDATE 必须在事务中才有意义
SELECT ... FOR UPDATE 用于「读出来并加锁,防止别人改」。它的锁随事务结束而释放——在隐式事务(自动提交)模式下,语句一结束锁就释放了,加锁毫无意义:
-- 错误!自动提交模式下 FOR UPDATE 的锁立即释放,起不到保护作用
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; balance
---------
1000.00
(1 row)
-- 锁在语句结束时已释放,后续 UPDATE 无任何保护-- 正确:放在显式事务里,锁持续到 COMMIT
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;BEGIN
balance
---------
1000.00
(1 row)
UPDATE 1
COMMIT同类的还有「读—判断—写」(check-then-act)模式:读取一个值、根据它做决定、再写回。没有事务包裹时,两次操作之间别人可以插进来修改,产生经典竞态。
-- 错误!两个语句之间没有事务保护,存在竞态
SELECT qty FROM inventory WHERE sku = 'A100'; -- 读到 1
-- (另一个事务此时把它改成 0)
UPDATE inventory SET qty = 0 WHERE sku = 'A100'; -- 基于过期信息写入-- 正确:用一个事务 + 行锁把「读—判断—写」包起来
BEGIN;
SELECT qty FROM inventory WHERE sku = 'A100' FOR UPDATE;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;
COMMIT;事务不能跨连接,也不能跨请求
这是分布式应用里最容易出错的地方。以下写法都是错误的:
错误!跨连接的事务(不存在这种事务)
连接 A: BEGIN; UPDATE ...; ← 事务只存在于连接 A
连接 B: UPDATE ...; ← 这是另一个事务,与 A 无关
连接 A: ROLLBACK; ← 只回滚 A 的修改,B 的修改已经提交错误!跨 HTTP 请求的事务
请求 1: BEGIN; INSERT INTO draft ...; ← 返回响应时连接被归还连接池
请求 2: INSERT INTO draft_item ...; ← 复用或不复用连接,语义都不对
请求 3: COMMIT; ← 无法保证还连在同一个后端上PostgreSQL 的事务是后端进程的状态,没有跨连接的事务。若必须让多个操作成为一个原子单元,只有三条路:
- 合并到一个连接的一个事务里(首选)。
- 两阶段提交(2PC):
PREPARE TRANSACTION/COMMIT PREPARED,代价是长持锁与悬挂事务(见 事务的 ACID 特性)。 - 应用层补偿:把「未完成」的中间状态显式建模(状态机、Saga),用幂等重试收敛。
-- 正确:同一连接、同一事务内完成所有相关写操作
BEGIN;
INSERT INTO drafts (id, owner) VALUES (1, 'alice');
INSERT INTO draft_items (draft_id, sku) VALUES (1, 'A100');
UPDATE drafts SET status = 'submitted' WHERE id = 1;
COMMIT;BEGIN
INSERT 0 1
INSERT 0 1
UPDATE 1
COMMIT长事务的危害:不只是「慢」
隐式事务天然短,所以长事务几乎总是显式事务造成的。它的危害是复合的:
| 危害 | 机制 |
|---|---|
| 阻塞 VACUUM | 长事务的 xmin 让所有比它更新的死元组都无法回收 |
| 表与索引膨胀 | 死元组堆积,表和索引持续变大,查询变慢 |
| 持锁时间过长 | 行锁/表锁一直不放,其它事务排队甚至死锁 |
| 事务 ID 环绕风险 | 长事务持有旧 XID,拖住 datfrozenxid 推进 |
| 主从延迟放大 | 长事务在备库上也要等同样长的快照 |
| 内存与提交日志压力 | 子事务、锁表、CLOG 条目持续占用 |
-- 找出运行时间超过 5 分钟的事务
SELECT pid, usename, state, xact_start,
now() - xact_start AS xact_age,
now() - query_start AS query_age,
left(query, 70) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
AND now() - xact_start > interval '5 minutes'
ORDER BY xact_start; pid | usename | state | xact_start | xact_age | query
------+---------+---------------------+-------------------------------+----------+-------------
8421 | app | idle in transaction | 2026-09-28 09:40:00.111111+08 | 00:25:00 | BEGIN
(1 row)注意 state = 'idle in transaction':事务开着但什么都没干,这通常意味着应用在事务中做了慢操作(HTTP 调用、等待用户输入、sleep)。这类事务对数据库的伤害和活跃长事务一样大。
-- 更严重:idle in transaction (aborted) —— 事务已失败但连接未回滚
SELECT pid, state, xact_start, now() - xact_start AS age
FROM pg_stat_activity
WHERE state = 'idle in transaction (aborted)'; pid | state | xact_start | age
-----+-------+------------+-----
(0 rows)监控事务:pg_stat_activity 的三个关键列
pg_stat_activity 是观察事务的第一现场,三个列要牢记:
| 列 | 含义 | 用途 |
|---|---|---|
xact_start | 当前事务的开始时间 | 有值即为「事务进行中」;判断长事务 |
query_start | 当前语句的开始时间 | 判断单条语句是否卡住 |
state | 后端状态 | 区分 active / idle / idle in transaction |
backend_xid | 后端当前事务 ID | 结合其它视图定位「谁在拖住 VACUUM」 |
backend_xmin | 快照的 xmin | 判断谁在拖住死元组回收 |
-- 综合监控:谁是长事务、谁在空闲事务中、谁在活跃
SELECT pid,
state,
now() - xact_start AS xact_age,
now() - query_start AS query_age,
wait_event_type,
wait_event,
left(query, 50) AS query
FROM pg_stat_activity
WHERE backend_type = 'client backend'
ORDER BY xact_start NULLS LAST; pid | state | xact_age | query_age | wait_event_type | wait_event | query
------+---------------------+----------+-----------+-----------------+------------+----------
8421 | idle in transaction | 00:25:00 | 00:25:00 | Client | ClientRead | UPDATE ...
8422 | active | 00:00:03 | 00:00:03 | Lock | transactionid | SELECT ...
8423 | idle | | | Client | ClientRead | SELECT 1
(3 rows)注意第二行的 wait_event = transactionid——它正在等第一行持有的事务锁,这就是「一个遗忘的事务阻塞整个系统」的现场。
-- 谁在阻止 VACUUM 清理死元组:按事务开始时间排序找到最老的 backend_xmin
SELECT pid, state, backend_xmin, now() - xact_start AS age
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY xact_start NULLS LAST; pid | state | backend_xmin | age
------+---------------------+--------------+----------
8421 | idle in transaction | 732401 | 00:25:00
(1 row)idle in transaction 与超时治理
idle in transaction 是长事务里最常见也最该治理的形态。PostgreSQL 提供了专门的超时参数:
| 参数 | 默认值 | 作用 |
|---|---|---|
idle_in_transaction_session_timeout | 0(禁用) | 空闲事务超过该时长则断开连接 |
statement_timeout | 0(禁用) | 单条语句超过该时长则取消 |
idle_session_timeout | 0(禁用) | 非事务状态下空闲超时则断开 |
transaction_timeout | 0(禁用) | 整个事务的最大时长(PostgreSQL 17 引入) |
-- 会话级设置
SET idle_in_transaction_session_timeout = '30s';
SET statement_timeout = '10s';SET
SET-- 全局设置(需重载配置生效)
ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min';
ALTER SYSTEM SET statement_timeout = '30s';
ALTER SYSTEM SET transaction_timeout = '10min';
SELECT pg_reload_conf();ALTER SYSTEM
ALTER SYSTEM
ALTER SYSTEM
pg_reload_conf
----------------
t
(1 row)选择阈值的原则:statement_timeout 设为「正常慢查询上界」(如 30s);idle_in_transaction_session_timeout 设为「业务最长合理事务时长」(如 1~5 分钟);transaction_timeout 作为总兜底。
-- 手动终止一个确认无用的事务(先看 SQL,再决定)
SELECT pg_terminate_backend(8421); pg_terminate_backend
----------------------
t
(1 row)事务边界划分原则
把业务逻辑映射到事务边界时,遵循这几条经验:
- 一个不可分割的业务动作 = 一个事务。 转账、下单、结算。
- 先查询校验,再写入,最后提交,但校验必须在事务内并能被锁保护。
- 不要在事务里做「等待外部系统」的事:HTTP、MQ、文件、用户交互。
- 失败路径必须显式回滚,且回滚要放在
finally/except里。 - 重试必须整事务重试,而不是重试事务中的某条语句(除了用保存点做局部重试)。
- 只读长查询与写入事务分开:只读长查询用
REPEATABLE READ+READ ONLY,避免与写事务互相拖累。
-- 推荐:只读长查询用只读事务 + 更低成本快照
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;
SELECT count(*) FROM big_table;
SELECT sum(amount) FROM orders WHERE created_at >= '2026-01-01';
COMMIT;BEGIN
count
---------
1000000
(1 row)
sum
----------
1234567.89
(1 row)
COMMIT-- 推荐:写入事务短小,把校验放在同一事务内
BEGIN;
SELECT qty FROM inventory WHERE sku = 'A100' FOR UPDATE;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;
INSERT INTO orders (sku, qty) VALUES ('A100', 1);
COMMIT;
-- 提交后再做通知、缓存失效等副作用错误处理模式:事务失败后必须回滚
一旦事务中有语句报错,事务进入 aborted 状态(SQLSTATE 25P02),此时除了 ROLLBACK / ROLLBACK TO SAVEPOINT,其它语句一律被拒。因此错误处理的标准形态是「捕获异常 → 回滚 → 决定是否重试」:
# Python + psycopg3:标准的错误处理与重试模式
import psycopg
from psycopg import errors
def transfer(conn, from_id: int, to_id: int, amount: float) -> None:
for attempt in range(3):
try:
with conn.transaction(): # 退出时自动 COMMIT,异常时自动 ROLLBACK
with conn.cursor() as cur:
cur.execute(
"UPDATE accounts SET balance = balance - %s WHERE id = %s",
(amount, from_id),
)
cur.execute(
"UPDATE accounts SET balance = balance + %s WHERE id = %s",
(amount, to_id),
)
return
except errors.SerializationFailure: # SQLSTATE 40001
if attempt == 2:
raise
# 稍作退避后重试整个事务Java 侧同理:
// JDBC:捕获异常必须回滚,且回滚放在 catch 中
int attempts = 0;
while (true) {
try {
conn.setAutoCommit(false);
// ... 业务语句 ...
conn.commit();
break;
} catch (SQLException e) {
conn.rollback(); // 关键:失败必须回滚
if ("40001".equals(e.getSQLState()) && ++attempts < 3) {
continue; // 序列化失败则整事务重试
}
throw e;
}
}两个常见反模式:
错误!捕获异常后不回滚,继续用同一连接执行语句
try { ... } catch (SQLException e) { log.warn(e); }
// 事务仍处于 aborted 状态,后续所有语句报 25P02
错误!只在事务里重试失败的语句
BEGIN; A(); B(); // B 失败
重试 B(); // 错误!事务已 aborted,B 无法执行
COMMIT;连接池里的悬挂事务治理
连接池(HikariCP、PgBouncer、Druid 等)会复用连接,一旦归还时事务未结束,下一个借用者就会「继承」这个事务。治理要点:
- 归还前强制清理:连接池一般支持
rollbackOnReturn/autoCommit重置;确认已开启。 - PgBouncer 的
transaction池模式要求应用严格在一个事务内使用一条连接,且事务结束后连接才能被复用——这正好强制了正确的边界习惯。 - 数据库兜底超时:
idle_in_transaction_session_timeout断开残留事务。 - 主动巡检:定期查询
pg_stat_activity中idle in transaction并告警。
-- 巡检:空闲事务按年龄排序,超过阈值即告警
SELECT pid,
usename,
datname,
state,
now() - xact_start AS xact_age,
client_addr,
left(query, 60) AS last_query
FROM pg_stat_activity
WHERE state IN ('idle in transaction', 'idle in transaction (aborted)')
AND now() - xact_start > interval '1 minute'
ORDER BY xact_start; pid | usename | datname | state | xact_age | client_addr | last_query
-----+---------+---------+-------+----------+-------------+------------
(0 rows)# postgresql.conf:兜底设置,避免任何来源的悬挂事务长期存在
idle_in_transaction_session_timeout = '5min'
statement_timeout = '30s'
idle_session_timeout = '30min'实战示例
场景:一个「下单扣库存」流程,先用错误写法复现长事务问题,再改正。
第 1 步:准备数据
DROP TABLE IF EXISTS orders, inventory;
CREATE TABLE inventory (
sku text PRIMARY KEY,
qty int NOT NULL CHECK (qty >= 0)
);
CREATE TABLE orders (
id bigserial PRIMARY KEY,
sku text NOT NULL REFERENCES inventory(sku),
qty int NOT NULL CHECK (qty > 0),
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO inventory (sku, qty) VALUES ('A100', 10), ('B200', 5);DROP TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 2第 2 步:复现隐式事务下「读—判断—写」的竞态
会话 A(隐式事务,无保护):
SELECT qty FROM inventory WHERE sku = 'A100'; -- 读到 10
会话 B(同时执行):
UPDATE inventory SET qty = 0 WHERE sku = 'A100';
-- B 提交
会话 A 继续(基于过期信息写入):
UPDATE inventory SET qty = 9 WHERE sku = 'A100'; -- 错误地覆盖了 B 的结果-- 会话 A 输出
qty
-----
10
(1 row)
UPDATE 1
-- 最终 qty = 9,但真实库存早已被 B 设为 0第 3 步:改正为显式事务 + 行锁
BEGIN;
SELECT qty FROM inventory WHERE sku = 'A100' FOR UPDATE; -- 加行锁,读到 9
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;
INSERT INTO orders (sku, qty) VALUES ('A100', 1);
COMMIT;BEGIN
qty
-----
9
(1 row)
UPDATE 1
INSERT 0 1
COMMIT第 4 步:观察事务状态与清理
SELECT sku, qty FROM inventory ORDER BY sku;
SELECT count(*) AS order_count FROM orders;
DROP TABLE orders, inventory; sku | qty
------+-----
A100 | 8
B200 | 5
(2 rows)
order_count
-------------
1
(1 row)
DROP TABLE注意事项
- 不存在跨连接、跨请求的事务。 任何「请求 1 开事务、请求 2 提交」的设计都是错的。错误! 用两个连接执行
BEGIN与COMMIT。
错误!
连接 A: BEGIN;
连接 B: INSERT ...; -- 不在 A 的事务里
连接 A: COMMIT; -- 无法提交 B 的写入SELECT ... FOR UPDATE放在自动提交模式里等于没加锁。 锁随语句结束即释放,保护窗口为零。
-- 错误!锁立即释放
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 正确
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
COMMIT;- 事务失败后不能「接着往下跑」。 必须
ROLLBACK或ROLLBACK TO SAVEPOINT,否则所有后续语句报25P02。不推荐在 catch 里只打日志不回滚。
BEGIN;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100';
SELECT 1/0;
-- 错误!current transaction is aborted
SELECT 1;ERROR: division by zero
ERROR: current transaction is aborted, commands ignored until end of transaction block- 不要把外部调用放进事务。 事务里做 HTTP / MQ / 文件 IO,会让事务时长等于外部系统响应时间。
-- 不推荐:事务跨越外部调用
BEGIN;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100';
-- 调用支付网关(数秒)
INSERT INTO orders (sku, qty) VALUES ('A100', 1);
COMMIT;idle in transaction与活跃长事务危害相同。 它一样占用xmin、阻止VACUUM、持有锁。错误! 认为「事务没在执行语句就不影响数据库」。
-- 一眼识别
SELECT pid, state, now() - xact_start AS age
FROM pg_stat_activity
WHERE state LIKE 'idle in transaction%';- psql 的
-c "A; B;"是一个事务,-c A -c B是两个事务。 写自动化脚本时必须分清。
# 一个隐式事务:任一句失败则全部回滚
psql -d mydb -c "UPDATE a SET x=1; UPDATE b SET y=2;"
# 两个隐式事务:互不影响
psql -d mydb -c "UPDATE a SET x=1;" -c "UPDATE b SET y=2;"最佳实践
- 用「事务脚本」把边界写死在代码结构里:一个函数一个事务,进入即
BEGIN,成功即COMMIT,异常即ROLLBACK。
# 推荐:上下文管理器保证「要么提交要么回滚」
with conn.transaction():
cur.execute("UPDATE inventory SET qty = qty - 1 WHERE sku = %s AND qty > 0", ("A100",))
cur.execute("INSERT INTO orders (sku, qty) VALUES (%s, %s)", ("A100", 1))- 所有涉及「读—判断—写」的逻辑都必须包在事务里,并视情况加
FOR UPDATE,杜绝隐式事务下的竞态。
BEGIN;
SELECT qty FROM inventory WHERE sku = 'A100' FOR UPDATE;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;
COMMIT;- 给数据库配置三层超时兜底,让任何遗忘的事务都无法长期存活。
ALTER SYSTEM SET statement_timeout = '30s';
ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min';
ALTER SYSTEM SET transaction_timeout = '10min';
SELECT pg_reload_conf();- 把长事务监控纳入日常巡检,用
pg_stat_activity按xact_start排序并对超阈值告警。
SELECT pid, state, now() - xact_start AS age, left(query, 60)
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start
LIMIT 10;- 连接池开启归还前回滚(HikariCP 的
rollbackOnReturn、PgBouncer 的server_reset_query等),从池层面兜住悬挂事务。
# HikariCP 关键配置
# autoCommit=true (连接级默认自动提交)
# rollbackOnReturn=true (归还时回滚未结束事务)
# maximumPoolSize=20