Skip to content

隐式与显式事务 ​

在 PostgreSQL 里,「有没有事务」这件事并不总是像 BEGIN / COMMIT 那样显眼:一条单独的 UPDATE 会被自动包进一个事务,一个 COPY 也是一个事务,而 PL/pgSQL 函数的调用整体也在事务里。理解隐式事务(implicit transaction)与显式事务(explicit transaction)的边界,是避免「长事务拖垮数据库」和「跨请求共享事务」这两类生产事故的前提。

前置知识

阅读本节前,建议先了解:BEGIN / COMMIT / ROLLBACK、事务自动提交、MVCC 概述

基础概念 ​

「隐式」与「显式」的区别只在于——事务边界是数据库替你划的,还是你亲手划的:

维度隐式事务显式事务
边界服务端按单条语句自动划定由 BEGIN / COMMIT 划定
覆盖范围一条语句(含其中的所有行与子语句)从 BEGIN 到 COMMIT / ROLLBACK 之间的全部语句
原子性粒度语句级多语句级
生命周期语句结束即结束,通常毫秒级由应用控制,可能持续数秒到数小时
典型风险几乎没有(天然短)长事务、悬挂事务、锁堆积
是否与连接绑定是(事务属于某个后端连接)是
能否跨连接 / 跨请求不能不能

一个必须反复强调的事实:事务是连接(session / backend)级的,不是应用级、不是线程级、不是请求级。 两个 HTTP 请求若使用同一连接池,它们的语句可能落在同一个事务里;反过来,同一次业务如果用了两个连接,就不是一个事务。

text
事务与连接的关系:

应用进程
 ├── 连接 1(后端 PID 8421)  ← 事务 A 只存在于这条连接上
 ├── 连接 2(后端 PID 8422)  ← 事务 B 只存在于这条连接上
 └── 连接 3(后端 PID 8423)  ← 事务 C 只存在于这条连接上

「同一事务」的唯一含义是:语句发往同一条连接,且该连接上事务尚未结束。

语法与用法 ​

显式事务只有三种开启方式与两种结束方式:

text
-- 开启显式事务块
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 直接执行语句」。最小对照示例:

sql
-- 隐式事务:一条语句一个事务
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 100.00 WHERE id = 2;
text
UPDATE 1
UPDATE 1
sql
-- 显式事务:两条语句一个事务
BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 100.00 WHERE id = 2;
COMMIT;
text
BEGIN
UPDATE 1
UPDATE 1
COMMIT
sql
-- 查看自己是否处在事务中:xact_start 有值时即为「事务进行中」
SELECT state, xact_start, now() - xact_start AS xact_age, query
FROM   pg_stat_activity
WHERE  pid = pg_backend_pid();
text
 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)函数里的异常处理用子事务

其中最反直觉的是「一次发送的多条语句会组成一个隐式事务」:

bash
# psql -c 里的多个分号语句,会作为一次简单查询发送 → 一个隐式事务
psql -d mydb -c "INSERT INTO t VALUES (1); INSERT INTO t VALUES (1); \echo done"
text
-- 两条语句在同一隐式事务中:第一条也一并回滚
ERROR:  duplicate key value violates unique constraint "t_pkey"
sql
-- 验证:第一条 INSERT 并没有生效(整串被回滚)
SELECT count(*) FROM t WHERE id = 1;
text
 count
-------
     0
(1 row)

这一点在写脚本时极重要:-c "A; B;" 与 -c A -c B 的语义完全不同——前者是一个事务,后者是两个。

显式事务的边界必须由你在正确的位置划 ​

显式事务的语法很简单,难的是「在哪里划」。三条原则:

  1. 一个业务动作 = 一个事务:一次转账、一次下单、一次库存扣减。
  2. 事务内只做数据库操作:网络调用、文件 IO、消息发送都应在事务外。
  3. 事务要短:以毫秒到秒级为目标,绝不让它跨越用户思考时间。
sql
-- 推荐:事务内纯数据库操作,短小紧凑
BEGIN;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;
INSERT INTO orders (sku, qty) VALUES ('A100', 1);
COMMIT;
-- 事务提交后再发消息/写缓存
text
BEGIN
UPDATE 1
INSERT 0 1
COMMIT
sql
-- 不推荐:把外部调用和「等待」放进事务
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 用于「读出来并加锁,防止别人改」。它的锁随事务结束而释放——在隐式事务(自动提交)模式下,语句一结束锁就释放了,加锁毫无意义:

sql
-- 错误!自动提交模式下 FOR UPDATE 的锁立即释放,起不到保护作用
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
text
 balance
---------
 1000.00
(1 row)
-- 锁在语句结束时已释放,后续 UPDATE 无任何保护
sql
-- 正确:放在显式事务里,锁持续到 COMMIT
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
text
BEGIN
 balance
---------
 1000.00
(1 row)

UPDATE 1
COMMIT

同类的还有「读—判断—写」(check-then-act)模式:读取一个值、根据它做决定、再写回。没有事务包裹时,两次操作之间别人可以插进来修改,产生经典竞态。

sql
-- 错误!两个语句之间没有事务保护,存在竞态
SELECT qty FROM inventory WHERE sku = 'A100';   -- 读到 1
-- (另一个事务此时把它改成 0)
UPDATE inventory SET qty = 0 WHERE sku = 'A100';  -- 基于过期信息写入
sql
-- 正确:用一个事务 + 行锁把「读—判断—写」包起来
BEGIN;
SELECT qty FROM inventory WHERE sku = 'A100' FOR UPDATE;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;
COMMIT;

事务不能跨连接,也不能跨请求 ​

这是分布式应用里最容易出错的地方。以下写法都是错误的:

text
错误!跨连接的事务(不存在这种事务)
  连接 A: BEGIN; UPDATE ...;     ← 事务只存在于连接 A
  连接 B: UPDATE ...;            ← 这是另一个事务,与 A 无关
  连接 A: ROLLBACK;              ← 只回滚 A 的修改,B 的修改已经提交
text
错误!跨 HTTP 请求的事务
  请求 1: BEGIN; INSERT INTO draft ...;   ← 返回响应时连接被归还连接池
  请求 2: INSERT INTO draft_item ...;     ← 复用或不复用连接,语义都不对
  请求 3: COMMIT;                          ← 无法保证还连在同一个后端上

PostgreSQL 的事务是后端进程的状态,没有跨连接的事务。若必须让多个操作成为一个原子单元,只有三条路:

  1. 合并到一个连接的一个事务里(首选)。
  2. 两阶段提交(2PC):PREPARE TRANSACTION / COMMIT PREPARED,代价是长持锁与悬挂事务(见 事务的 ACID 特性)。
  3. 应用层补偿:把「未完成」的中间状态显式建模(状态机、Saga),用幂等重试收敛。
sql
-- 正确:同一连接、同一事务内完成所有相关写操作
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;
text
BEGIN
INSERT 0 1
INSERT 0 1
UPDATE 1
COMMIT

长事务的危害:不只是「慢」 ​

隐式事务天然短,所以长事务几乎总是显式事务造成的。它的危害是复合的:

危害机制
阻塞 VACUUM长事务的 xmin 让所有比它更新的死元组都无法回收
表与索引膨胀死元组堆积,表和索引持续变大,查询变慢
持锁时间过长行锁/表锁一直不放,其它事务排队甚至死锁
事务 ID 环绕风险长事务持有旧 XID,拖住 datfrozenxid 推进
主从延迟放大长事务在备库上也要等同样长的快照
内存与提交日志压力子事务、锁表、CLOG 条目持续占用
sql
-- 找出运行时间超过 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;
text
 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)。这类事务对数据库的伤害和活跃长事务一样大。

sql
-- 更严重:idle in transaction (aborted) —— 事务已失败但连接未回滚
SELECT pid, state, xact_start, now() - xact_start AS age
FROM   pg_stat_activity
WHERE  state = 'idle in transaction (aborted)';
text
 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判断谁在拖住死元组回收
sql
-- 综合监控:谁是长事务、谁在空闲事务中、谁在活跃
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;
text
 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——它正在等第一行持有的事务锁,这就是「一个遗忘的事务阻塞整个系统」的现场。

sql
-- 谁在阻止 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;
text
 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_timeout0(禁用)空闲事务超过该时长则断开连接
statement_timeout0(禁用)单条语句超过该时长则取消
idle_session_timeout0(禁用)非事务状态下空闲超时则断开
transaction_timeout0(禁用)整个事务的最大时长(PostgreSQL 17 引入)
sql
-- 会话级设置
SET idle_in_transaction_session_timeout = '30s';
SET statement_timeout = '10s';
text
SET
SET
sql
-- 全局设置(需重载配置生效)
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();
text
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
-- 手动终止一个确认无用的事务(先看 SQL,再决定)
SELECT pg_terminate_backend(8421);
text
 pg_terminate_backend
----------------------
 t
(1 row)

事务边界划分原则 ​

把业务逻辑映射到事务边界时,遵循这几条经验:

  1. 一个不可分割的业务动作 = 一个事务。 转账、下单、结算。
  2. 先查询校验,再写入,最后提交,但校验必须在事务内并能被锁保护。
  3. 不要在事务里做「等待外部系统」的事:HTTP、MQ、文件、用户交互。
  4. 失败路径必须显式回滚,且回滚要放在 finally / except 里。
  5. 重试必须整事务重试,而不是重试事务中的某条语句(除了用保存点做局部重试)。
  6. 只读长查询与写入事务分开:只读长查询用 REPEATABLE READ + READ ONLY,避免与写事务互相拖累。
sql
-- 推荐:只读长查询用只读事务 + 更低成本快照
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;
text
BEGIN
 count
---------
 1000000
(1 row)

   sum
----------
 1234567.89
(1 row)

COMMIT
sql
-- 推荐:写入事务短小,把校验放在同一事务内
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
# 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 侧同理:

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;
    }
}

两个常见反模式:

text
错误!捕获异常后不回滚,继续用同一连接执行语句
  try { ... } catch (SQLException e) { log.warn(e); }
  // 事务仍处于 aborted 状态,后续所有语句报 25P02

错误!只在事务里重试失败的语句
  BEGIN; A(); B();  // B 失败
  重试 B();          // 错误!事务已 aborted,B 无法执行
  COMMIT;

连接池里的悬挂事务治理 ​

连接池(HikariCP、PgBouncer、Druid 等)会复用连接,一旦归还时事务未结束,下一个借用者就会「继承」这个事务。治理要点:

  1. 归还前强制清理:连接池一般支持 rollbackOnReturn / autoCommit 重置;确认已开启。
  2. PgBouncer 的 transaction 池模式要求应用严格在一个事务内使用一条连接,且事务结束后连接才能被复用——这正好强制了正确的边界习惯。
  3. 数据库兜底超时:idle_in_transaction_session_timeout 断开残留事务。
  4. 主动巡检:定期查询 pg_stat_activity 中 idle in transaction 并告警。
sql
-- 巡检:空闲事务按年龄排序,超过阈值即告警
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;
text
 pid | usename | datname | state | xact_age | client_addr | last_query
-----+---------+---------+-------+----------+-------------+------------
(0 rows)
ini
# postgresql.conf:兜底设置,避免任何来源的悬挂事务长期存在
idle_in_transaction_session_timeout = '5min'
statement_timeout = '30s'
idle_session_timeout = '30min'

实战示例 ​

场景:一个「下单扣库存」流程,先用错误写法复现长事务问题,再改正。

第 1 步:准备数据

sql
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);
text
DROP TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 2

第 2 步:复现隐式事务下「读—判断—写」的竞态

text
会话 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 的结果
text
-- 会话 A 输出
 qty
-----
  10
(1 row)

UPDATE 1
-- 最终 qty = 9,但真实库存早已被 B 设为 0

第 3 步:改正为显式事务 + 行锁

sql
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;
text
BEGIN
 qty
-----
   9
(1 row)

UPDATE 1
INSERT 0 1
COMMIT

第 4 步:观察事务状态与清理

sql
SELECT sku, qty FROM inventory ORDER BY sku;
SELECT count(*) AS order_count FROM orders;
DROP TABLE orders, inventory;
text
 sku  | qty
------+-----
 A100 |   8
 B200 |   5
(2 rows)

 order_count
-------------
           1
(1 row)

DROP TABLE

注意事项 ​

  • 不存在跨连接、跨请求的事务。 任何「请求 1 开事务、请求 2 提交」的设计都是错的。错误! 用两个连接执行 BEGIN 与 COMMIT。
text
错误!
  连接 A: BEGIN;
  连接 B: INSERT ...;   -- 不在 A 的事务里
  连接 A: COMMIT;       -- 无法提交 B 的写入
  • SELECT ... FOR UPDATE 放在自动提交模式里等于没加锁。 锁随语句结束即释放,保护窗口为零。
sql
-- 错误!锁立即释放
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 里只打日志不回滚。
sql
BEGIN;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100';
SELECT 1/0;
-- 错误!current transaction is aborted
SELECT 1;
text
ERROR:  division by zero
ERROR:  current transaction is aborted, commands ignored until end of transaction block
  • 不要把外部调用放进事务。 事务里做 HTTP / MQ / 文件 IO,会让事务时长等于外部系统响应时间。
sql
-- 不推荐:事务跨越外部调用
BEGIN;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100';
-- 调用支付网关(数秒)
INSERT INTO orders (sku, qty) VALUES ('A100', 1);
COMMIT;
  • idle in transaction 与活跃长事务危害相同。 它一样占用 xmin、阻止 VACUUM、持有锁。错误! 认为「事务没在执行语句就不影响数据库」。
sql
-- 一眼识别
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 是两个事务。 写自动化脚本时必须分清。
bash
# 一个隐式事务:任一句失败则全部回滚
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。
python
# 推荐:上下文管理器保证「要么提交要么回滚」
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,杜绝隐式事务下的竞态。
sql
BEGIN;
SELECT qty FROM inventory WHERE sku = 'A100' FOR UPDATE;
UPDATE inventory SET qty = qty - 1 WHERE sku = 'A100' AND qty > 0;
COMMIT;
  • 给数据库配置三层超时兜底,让任何遗忘的事务都无法长期存活。
sql
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 排序并对超阈值告警。
sql
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 等),从池层面兜住悬挂事务。
ini
# HikariCP 关键配置
# autoCommit=true            (连接级默认自动提交)
# rollbackOnReturn=true      (归还时回滚未结束事务)
# maximumPoolSize=20

下一节

继续学习:READ UNCOMMITTED / READ COMMITTED

参考链接 ​