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中,如果事务内发生了错误(比如违反唯一约束),事务并不会自动回滚。你需要手动执行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,问题就解决了。

小技巧:如果你只是做普通的CRUD,READ COMMITTED完全够用。不要为了「安全」盲目升级隔离级别,性能代价不小。

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,还是可能看到新插入的行。

我曾经踩过的坑:在REPEATABLE READ级别下,如果两个事务同时修改同一行数据,后提交的那个会收到「无法序列化访问」的错误。我第一次遇到时一脸懵,后来才知道这是PostgreSQL的防冲突机制。你需要捕获这个错误,然后重试事务。

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的一个特点,也算是一种「安全设计」吧。

知识体系图:事务与隔离级别核心逻辑

PostgreSQL 事务与隔离级别核心逻辑 事务控制 BEGIN / COMMIT / ROLLBACK 隔离级别 READ COMMITTED(默认) REPEATABLE READ SERIALIZABLE 并发问题 脏读 不可重复读 幻读 选择建议 日常CRUD → READ COMMITTED 金融/库存 → REPEATABLE READ 或 SERIALIZABLE

如何设置隔离级别?

有两种方式:

  1. 事务级别设置:在BEGIN时指定
  2. 会话级别设置:修改当前连接的默认隔离级别
-- 方式一:事务级别
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- 你的SQL
COMMIT;

-- 方式二:会话级别
SET default_transaction_isolation = 'serializable';
-- 之后所有事务都默认使用SERIALIZABLE

-- 查看当前隔离级别
SHOW transaction_isolation;
个人习惯:我一般不在会话级别改隔离级别,而是在每个事务的BEGIN时显式指定。这样代码可读性更好,别人一看就知道这个事务用了什么隔离级别。

避坑指南

最后分享几个我实际工作中遇到的坑:

  • 不要依赖默认值:虽然默认是READ COMMITTED,但如果你在代码里显式写出来,别人维护时会更清楚。
  • SERIALIZABLE的重试逻辑:如果你用了这个级别,一定要在应用层捕获序列化错误,然后重试。我见过有人没做重试,结果线上频繁报错。
  • 长事务要小心:隔离级别越高,事务持续时间越长,对数据库的压力越大。我建议所有事务都尽量短,别在事务里做网络请求或用户交互。

好了,关于PostgreSQL的事务与隔离级别,就聊到这里。记住一句话:隔离级别越高,数据越安全,但性能越差。选哪个,取决于你的业务场景。


公众号:蓝海资料掘金营,微信 deep3321