事务与 MVCC
事务保证「一组操作要么全成、要么全败」。PostgreSQL 用 MVCC(多版本并发控制) 实现高并发——读不阻塞写、写不阻塞读。理解 MVCC 才能搞懂「为什么表会膨胀」「长事务有什么危害」。
1. ACID 与基本用法
BEGIN;
UPDATE core.products SET stock = stock - 1 WHERE id = 1;
UPDATE core.orders SET total_amount = total_amount + 7999 WHERE id = 10;
COMMIT; -- 全部成功才提交
-- 出错时 ROLLBACK; 回滚SAVEPOINT 可以设置「存档点」,局部回滚而不影响整个事务:
BEGIN;
INSERT INTO core.users(username,email) VALUES ('x','x@e.com');
SAVEPOINT sp1;
INSERT INTO core.users(username,email) VALUES ('x','x@e.com'); -- 违反 UNIQUE 会失败
ROLLBACK TO sp1; -- 只回滚到 sp1,前面的插入保留
COMMIT;2. 隔离级别
PG 支持 Read Uncommitted(实际等同 Read Committed)、Read Committed(默认)、Repeatable Read、Serializable。
- Read Committed:每条语句看到的是它开始时的快照;同一事务内不同语句可能看到不同数据。
- Repeatable Read:整个事务看同一快照,避免不可重复读;但可能出现序列化异常(PG 会报错让你重试)。
- Serializable:最高隔离,自动检测冲突(冲突时抛
serialization_failure,应用重试)。
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- ...
COMMIT;💡PG 默认是 RR,不是 RC
很多数据库默认 Read Committed,但 PostgreSQL 的默认是 Read Committed——注意别记混。若需要「可重复读」语义请显式声明。
3. MVCC 原理:多版本
PG 更新一行时不原地覆盖:插入新版本、旧版本保留(直到无人需要)。每行有 xmin/xmax(创建/失效的事务号)。读事务只看到「对自己快照可见的版本」——所以:
- 读不阻塞写(读的是旧版本),写不阻塞读(读不碰新版本)。
- 但这带来代价:旧版本会累积,表会膨胀(bloat)。
4. VACUUM 与膨胀
死元组(旧版本、已删除行)需要清理,否则表越来越大、查询越来越慢:
VACUUM:回收空间供复用(不收缩文件大小),autovacuum 会自动跑。VACUUM FULL:真正收缩文件(但会锁表、重写整表,慎用于大表)。autovacuum:默认开启的后台进程,按阈值自动清理。
SELECT relname, n_dead_tup FROM pg_stat_user_tables ORDER BY n_dead_tup DESC;
VACUUM (ANALYZE, VERBOSE) core.orders;5. 长事务的危害
一个长时间未提交的事务会钉住它开始时的快照,导致它之后产生的所有旧版本都无法被 VACUUM 回收——表持续膨胀,甚至撑爆磁盘。
⚠️避免长事务
- 不要在事务里做耗时的人肉操作或外部调用(如等接口、等人工确认)。
- 用
SELECT * FROM pg_stat_activity WHERE state <> 'idle' ORDER BY xact_start排查长事务。 - 应用端设置
statement_timeout/idle_in_transaction_session_timeout兜底。
🎯动手
开两个 psql 会话:会话 A 执行 BEGIN; UPDATE core.products SET price=1 WHERE id=1;(不提交)。会话 B 查询该行,确认 B 看到的是旧值(MVCC)。然后在 A 里 COMMIT;,再回到 B(新事务)确认看到新值。体会「读不阻塞写、写不阻塞读」。