从 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 使用统计信息

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) 会真正执行语句,分析 UPDATE、DELETE 时要特别小心。
如果估算行数和实际行数差异很大,先考虑统计信息、数据倾斜和列间相关性,再考虑索引。不要一遇到慢查询就关闭 enable_seqscan。
VACUUM:MVCC 旧版本的维护成本
UPDATE 和 DELETE 会留下旧 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 ...;
按这个顺序读计划:
actual time和actual rows;- 估算行数与实际行数是否严重偏差;
Buffers的 hit、read、dirtied、written;- 是否出现意外的 Seq Scan、Nested Loop 或大量排序;
- 返回数据量是否让应用端读取成为瓶颈。
线上长期统计可以使用 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 | 宕机恢复、复制、PITR | COMMIT 后重启仍保留订单 |
| 运行时条件 | 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_activity、pg_locks、EXPLAIN (ANALYZE, BUFFERS)、pg_stat_user_tables、复制视图和错误码去验证,而不是凭经验直接加索引、调大连接数或重启数据库。