PostgreSQL事务隔离级别:搞懂并发一致性的核心逻辑
PostgreSQL事务隔离级别:搞懂并发一致性的核心逻辑
在数据库的日常使用中,我们总在追求两个看似矛盾的目标:高并发和数据一致性。多用户同时操作数据库时,既要让大家都能高效干活,又要避免数据乱掉,这背后的核心支撑就是事务隔离级别。PostgreSQL作为开源关系型数据库的标杆,对事务隔离的实现既遵循SQL标准,又有自己的独特优化。
先搞清楚:为什么需要事务隔离?
事务的四大特性ACID里,隔离性(Isolation) 是最复杂的一个。如果没有隔离机制,多个事务并发执行时,会出现各种数据异常问题,PG里主要需要解决这三类:脏读、不可重复读、幻读。这三个问题是理解隔离级别的基础,先通过实际场景搞明白。
脏读:读了“临时”的数据,白忙活一场
脏读就是一个事务读取到了另一个还没提交的事务修改的数据。这些数据就像“草稿”,随时可能因为原事务回滚而消失,基于这种数据做的计算,全是无效的。
举个银行账户的例子,表结构很简单:
| AccountNumber | AccountHolder | Balance |
|---|---|---|
| 1001 | Alice | $100 |
| 1002 | Bob | $150 |
| 并发过程: |
事务1开始,读Alice的余额是$100;
事务2开始,把Alice的余额更改为$200,不提交;
事务1再次读Alice的余额,拿到了$200,基于这个数扣了$50;
哪怕后续事务2提交了,事务1的计算也是基于“临时数据”来的,逻辑上已经出问题了。
简单说,脏读就是读了“不算数”的数据,这是最基础的并发异常,也是最容易解决的。
不可重复读:同一个事务里,两次读同一数据,结果不一样
这种异常比脏读更隐蔽,指的是一个事务执行过程中,两次读取同一行数据,结果却不同——因为中间有另一个事务修改并提交了这条数据。
用学生信息表举例子:
| StudentID | Name | Age |
|---|---|---|
| 1 | Alice | 20 |
| 2 | Bob | 22 |
| 并发过程: |
事务1开始,读Alice的年龄是20;
事务2开始,把Alice的年龄改成21,提交;
事务1再次读Alice的年龄,变成了21。
同一个事务内,数据居然变了,这会导致业务逻辑混乱,比如报表统计、数据核对时,前后结果不一致,根本没法用。
幻读:同一个查询条件,两次查出来的行数不一样
幻读是更进阶的异常,指一个事务基于相同的条件两次查询,返回的结果集行数不同——因为中间有另一个事务插入/删除了满足该条件的行,新行像“幻影”一样突然出现,消失的行则像“幻影”一样不见了。
看产品价格表的例子:
| ProductID | ProductName | Price |
|---|---|---|
| 1 | Laptop | $800 |
| 2 | Smartphone | $400 |
| 并发过程: |
事务1开始,查价格小于$500的产品,只拿到1行(智能手机);
事务2开始,插入一款$350的平板,提交;
事务1再次执行相同的查询,拿到了2行,平白多了一条记录。
幻读和不可重复读的区别在于:不可重复读是单条数据的内容变了,幻读是数据的行数变了,前者针对行,后者针对结果集。
SQL标准vsPG实现:四种级别,三种实际效果
SQL标准定义了四种事务隔离级别,从低到高依次是:读未提交、读已提交、可重复读、可序列化。级别越高,隔离性越强,能解决的并发异常越多,但并发性能也会越低,这是一个必然的权衡。
但PostgreSQL对这四个级别的实现有自己的优化:把读未提交映射成了读已提交,也就是说PG实际只有三种不同的隔离级别。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 序列化异常 |
|---|---|---|---|---|
| 读未提交 | 允许,但不在 PG 中 | 可能 | 可能 | 可能 |
| 读已提交 | 不可能 | 可能 | 可能 | 可能 |
| 可重复读 | 不可能 | 不可能 | 允许,但不在 PG 中 | 可能 |
| 可序列化 | 不可能 | 不可能 | 不可能 | 不可能 |
这里有两个关键细节要注意:
PG里的读未提交和读已提交行为完全一样,所以实际开发中不用考虑读未提交;
SQL标准里,可重复读并不能解决幻读,但PG对可重复读做了优化,实际使用中基本不会出现幻读,这是PG的亮点。
接下来逐个讲PG实际可用的三个隔离级别,重点说特性、能解决的问题、适用场景,让你能直接落地。
PG实际隔离级别详解:从基础到高级,按需选择
读已提交(Read Committed):PG默认级别,兼顾并发和基础一致性
这是PostgreSQL的默认隔离级别,也是绝大多数业务的首选,核心规则就一句话:一个事务只能读取其他事务已经提交的数据。
核心逻辑
事务中每执行一条SQL语句,都会获取一个新的数据库“快照”,这个快照里只包含当前语句执行前,所有已经提交的事务修改的数据。简单说,就是只看定稿,不看草稿。
能解决&不能解决
✅ 彻底解决脏读:因为未提交的数据不会被任何其他事务读取;
❌ 无法解决不可重复读和幻读:同一个事务内,不同语句的快照不同,中间有其他事务提交修改/插入,就会读到不同结果。
适用场景
绝大多数日常业务都适合,比如电商的商品展示、用户信息查询、普通的增删改查。这个级别能保证基础的数据一致性,同时并发性能最高,因为没有额外的锁和快照开销,是平衡并发和一致性的最佳选择。
可重复读(Repeatable Read):事务内数据一致,解决不可重复读
这是PG中最实用的中高级隔离级别,在实际开发中使用频率很高,核心规则:一个事务在第一次读取数据时,生成一个全局快照,后续所有读取都基于这个快照。
核心逻辑
和读已提交的“每次语句都新快照”不同,可重复读是事务级快照:事务启动后,第一次读数据时创建一个静态的数据库快照,后续无论其他事务怎么修改、怎么提交,这个事务都只能看到快照里的数据,相当于给事务开了一个“数据平行空间”。
能解决&不能解决
✅ 解决脏读和不可重复读:快照一旦生成,事务内多次读同一数据,结果完全一致;
❌ SQL标准里允许幻读,但PG实际实现中基本不会出现幻读:这是PG的优化,远超SQL标准,也是为什么这个级别是PG的“明星级别”。
适用场景
需要事务内数据一致性的场景,比如:
财务对账、报表生成:同一个报表事务,前后查询结果必须一致;
订单结算:从查询商品库存到扣减库存,整个事务内库存数据不能变;
数据统计:一次统计过程中,基础数据不能被修改。
这个级别的并发性能略低于读已提交,但牺牲的性能非常少,换来的一致性提升却很明显,是性价比最高的隔离级别。
可序列化(Serializable):最高级别,杜绝所有并发异常
这是PostgreSQL的最高隔离级别,也是最严格的级别,核心规则:让所有并发执行的事务,表现得如同串行执行一样,简单说,就是“看起来同一时间只有一个事务在跑”。
核心逻辑
通过严格的锁机制和谓词锁定实现:
事务访问的数据会被加锁,其他事务无法修改;
谓词锁定:不仅锁已存在的行,还会锁“满足查询条件的潜在行”,比如查价格<500的产品,会锁定所有可能符合这个条件的行,阻止其他事务插入新的符合条件的行,从根本上解决幻读;
会检测并发冲突,若发现两个事务的执行顺序会导致数据不一致,会直接抛出异常,让其中一个事务回滚。
能解决&不能解决
✅ 解决所有并发异常:脏读、不可重复读、幻读、序列化异常,数据一致性达到最高;
❌ 并发性能最低:严格的锁会导致事务之间互相阻塞,而且冲突时会抛出异常,需要业务层写重试逻辑。
适用场景
仅适用于对数据一致性要求极高,且能接受并发性能下降的核心业务,比如:
银行转账、证券交易;
核心账务系统、资金结算;
金融类核心交易,不允许任何数据偏差。
注意:不要盲目使用可序列化,普通业务用这个级别会导致数据库并发能力大幅下降,反而影响系统性能。
实际开发中,怎么选隔离级别?
讲了这么多,核心还是落地——到底该怎么选?其实就一个原则:在满足业务一致性要求的前提下,尽量选择级别低的隔离级别,因为隔离级别越低,并发性能越好。
给大家一个简单的选择流程,直接套用就行:
无特殊需求:直接用默认的读已提交,搞定绝大多数场景;
需要事务内数据一致:升级为可重复读,比如报表、对账、订单处理,这是PG最推荐的进阶选择;
核心金融/账务场景:只有这种情况才用可序列化,同时做好业务层的重试逻辑,处理数据库抛出的序列化异常;
永远不要用读未提交:因为PG里它和读已提交一样,选了也没意义。
最后总结:隔离级别的本质是权衡
PostgreSQL的事务隔离级别,本质上就是数据一致性和数据库并发性能之间的权衡,没有最好的级别,只有最适合的级别。
再用一句话总结三个实际可用的级别:
读已提交:默认选择,平衡并发和基础一致;
可重复读:PG亮点,性价比最高,解决事务内数据一致;
可序列化:终极选择,牺牲并发换绝对一致,仅用在核心金融场景。
理解了隔离级别,就能在开发中避开大部分并发数据问题,比如不用再疑惑“为什么同一个事务里两次查数据结果不一样”,也不用再为了追求一致性而盲目加锁,让数据库的并发能力和数据一致性达到最优的平衡。
