事务、MVCC 与并发
理解 PostgreSQL 的快照、隔离级别、行锁和整事务重试
最小事务
BEGIN;
SELECT balance_cents
FROM accounts
WHERE id = $1
FOR UPDATE;
UPDATE accounts
SET balance_cents = balance_cents - $2
WHERE id = $1
AND balance_cents >= $2;
COMMIT;事务把多条语句组成一个原子单元。FOR UPDATE 锁住选中的行,直到提交或回滚;业务仍应检查 UPDATE 的影响行数。
MVCC 的直觉
PostgreSQL 通过多版本并发控制让读取通常不阻塞写入、写入通常不阻塞普通读取。更新会创建新行版本;旧版本在对所有活跃快照都不可见后,由 autovacuum 回收空间。
长事务会让旧版本迟迟不能回收,并增加表膨胀、WAL 保留和复制延迟风险。不要把事务跨越用户思考、网络重试或外部 API 调用。
隔离级别
| 级别 | PostgreSQL 行为 | 应用责任 |
|---|---|---|
READ COMMITTED | 默认;每条语句获取新快照 | 不假设同一事务两次查询结果不变 |
REPEATABLE READ | 事务快照稳定;可能序列化失败 | 捕获 40001 并重试整个事务 |
SERIALIZABLE | 只允许可证明等价于串行执行的结果 | 必须设计整事务重试与退避 |
PostgreSQL 的 READ UNCOMMITTED 实际按 READ COMMITTED 处理。
与 SQL 标准最低要求相比,PostgreSQL 的 REPEATABLE READ 还会阻止 phantom read,但仍可能出现需要整事务重试的序列化失败。另一个重要边界是 sequence:nextval() 等变更不会因事务回滚而撤销,因此序号存在空洞是正常现象。
重试的正确边界
序列化失败或死锁后,当前事务已经不能继续。应用应回滚并重放整个事务,不是只重跑最后一条 SQL。
begin
run all reads and writes
commit
on SQLSTATE 40001 or 40P01
rollback
retry whole unit with bounded exponential backoff确保重试边界内的外部副作用是幂等的,或把它们放到提交后的 outbox 消费流程。
死锁与锁等待
降低死锁概率:所有事务按相同顺序锁定资源;保持事务短小;为定位条件建立合适索引;为请求设置合理的 lock_timeout 和 statement_timeout。
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '10s';SET LOCAL 只在当前事务生效。
Idle in transaction
客户端开启事务后不提交,会继续持有快照甚至锁。监控 pg_stat_activity.state = 'idle in transaction',并考虑设置 idle_in_transaction_session_timeout。
隔离级别和允许现象以 PostgreSQL 18 事务隔离文档为准。
继续深入:MVCC 与快照可见性解释旧行版本和长事务;锁等待与死锁提供阻塞链诊断与安全处理流程。
Last updated on