Heath Kang

从 ACID 到生产排障:PostgreSQL 的重要机制

上一篇对比 PostgreSQL、Cassandra 和 Elasticsearch时,我把 PostgreSQL 概括成“维护业务事实的关系型数据库”。这次换一个更适合理解数据库的入口:ACID

ACID 不是 PostgreSQL 独有的四个开关,而是关系型数据库希望提供的一组事务语义:一次业务操作如何完整地发生,多个事务如何互不破坏,提交之后的数据如何在崩溃中保留下来。

真正有用的问题不是背出 Atomicity、Consistency、Isolation、Durability,而是当线上出现下面这些现象时,知道应该回到哪一层:

订单只写了一半,为什么?                 → Atomicity / 事务边界
两个请求都扣了最后一件库存,为什么?       → Isolation / 原子更新
数据库突然变慢,为什么所有请求都在等?     → Lock / 长事务 / 连接池
重启后数据还在,但恢复很久,为什么?       → Durability / WAL / Checkpoint
表越来越大,磁盘快满了,为什么?           → MVCC / VACUUM / 膨胀
一条 SQL 昨天很快,今天突然变慢,为什么?   → Planner / 统计信息 / 索引

本文的主线是:

业务事务
约束 + 原子写入                  Consistency / Atomicity
MVCC 快照 + 锁                   Isolation
WAL flush + COMMIT               Durability
Checkpoint / Replication
VACUUM 回收旧版本,Planner 使用统计信息

PostgreSQL 以 ACID 为主线,从事务、MVCC、锁、WAL 到 VACUUM 和生产排障的机制总览

1. 先建立一张 ACID 地图

特性要回答的问题PostgreSQL 的主要机制常见故障入口
Atomicity 原子性事务是否要么全部发生、要么全部不发生?Transaction、WAL、回滚、Savepoint事务中途报错、外部系统无法回滚
Consistency 一致性提交后数据是否满足规则?Primary Key、Foreign Key、UNIQUE、CHECK约束失败、业务不变量遗漏
Isolation 隔离性并发事务互相看到什么、如何冲突?MVCC、快照、行锁、表级锁锁等待、死锁、序列化失败
Durability 持久性COMMIT 成功后崩溃,结果是否保留?WAL、fsync、同步提交、Checkpoint恢复时间、WAL 堆积、复制延迟

这四者不是四个完全分开的模块。例如,约束失败会让事务回滚,这是 Consistency 通过 Atomicity 生效;MVCC 让读取互不阻塞,但更新仍需要锁,这是 Isolation 的两个不同组成部分;WAL 既支持崩溃恢复,也支持复制,因此 Durability 会延伸到高可用架构。

2. 一次事务在 PostgreSQL 里经过什么

考虑一个创建订单的事务:

BEGIN;

INSERT INTO orders (order_no, user_id)
VALUES ('O-1001', 42);

UPDATE products
SET stock = stock - 1
WHERE id = 7 AND stock > 0;

INSERT INTO payments (order_no, status)
VALUES ('O-1001', 'pending');

COMMIT;

可以把它放回数据库内部:

SQL
Parser / Analyzer
Planner:选择 Seq Scan、Index Scan、Join 算法
Executor:读取 heap / index,检查快照与约束
MVCC:判断哪些 tuple 对当前事务可见
Lock:协调相互冲突的修改
WAL:记录可以重放的变更
COMMIT:WAL 按持久性策略 flush 后提交
后续事务看到新的已提交版本

Index 负责更快找到候选行,MVCC 判断候选 tuple 对当前快照是否可见,Lock 协调同一资源的并发修改,WAL 让崩溃后可以恢复。它们不是互相替代的机制,而是同一次请求中的不同环节。

3. Atomicity:事务为什么能“要么全成功,要么全失败”

事务原子性的直观含义是:

BEGIN → 写订单 → 扣库存 → 写支付意图 → COMMIT

→ 三步一起对外可见
→ 任一步失败,整个事务回滚

如果第二步违反约束或执行失败,事务不会留下一个“只有订单、没有库存变化”的中间状态。事务内部可能已经产生了新 tuple 和 WAL 记录,但它们不会成为后续事务可见的已提交事实。

事务出错后不能继续使用

BEGIN;
INSERT INTO users (email) VALUES ('[email protected]');
-- duplicate key error

SELECT * FROM users;
-- ERROR: current transaction is aborted

一条语句失败后,当前事务进入 aborted 状态。应用必须执行 ROLLBACK,或者使用 SAVEPOINT 回到局部状态;不能忽略错误继续发送普通 SQL。

BEGIN;
SAVEPOINT optional_step;

-- 可能失败的操作
ROLLBACK TO SAVEPOINT optional_step;

-- 继续执行事务的其他部分
COMMIT;

生产代码里更常见的做法是把异常事务完整回滚,并由应用决定重试、返回错误还是走补偿流程。

ACID 的边界:数据库不能回滚外部系统

数据库事务
  ├── orders
  ├── inventory
  └── outbox_events
COMMIT
异步 worker 调用支付网关或消息系统

如果在数据库事务里调用支付服务,支付已经成功而数据库随后回滚,数据库无法替外部服务撤销这次操作。常见的解决方式是事务内记录 outbox 事件,提交后异步发送;外部请求使用幂等键,失败通过重试和状态机恢复。

事务边界应该画在数据库能够原子提交的地方;跨系统一致性需要 outbox、幂等和补偿。

4. Consistency:谁来保证数据满足规则

Consistency 在 ACID 语境中不是“每个读都立刻看到最新值”,而是事务提交后,数据满足数据库和应用声明的不变量。

优先把不变量交给数据库

CREATE TABLE memberships (
  user_id bigint REFERENCES users(id),
  team_id bigint REFERENCES teams(id),
  role text NOT NULL CHECK (role IN ('admin', 'member')),
  PRIMARY KEY (user_id, team_id)
);
PRIMARY KEY → 身份唯一
FOREIGN KEY → 引用不能悬空
UNIQUE      → 业务键不能重复
CHECK       → 当前行必须满足表达式
NOT NULL    → 字段必须存在

约束是在并发环境中的最后一道边界。并发创建订单时,不要先 SELECT 检查订单号不存在,再依赖应用不出错;应该让唯一约束做最终裁决:

INSERT INTO orders (order_no, user_id)
VALUES ($1, $2)
ON CONFLICT (order_no) DO NOTHING;

业务规则没有被自动表达

CHECK 通常只检查当前行,不能直接表达“所有订单总额不超过账户余额”这类跨行规则。可以按规则选择机制:

单行不变量       → CHECK / NOT NULL
唯一性           → UNIQUE / PRIMARY KEY
引用关系         → FOREIGN KEY
竞争资源         → 原子 UPDATE 或 SELECT FOR UPDATE
复杂跨行规则     → 事务 + 锁 / Serializable

库存扣减的安全写法是把条件和修改放在同一条 SQL 中:

UPDATE products
SET stock = stock - 1
WHERE id = 42
  AND stock > 0
RETURNING id, stock;

没有返回行时,应用应判断库存不足或商品不存在,而不是默认扣减成功。也可以增加 CHECK (stock >= 0),让数据库保护最后一道边界。

5. Isolation:MVCC、快照与锁如何一起工作

隔离性要回答两个问题:一个事务能看到什么版本,以及两个事务修改同一资源时如何协调。它不是“数据库有没有锁”的二选一,而是快照、版本可见性、行锁和冲突检测共同形成的行为。

MVCC:UPDATE 不是把旧行覆盖掉

旧 Tuple
id=1, balance=1000
      │ UPDATE
新 Tuple
id=1, balance=900

PostgreSQL 的 heap 中会保留 tuple 版本及其事务可见性信息。普通查询根据自己的快照判断哪个版本可见,因此未提交的更新不会被另一个普通 SELECT 读到:

Heap
├── balance=1000 ← 某些快照仍可见
└── balance=900  ← 新版本,提交后对后续快照可见

这就是“普通读通常不阻塞写、写通常也不阻塞普通读”的来源。但 MVCC 只解决可见性,不会自动保证业务判断和后续修改不可分割。

四个隔离等级怎么比较

PostgreSQL 支持 SQL 标准中的四个名字,但 READ UNCOMMITTED 在 PostgreSQL 中实际按 READ COMMITTED 执行,因为 PostgreSQL 不允许脏读:

隔离等级PostgreSQL 的快照语义可能观察到的现象应用需要准备什么
READ UNCOMMITTED等同于 READ COMMITTED不允许脏读通常直接使用默认等级
READ COMMITTED每条语句一个快照同一事务两次查询可能不同正确表达原子更新或显式加锁
REPEATABLE READ事务级快照读取保持一致;并发更新可能失败处理 40001 并重试
SERIALIZABLE事务级快照 + 可串行化冲突检测可能因依赖冲突失败重试整个事务,控制事务范围

可以把它们理解成逐渐收紧的约束:

Read Committed
  → 每条语句看到当时已提交的世界

Repeatable Read
  → 一个事务里的普通读取看到同一个快照

Serializable
  → 最终结果必须等价于某个串行执行顺序

隔离等级越强,不代表业务一定越正确,也不代表性能一定越差;它意味着数据库可能更频繁地让事务等待或失败,应用必须具备重试和幂等能力。

Read Committed:每条语句一个快照

PostgreSQL 默认隔离级别是 READ COMMITTED。同一个事务里的每条 SQL 通常获得自己的语句快照:

T1: BEGIN;
T1: SELECT balance ...;  -- 看到 1000

T2: UPDATE accounts SET balance = balance - 100 WHERE id = 1;
T2: COMMIT;

T1: SELECT balance ...;  -- 这条语句可能看到 900
T1: COMMIT;

READ COMMITTED 不允许读取未提交数据,但允许不可重复读和幻读:同一事务再次执行查询时,可能看到其他事务已经提交的新版本或新行。

还有一个容易忽略的行为:如果 UPDATE 找到的行在语句快照之后被另一个事务更新,PostgreSQL 通常会等待对方结束,然后基于更新后的版本重新检查 WHERE 条件。这也是下面这种原子扣库存写法比“先 SELECT 再 UPDATE”更安全的原因:

UPDATE products
SET stock = stock - 1
WHERE id = 42 AND stock > 0
RETURNING stock;

Repeatable Read:事务级快照

如果业务需要事务内多次读取保持一致,可以使用:

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

REPEATABLE READ 在事务第一次普通查询时建立快照,后续普通查询继续使用这个快照。因此 PostgreSQL 不会让事务中的普通读取看到后来提交的新版本,也不会出现幻读;这比 SQL 标准对 REPEATABLE READ 的最低要求更强。

但它仍然不等于完整的 Serializable。两个事务可能分别读到不同的事实,然后修改不同的行,最终组合出违反业务规则的结果,这类问题常被称为 write skew。需要保护跨行不变量时,应该考虑显式锁或 SERIALIZABLE,而不是仅仅依赖“我在 Repeatable Read 里”。

Serializable:允许并发,但要求结果可串行化

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 读取、判断、修改
COMMIT;

Serializable 并不是把所有事务排成队列。PostgreSQL 仍允许事务并发执行,但会通过 Serializable Snapshot Isolation 检测危险依赖;如果无法证明结果等价于某个串行顺序,就让部分事务失败:

ERROR: could not serialize access due to concurrent update
SQLSTATE: 40001

应用必须捕获 serialization_failure,回滚并从 BEGIN 重新执行整个事务。只重试最后一条 SQL 不够,因为事务使用的快照和之前的业务判断已经失效。重试应有次数上限、退避和幂等设计。

隔离等级和锁不是一回事

可以用下面的方式区分:

Isolation Level
→ 一个事务能看到哪些版本,如何处理并发依赖

Row Lock
→ 多个事务如何协调修改同一行

READ COMMITTED 下也可以使用 SELECT ... FOR UPDATE;在 SERIALIZABLE 下也可能因为锁顺序或长事务产生等待。提高隔离等级不能替代合理的锁设计,显式加锁也不能替代正确的事务边界。

锁:只在真正需要协调时等待

普通 SELECT 不会因为另一个事务更新同一行就自动等待。如果应用需要“读出当前值,再基于它做一组修改”,可以明确获取行锁:

BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

常用锁语义:

语句意图
FOR UPDATE当前事务准备修改这行
FOR NO KEY UPDATE修改行,但不改变被外键引用的 key
FOR SHARE读取期间不希望这行被修改
SKIP LOCKED跳过已被其他 worker 占用的任务
NOWAIT冲突时立即报错,不等待

任务队列可以使用 FOR UPDATE SKIP LOCKED

WITH picked AS (
  SELECT id FROM jobs
  WHERE status = 'pending'
  ORDER BY id
  FOR UPDATE SKIP LOCKED
  LIMIT 10
)
UPDATE jobs AS j
SET status = 'processing'
FROM picked
WHERE j.id = picked.id
RETURNING j.*;

它换取吞吐量,但返回的不是严格全局最早任务;生产系统还需要 worker 崩溃后的超时回收、重试和幂等。

6. Durability:WAL、COMMIT 与崩溃恢复

持久性的核心是 WAL:数据页真正写回数据文件之前,描述修改的 WAL 必须先写入持久日志。

SQL 修改
   ├─ shared buffers 中的脏页
   └─ WAL buffer → WAL 文件 → WAL flush
                         COMMIT 返回

Checkpoint:把部分脏页写回数据文件,缩短恢复时需要重放的 WAL 范围

默认同步提交下,提交成功通常意味着对应 WAL 已按持久性策略 flush。若关闭 synchronous_commit,响应可能早于 WAL 真正落盘,需要接受数据库崩溃时最近事务丢失的可能性。fsync 和底层存储设备缓存的可靠性也属于持久性边界。

Checkpoint 不是每次提交都发生的保存按钮,而是恢复锚点。数据库异常重启后,会从合适的 checkpoint 开始重放 WAL,把数据页恢复到一致状态。

WAL 也支撑流复制、WAL 归档和时间点恢复(PITR):基础备份保存某个时刻的数据,之后的 WAL 保存变化。复制槽长期没有消费时,旧 WAL 不能回收,可能迅速吃满磁盘。

7. Index、Planner 与 VACUUM:ACID 的运行时条件

ACID 定义的是事务语义,但数据库能否稳定提供这些语义,还依赖访问路径和后台维护。

Index 找到候选行,MVCC 再判断可见性

B-tree → key → TID → Heap Page → tuple visibility check

Index Scan 找到的不是“最终行”,而是可能需要进行可见性判断的候选位置。Index Only Scan 能否跳过 heap,取决于查询列是否都在索引中,以及 visibility map 是否表明对应 heap page 已全部可见。

CREATE INDEX idx_jobs_pending_created
ON jobs (status, created_at) INCLUDE (id)
WHERE status = 'pending';

索引不是越多越好。写入、更新和删除都可能维护相关索引;HOT(Heap-Only Tuple)可以在更新不涉及索引列且页面有空间时减少索引写入,但 heap 仍会产生新版本,仍需要 VACUUM。

Planner:SQL 写法和执行方式是两回事

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, created_at
FROM jobs
WHERE tenant_id = 10 AND status = 'pending'
ORDER BY created_at
LIMIT 10;

Planner 会基于统计信息、表大小、选择性和代价,在 Seq Scan、Index Scan、Bitmap Scan 以及多种 join 算法中选择方案。EXPLAIN (ANALYZE) 会真正执行语句,分析 UPDATEDELETE 时要特别小心。

如果估算行数和实际行数差异很大,先考虑统计信息、数据倾斜和列间相关性,再考虑索引。不要一遇到慢查询就关闭 enable_seqscan

VACUUM:MVCC 旧版本的维护成本

UPDATEDELETE 会留下旧 tuple。VACUUM 负责让不再被任何可能快照看到的空间重新可用,清理不需要的索引引用,更新 visibility map,并推进事务 ID 冻结以避免 wraparound。

长事务持有旧快照
新版本不断产生
VACUUM 不敢回收旧 tuple
表、索引和磁盘逐渐膨胀

普通 VACUUM 通常不把空间还给操作系统;VACUUM FULL 会重写表并持有强锁,应安排维护窗口。表膨胀不只是写入量大,也可能来自长事务、失控连接和低频批处理。

8. 生产排障:先看现象,再找机制

请求变慢,但 CPU 很高       → 查询计划、计算量
请求变慢,但大量 waiting    → 锁、IO、连接池
数据库连接打满             → 连接池、长事务、连接泄漏
磁盘持续增长               → 表膨胀、WAL、复制槽
重启恢复或副本延迟         → WAL、Checkpoint、IO
错误集中出现               → 约束、死锁、序列化失败

遇到“数据库变慢”,可以按下面的顺序定位:

1. 范围:单条 SQL、单租户、全部请求,还是只有副本?
2. 状态:谁 active,谁 waiting,谁 idle in transaction?
3. 阻塞:是否有长事务或锁环?
4. 计划:行数、IO、join 和排序是否符合预期?
5. 维护:autovacuum、dead tuple、统计信息、checkpoint 是否异常?
6. 资源:CPU、内存、磁盘 IO、空间和连接数是否到上限?
7. 变更:是否刚发布代码、改参数、加索引或改变流量?

9. 常见问题一:慢查询到底慢在哪里

当前谁在运行、谁在等待

SELECT pid, usename, datname, state,
       wait_event_type, wait_event,
       now() - query_start AS query_age,
       now() - xact_start AS xact_age,
       left(query, 160) AS query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;

query_age 很长不一定代表 SQL 正在执行;它可能大部分时间都在等待锁。xact_age 更值得关注:SQL 已结束但事务仍未提交,依然可能持有锁和旧快照。

用执行计划验证猜测

EXPLAIN (ANALYZE, BUFFERS, WAL)
SELECT ...;

按这个顺序读计划:

  1. actual timeactual rows
  2. 估算行数与实际行数是否严重偏差;
  3. Buffers 的 hit、read、dirtied、written;
  4. 是否出现意外的 Seq Scan、Nested Loop 或大量排序;
  5. 返回数据量是否让应用端读取成为瓶颈。

线上长期统计可以使用 pg_stat_statements,按总时间、平均时间、调用次数和读写量找到最值得优化的 SQL,而不是只追一条偶然慢的请求。

常见修复方向是:统计信息不准则 ANALYZE,过滤选择性差则重新设计查询或部分索引,分页昂贵则考虑 keyset pagination,写入变慢则检查索引数量、WAL、checkpoint 和锁等待。

10. 常见问题二:锁等待和死锁

锁等待本身可能正常;危险信号是 blocker 持锁很久,或大量请求形成排队。排查当前阻塞关系:

SELECT
  blocked.pid AS blocked_pid,
  blocked.query AS blocked_query,
  blocker.pid AS blocker_pid,
  blocker.query AS blocker_query,
  blocked.wait_event_type,
  blocked.wait_event,
  now() - blocker.xact_start AS blocker_transaction_age
FROM pg_stat_activity AS blocked
CROSS JOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS p(blocker_pid)
JOIN pg_stat_activity AS blocker
  ON blocker.pid = p.blocker_pid;

常见根因是事务里调用外部服务、等待用户输入、批量处理过多行,或不同代码路径以不同顺序获取资源。修复通常是缩短事务、避免持锁调用外部服务、统一加锁顺序,并让连接池归还连接前清理事务状态。

死锁是环形等待:

事务 A 持有 row 1,等待 row 2
事务 B 持有 row 2,等待 row 1

PostgreSQL 会取消其中一个事务。应用应该有限重试,但更重要的是稳定加锁,例如总是先锁较小的 account_id

11. 常见问题三:连接池打满

SELECT state, wait_event_type, count(*)
FROM pg_stat_activity
GROUP BY state, wait_event_type
ORDER BY count(*) DESC;

重点看:

active 很多         → 查询真的在执行,检查 CPU、IO、计划
idle in transaction → 开启事务后没有提交或回滚
大量 idle           → 连接池过大或连接没有及时归还

PostgreSQL 连接有进程、内存和上下文切换成本。应用应限制连接池大小,设置连接和事务超时,确保异常路径也会回滚。max_connections 不是越大越好;连接池、数据库资源和查询并发必须一起设计。

12. 常见问题四:VACUUM 跟不上和表膨胀

SELECT relname, n_live_tup, n_dead_tup,
       last_autovacuum, last_autoanalyze,
       autovacuum_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;

再检查长事务:

SELECT pid, usename, state,
       now() - xact_start AS xact_age,
       backend_xmin,
       left(query, 160) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;

判断路径通常是:

dead tuple 增长
  ├─ 有长事务 → 先处理事务生命周期
  ├─ autovacuum 很少运行 → 检查参数、资源和权限
  ├─ 更新非常频繁 → 调整表设计、fillfactor、索引与批处理
  └─ 已严重膨胀 → 评估 VACUUM FULL、重建或在线工具

不要一看到磁盘占用高就执行 VACUUM FULL。它会重写表并持有强锁;若接近 XID wraparound 风险,事务冻结的优先级则高于普通空间优化。

13. 常见问题五:WAL、复制延迟和磁盘打满

WAL 增长本身不一定是问题,需要区分“写入多”和“旧 WAL 无法回收”。检查复制状态:

SELECT application_name, client_addr, state,
       sent_lsn, write_lsn, flush_lsn, replay_lsn,
       write_lag, flush_lag, replay_lag
FROM pg_stat_replication;

检查复制槽:

SELECT slot_name, slot_type, active,
       restart_lsn, wal_status
FROM pg_replication_slots;

常见原因包括副本 IO 或查询太慢、复制槽消费者失效、长事务、大批量写入和 checkpoint 压力。复制延迟还要区分发送、写入、flush 和 replay:主库已发送不代表副本已经落盘,更不代表查询已经应用。

14. 常见问题六:约束错误和“偶发”事务失败

应用日志里的 PostgreSQL 错误码应该分类处理:

23505 unique_violation          → 幂等冲突或并发重复写入
23503 foreign_key_violation     → 引用不存在或删除顺序错误
23514 check_violation           → 数据违反业务约束
40P01 deadlock_detected         → 统一加锁顺序,有限重试
40001 serialization_failure     → 缩短事务,有限重试
25P02 in_failed_sql_transaction → 先 rollback 当前事务

不能把所有错误都简单重试:唯一键冲突可能意味着重复提交,外键错误通常不是瞬时故障,事务 aborted 状态也必须先回滚。重试要有上限、退避和幂等键,否则会把数据库问题放大成请求风暴。

15. 把 ACID 放回一次订单更新

BEGIN
  ├─ INSERT orders
  ├─ UPDATE products ... WHERE stock > 0
  ├─ INSERT payments(status = 'pending')
  ├─ 约束:订单号唯一、库存不能为负、引用必须存在
  ├─ MVCC:普通读不会看到半个事务
  ├─ 锁:竞争同一库存行时协调并发更新
  ├─ WAL:崩溃恢复时重放已记录的修改
  └─ COMMIT:事务作为整体对后续快照可见
Atomicity   → 订单、库存、支付意图一起成功或一起回滚
Consistency → 约束和原子 UPDATE 保护业务不变量
Isolation   → MVCC 提供快照,锁协调竞争资源
Durability  → WAL 和提交策略保护已提交结果

支付网关和消息队列不在这个 ACID 边界内,所以需要 outbox、幂等键、状态机和补偿。数据库的强一致性不是消除了所有分布式系统问题,而是把最重要的一段事实维护在一个可靠的事务边界内。

总结

特性核心问题PostgreSQL 机制常见场景例子
Atomicity 原子性事务是否完整发生?Transaction、回滚、Savepoint、WAL多表写入必须成组成功创建订单、扣库存、写支付意图
Consistency 一致性提交后是否满足规则?Primary Key、Foreign Key、UNIQUE、CHECK、原子 SQL保护业务不变量订单号唯一、库存不能为负
Isolation 隔离性并发事务看到什么、如何冲突?MVCC、快照、行锁、Serializable并发更新、报表、任务领取FOR UPDATE SKIP LOCKED 领取任务
Durability 持久性提交后崩溃是否保留?WAL、flush、Checkpoint、fsync宕机恢复、复制、PITRCOMMIT 后重启仍保留订单
运行时条件ACID 如何长期稳定提供?Index、Planner、VACUUM、连接池慢查询、膨胀、连接耗尽EXPLAIN 定位慢 SQL,VACUUM 回收旧版本

隔离等级还可以这样快速选择:

隔离等级适合场景例子
READ COMMITTED普通 OLTP、原子 SQL、任务队列更新库存:UPDATE ... WHERE stock > 0
REPEATABLE READ事务内需要稳定快照的读取导出一份前后一致的订单报表
SERIALIZABLE跨行不变量必须等价于串行执行至少一名医生值班、并发预约不能超容量

READ COMMITTED 适合大多数普通业务;REPEATABLE READ 解决事务内快照变化,但仍可能出现跨行 write skew;SERIALIZABLE 能进一步检查读写依赖,但应用必须处理 40001 并重试整个事务。隔离级别不是越高越好,事务也不应因此变成长事务。

遇到线上问题时,先问它属于事务边界、约束、快照与锁、WAL 与 IO,还是访问计划和后台维护。然后用 pg_stat_activitypg_locksEXPLAIN (ANALYZE, BUFFERS)pg_stat_user_tables、复制视图和错误码去验证,而不是凭经验直接加索引、调大连接数或重启数据库。

References