这是《PostgreSQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 事务是数据库保证一致性的基本单位。PostgreSQL 使用 MVCC 实现读写互不阻塞,并通过锁和谓词机制处理写冲突。
14.1 基本用法
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
回滚:
ROLLBACK;
保存点:
BEGIN;
INSERT INTO orders(order_no) VALUES ('O001');
SAVEPOINT sp1;
UPDATE orders SET status = 'PAID' WHERE order_no = 'O001';
ROLLBACK TO SAVEPOINT sp1;
COMMIT;
14.2 ACID
| 特性 | PostgreSQL 实现 |
|---|---|
| Atomicity | 事务提交或回滚 |
| Consistency | 约束、触发器、应用规则 |
| Isolation | MVCC + 锁 |
| Durability | WAL 与同步提交策略 |
14.3 隔离级别
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
PostgreSQL 默认 READ COMMITTED。
| 级别 | 防止 |
|---|---|
| Read Committed | 脏读 |
| Repeatable Read | 脏读、不可重复读 |
| Serializable | 脏读、不可重复读、幻读、写倾斜 |
PostgreSQL 的实现细节与 SQL 标准描述并不一一对应,应通过行为验证。
14.4 异常现象
脏读
读到未提交数据。PostgreSQL 隔离级别下均不允许。
不可重复读
同一事务两次读取同一行结果不同。
BEGIN;
SELECT amount FROM orders WHERE id = 1;
-- 其他事务修改并提交
SELECT amount FROM orders WHERE id = 1;
COMMIT;
幻读
同一查询条件两次返回不同行集。
写倾斜
两个事务分别读取相交条件,再分别修改不同行,导致业务约束被破坏。SERIALIZABLE 通过 SSI 防止。
14.5 锁冲突
SELECT FOR UPDATE;
SELECT FOR SHARE;
SELECT FOR NO KEY UPDATE;
SELECT FOR KEY SHARE;
常见冲突:
| 操作 | 冲突 |
|---|---|
| UPDATE 同一行 | 行锁等待 |
| 外键检查 | 引用行锁 |
| 唯一约束 | 唯一索引等待 |
| DDL | AccessExclusiveLock |
14.6 SERIALIZABLE
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*) FROM seats WHERE room_id = 1;
INSERT INTO seats(room_id, user_id) VALUES (1, 1001);
COMMIT;
可能返回:
ERROR: could not serialize access due to read/write dependencies among transactions
应用必须重试。串行化隔离会增加谓词锁和取消概率,适合强一致性关键业务。
14.7 事务边界
推荐:
- 事务尽量短;
- 不在事务中调用慢外部服务;
- 批量任务分批提交;
- 重试逻辑处理序列化失败和死锁;
- 幂等键防止重试重复;
- 避免交互式客户端长期打开事务。
14.8 查看事务
SELECT
pid,
state,
xact_start,
now() - xact_start AS duration,
query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY xact_start;
长事务会阻止 VACUUM 回收旧版本,是 PostgreSQL 高发问题。
本章小结
PostgreSQL 通过 MVCC 和锁提供多级隔离。默认 Read Committed 适合多数业务;需要快照一致或串行化时,要接受相应代价并设计重试。事务边界直接影响并发和清理能力。
思考题
- 默认隔离级别下能否出现不可重复读?
- SERIALIZABLE 为什么要重试?
- 长事务有哪些危害?
SELECT FOR UPDATE适合什么场景?- 事务中为什么不能调用慢外部服务?