首页 / PostgreSQL 入门教程 / 隔离级别与锁

PostgreSQL 入门教程

隔离级别与锁

本教程共 50 篇 · 第 47 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

PostgreSQLPostgreSQL 入门教程事务隔离级别并发控制脏读

47. 隔离级别与锁

本节目标:学完你能说清四种隔离级别分别防住哪些并发问题,并会用 LOCK 主动加锁保护关键数据。

数据库常常被很多人同时访问。比如你正在给订单改状态,另一个同事刚好在统计订单总额。如果两个操作互相踩脚,就可能读出错误的数据,甚至算出错的报表。事务的「隔离性(Isolation)」就是用来规定:并发运行时,一个事务能看到另一个事务的哪些中间结果。

为什么需要隔离级别

隔离级别(Isolation Level)是一档档「宽松程度」的开关。越宽松,并发性能越好,但越容易看到别人的「半成品」;越严格,数据越稳,但性能开销越大。它不是越严越好——太严会让大家排队等锁,吞吐反而掉下来。

PostgreSQL 内部用「多版本并发控制(MVCC,Multi-Version Concurrency Control)」来实现隔离。简单说,每行数据其实都带着版本号(事务 ID)。当你去读的时候,PG 只让你看到「在你这个事务开始之前就已经提交」的版本。正是靠这套机制,PG 默认就杜绝了脏读,连最松的 READ UNCOMMITTED 都不会脏读。

Note

其它数据库的隔离行为可能不同。本文只讲 PostgreSQL 的规则,别把别的库的直觉直接搬过来,尤其别以为 PG 会脏读。

四种隔离级别一览

隔离级别脏读不可重复读幻读
READ UNCOMMITTED不会*可能可能
READ COMMITTED(默认)不会可能可能
REPEATABLE READ不会不会不会**
SERIALIZABLE不会不会不会

* 是因为 PostgreSQL 的 READ UNCOMMITTED 实际上会按 READ COMMITTED 来执行,真正实现层面它根本不会脏读。

** 是因为 REPEATABLE READ 在 PG 里通过快照机制也挡住了幻读,这点和 SQL 标准略有出入。SQL 标准里 REPEATABLE READ 是允许幻读的,PG 把它也挡了,算是个「加强版」。

三种并发异常是什么

说人话就是这三种「串味」:

  1. 脏读(Dirty Read):读到了别人还没提交的数据。对方一回滚,你读到的就是垃圾。PG 因为有 MVCC,天然没有这个问题。
  2. 不可重复读(Non-repeatable Read):同一事务里两次读同一行,结果不一样(因为别的事务把那行改了并提交)。常见于「先查后改」逻辑。
  3. 幻读(Phantom Read):同一事务里两次按同样条件查,第二次多出了几行(因为别的事务插入了符合条件的新行)。比如「查今天所有订单」第一次 10 条,别人插了 1 条后变 11 条。
Tip

记不住就抓关键词:脏读是「读了没提交的」,不可重复读是「同一条变了」,幻读是「多了一批发」。

READ COMMITTED:默认级别的行为细节

READ COMMITTED 是 PG 的出厂默认。它的规则是「每条语句只认自己开始时已提交的数据」。注意是语句级快照:同一个事务里,第一条 SELECT 和第二条 SELECT 之间,如果别人提交了新数据,你第二条 SELECT 会看到。

-- 会话 A 先改但不提交
postgres=# BEGIN;
postgres=# UPDATE orders SET status = 'paid' WHERE id = 1;

-- 会话 B 此时查,还是旧值(A 没提交)
postgres=# SELECT status FROM orders WHERE id = 1;
-- 返回改动前的值

-- 会话 A 提交
postgres=# COMMIT;

-- 会话 B 再查,这次看到 paid
postgres=# SELECT status FROM orders WHERE id = 1;

这也是为什么在 READ COMMITTED 下,两次相同的 SELECT 可能返回不同结果——它不是「可重复读」。

REPEATABLE READ:快照固定在事务开头

REPEATABLE READ 用的是事务级快照:从 BEGIN 那一刻起,你整个事务看到的都是同一个版本的世界,外面怎么改都影响不到你。

postgres=# BEGIN;
postgres=# SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
postgres=# SELECT amount FROM orders WHERE user_id = 1;
-- 无论别人怎么改、怎么插,这个事务里再查还是这一份快照
postgres=# COMMIT;

代价是:如果两个 REPEATABLE READ 事务改了同一批数据,后提交的那个会报「序列化失败(serialization failure)」,错误码 40001。这时你要在应用里捕获这个错误并重试整个事务。这叫「乐观锁」思路——冲突了再重来。

-- 应用层重试伪代码思路
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 业务逻辑...
COMMIT;
-- 若捕获到 SQLSTATE 40001,sleep 一小会儿再重跑整段事务
Warning

REPEATABLE READ 下如果频繁冲突,重试会拖慢性能。冲突多的热点数据,未必适合高隔离级别,先想清楚再上。

SERIALIZABLE:最严,但靠重试兜底

SERIALIZABLE 让所有事务看起来「一个接一个顺序执行」。它内部用「可串行化快照隔离(SSI,Serializable Snapshot Isolation)」检测真正的冲突。遇到冲突同样会报 40001,需要应用重试。它最稳,但并发度最低、对写多的场景压力最大。日常很少直接用它,多在「算钱必须绝对不出错」的核心账务里才上。

怎么设置隔离级别

两种做法。第一种在事务里开头指定:

-- 开启一个可重复读的事务
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SELECT amount FROM orders WHERE user_id = 1;
-- 在这个事务里,无论别人怎么改,你看到的快照都不变

COMMIT;

第二种是改整个连接的默认值,后面所有事务都按它来:

-- 设置当前会话的默认隔离级别
SET default_transaction_isolation = 'serializable';

-- 查看当前会话的隔离级别
SHOW default_transaction_isolation;
Warning

SET TRANSACTION 必须紧跟在 BEGIN 之后、第一条查询之前才生效。已经跑了查询再改就晚了。

LOCK 主动加锁

有时候光靠隔离级别还不够。比如你要先读订单总额、再据此更新账户,期间不想让别人插进新订单。这时可以显式加表锁。

postgres=# BEGIN;
postgres=# LOCK TABLE orders IN SHARE MODE;
-- 在事务结束前,其它会话不能对 orders 做 UPDATE / INSERT / DELETE
-- 但能 SELECT(共享锁允许并发读)

SELECT sum(amount) FROM orders;
-- 在这里做你的业务逻辑

COMMIT; -- 锁在事务结束时自动释放,没有「解锁」命令

常用的锁模式(从松到严):

  • ACCESS SHARE:读表,最松,多数查询默认就带。
  • ROW SHARE / ROW EXCLUSIVE:改数据时自动加。
  • SHARE:阻止写入,但允许读,适合「我要统计、别让人改」的场景。
  • EXCLUSIVE / ACCESS EXCLUSIVE:最严,连读都拦,建索引、改表结构时常用。

不想傻等锁,可以加 NOWAIT,拿不到立刻报错而不是挂着:

postgres=# LOCK TABLE orders IN EXCLUSIVE MODE NOWAIT;
-- 拿不到锁马上返回错误,而不是一直等
Warning

LOCK 只在事务里有效,且锁会一直持有到事务结束(提交或回滚)。别在事务里长时间挂着锁,否则容易堵住别人。

死锁与咨询锁

死锁(Deadlock):两个事务互相等对方手里的锁,谁都走不下去。比如 A 锁了订单 1 去等订单 2,B 锁了订单 2 去等订单 1。PG 能检测出来并强制回滚其中一个,抛出错误。但被回滚的那个事务就白干了,最好从设计上避免——让所有会话都按相同顺序去锁对象。

-- 会话 A:先锁 1 再锁 2
postgres=# BEGIN;
postgres=# UPDATE orders SET status='x' WHERE id=1;
postgres=# UPDATE orders SET status='x' WHERE id=2; -- 等 B 手里的 2

-- 会话 B:反过来先锁 2 再锁 1,制造死锁
postgres=# BEGIN;
postgres=# UPDATE orders SET status='y' WHERE id=2;
postgres=# UPDATE orders SET status='y' WHERE id=1; -- 等 A 手里的 1
-- PG 检测到死锁,回滚其中一个并报错

另外 PG 还提供「咨询锁(Advisory Lock)」,用 pg_advisory_lock() 加、由应用自己约定含义,适合那种不适合用表锁表达的互斥场景:

postgres=# SELECT pg_advisory_lock(12345);  -- 拿到一把应用自定义的锁
postgres=# SELECT pg_advisory_unlock(12345); -- 释放
Tip

日常开发里,能用事务隔离级别解决的就别手动加锁;真要加,优先选最松的锁模式,影响面最小。锁越严,别人越容易卡在你这儿。

常见误区

  • 以为「隔离级别越高越安全所以全部用 SERIALIZABLE」:会牺牲并发,且仍需处理 40001 重试。
  • 以为 LOCK 有对应的 UNLOCK 命令:没有,锁随事务结束自动释放。
  • 在长事务里加 ACCESS EXCLUSIVE 还去喝茶:别人全卡住,连查询都进不来。
  • 把别的数据库的「读已提交」行为套到 PG:PG 的 READ COMMITTED 不会脏读,这点要记牢。
  • 以为 REPEATABLE READ 一定不报错:它遇到写冲突会报 40001,应用必须准备好重试。