36、PostgreSQL事务与隔离级别:BEGIN/COMMIT/ROLLBACK、READ COMMITTED、REPEATABLE READ、SERIALIZABLE
事务,说白了就是一组数据库操作的「打包」。要么全部成功,要么全部失败。我刚开始用PostgreSQL时,觉得事务就是个begin加commit,没什么好学的。直到有一次线上环境出了数据不一致的问题……嗯,从那以后我老老实实把隔离级别研究了一遍。
今天我们就来聊聊PostgreSQL的事务控制,以及那几个让人头疼的隔离级别。我会结合自己踩过的坑,帮你理清楚它们到底有什么区别。
事务的基本操作:BEGIN / COMMIT / ROLLBACK
PostgreSQL里,事务的边界非常清晰。你写一条SQL,默认就是自动提交的。但如果你想要多条SQL作为一个整体,就得手动控制。
-- 开始一个事务
BEGIN;
-- 执行一些操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 如果一切正常,提交
COMMIT;
-- 如果出错了,回滚
-- ROLLBACK;
我个人习惯在写批量更新时,总是先BEGIN,然后逐条执行,最后COMMIT。万一中间某条语句报错,我可以立刻ROLLBACK,不会留下「半残」的数据。
隔离级别:为什么需要它?
多个事务同时操作同一份数据时,会出现三种问题:
- 脏读:读到另一个事务未提交的数据。万一它回滚了,你读到的就是「假数据」。
- 不可重复读:同一个事务内,两次读取同一行数据,结果不一样。因为别的事务提交了更新。
- 幻读:同一个事务内,两次查询同一条件的结果集不一样。因为别的事务插入了新行。
PostgreSQL提供了四种隔离级别,用来平衡「数据一致性」和「并发性能」。你想想看,如果完全不加控制,数据乱套;如果完全串行化,性能又太差。
核心观点:PostgreSQL的默认隔离级别是READ COMMITTED。绝大多数场景下够用了。但如果你做金融、库存类业务,建议用REPEATABLE READ甚至SERIALIZABLE。
READ COMMITTED:默认级别,够用就好
这是PostgreSQL的默认级别。它的规则很简单:只能读到已提交的数据。脏读被杜绝了。
但不可重复读是可能发生的。什么意思呢?
-- 事务A
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 返回 100
-- 此时事务B更新了id=1的余额为200并提交
SELECT balance FROM accounts WHERE id = 1; -- 返回 200(不可重复读)
COMMIT;
我在项目中遇到过这种情况:一个统计报表的存储过程,先查询了总金额,然后又查询了明细,结果总金额和明细对不上。排查了半天,才发现是另一个事务在中间插了一脚。后来我把报表查询改成了REPEATABLE READ,问题就解决了。
REPEATABLE READ:读一致性更强
这个级别保证:同一个事务内,多次读取同一行数据,结果始终一致。它通过快照(snapshot)机制实现——事务开始时,数据库给你拍一张「数据快照」,之后所有读操作都基于这个快照。
-- 事务A
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1; -- 返回 100
-- 此时事务B更新了id=1的余额为200并提交
SELECT balance FROM accounts WHERE id = 1; -- 仍然返回 100(基于快照)
COMMIT;
但注意,REPEATABLE READ并不能完全避免幻读。在PostgreSQL中,它实际上通过快照避免了幻读(因为查询结果集也是基于快照的)。不过如果你执行SELECT ... FOR UPDATE,还是可能看到新插入的行。
SERIALIZABLE:最严格,但代价也最大
SERIALIZABLE级别模拟了「事务串行执行」的效果。它使用了一种叫「可串行化快照隔离」(SSI)的技术,能检测出所有可能的并发冲突。
说白了,就是让数据库帮你判断:如果这些事务按某种顺序串行执行,结果会不会一样?如果不一样,就拒绝其中一个事务。
-- 事务A
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 10;
-- 事务B(同时)
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 10;
-- 其中一个事务提交成功,另一个会收到错误:
-- ERROR: could not serialize access due to read/write dependencies among transactions
你想想看,这种级别的并发性能肯定是最差的。我一般只在两种场景下用:
- 金融交易,一分钱都不能错
- 库存扣减,超卖会出大事
我的建议:能用REPEATABLE READ就别用SERIALIZABLE。后者会大幅降低并发吞吐量。如果业务逻辑允许,尽量用乐观锁(比如版本号字段)来替代。
隔离级别对比一览
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能(PG中实际不会) | 可能 | 可能 | 最高 |
| READ COMMITTED | 不可能 | 可能 | 可能 | 高 |
| REPEATABLE READ | 不可能 | 不可能 | 不可能(PG中) | 中 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 低 |
注意:PostgreSQL实际上不支持READ UNCOMMITTED,你设置了这个级别,它也会当作READ COMMITTED来处理。这是PG的一个特点,也算是一种「安全设计」吧。
知识体系图:事务与隔离级别核心逻辑
如何设置隔离级别?
有两种方式:
- 事务级别设置:在BEGIN时指定
- 会话级别设置:修改当前连接的默认隔离级别
-- 方式一:事务级别
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- 你的SQL
COMMIT;
-- 方式二:会话级别
SET default_transaction_isolation = 'serializable';
-- 之后所有事务都默认使用SERIALIZABLE
-- 查看当前隔离级别
SHOW transaction_isolation;
避坑指南
最后分享几个我实际工作中遇到的坑:
- 不要依赖默认值:虽然默认是READ COMMITTED,但如果你在代码里显式写出来,别人维护时会更清楚。
- SERIALIZABLE的重试逻辑:如果你用了这个级别,一定要在应用层捕获序列化错误,然后重试。我见过有人没做重试,结果线上频繁报错。
- 长事务要小心:隔离级别越高,事务持续时间越长,对数据库的压力越大。我建议所有事务都尽量短,别在事务里做网络请求或用户交互。
好了,关于PostgreSQL的事务与隔离级别,就聊到这里。记住一句话:隔离级别越高,数据越安全,但性能越差。选哪个,取决于你的业务场景。