文章

PostgreSQL 事务隔离

PostgreSQL 事务隔离

1 简介

在没有事务隔离机制的情况下,并发事务间相互作用会产生一些异常现象:

  • 脏读(Dirty Read):一个事务读取了另一个未提交并发事务写入的数据。
  • 不可重复读(Nonrepeatable Read):一个事务重读之前已读过的数据,发现该数据已被其他并发事务修改(该事务已在初次读取之后提交)。
  • 幻读(Phantom Read):一个事务重新执行了一个之前执行过的查询,发现返回的行集发生了变化,这种变化可能是多了行、少了行、行被替换(比如某行不见了,但是又多了一个新行),这种变化是由于其他并发事务修改导致(该事务已在初次查询之后提交)。如果返回的行还是之前返回的那些行,但是某些行的某些列的值发生了变化,这种应该算不可重复读。
  • 串行化异常(Serialization Anomaly):一组并发事务成功提交后的结果,与将这些事务逐个按某种顺序执行的任何可能结果都不一致。

SQL 标准定义了 4 种事务隔离级别,越高的级别禁止的异常现象越多:

隔离级别脏读不可重复读幻读串行化异常
读未提交(READ UNCOMMITTED)允许(但 PG 中不允许)允许允许允许
读已提交(READ COMMITTED)不允许允许允许允许
可重复读(REPEATABLE READ)不允许不允许允许(但 PG 中不允许)允许
可串行化(SERIALIZABLE)不允许不允许不允许不允许

在 PostgreSQL 中可通过命令 SET TRANSACTION 设置当前事务的隔离级别,其接受 SQL 标准定义的 4 种隔离级别,但是在 PostgreSQL 内部实际上只有后 3 种隔离级别,因为读未提交和读已提交的行为一致。另外,PostgreSQL 中可重复读的的限制也比 SQL 标准更严格,其不允许幻读,而 SQL 标准允许。

2 读已提交(READ COMMITTED)

读已提交是 PostgreSQL 的默认隔离级别。记住:每条命令一个快照;遇到并发修改,可以基于新版本重新判断。 本事务之前的修改仍然可见。

  1. SELECT(不带 FOR UPDATE/SHARE):读取命令开始时的快照。别人之后提交的修改,本次查询看不到,下次查询可能看到。

  2. UPDATE、DELETE、SELECT FOR UPDATE/SHARE:先按快照找行。遇到并发修改或冲突的行锁,必要时等待,再判断:
    • 回滚了:继续进行原计划的操作。
    • 提交了:判断目标行如何变更:
      • 无变更:继续进行原计划的操作。
      • 删除了:忽略该行。
      • 更新了:用新版本重新判断 WHERE 条件:
        • 符合:在更新后的目标行版本上进行操作。
        • 不符合:忽略该行。
  3. INSERT(含 ON CONFLICT):唯一性检查不受快照限制,能发现快照中不可见的冲突行,但这不是脏读。必要时先等待,再检查冲突:
    • 无冲突,或等待后冲突消失了:继续尝试插入。
    • 冲突仍然存在:
      • 普通 INSERT:报唯一性违例(23505)。
      • DO NOTHING:跳过该行。
      • DO UPDATE:更新冲突行;带 WHERE 时,条件成立才更新。

    后两种处理即使遇到快照不可见的冲突行也适用,但只处理 ON CONFLICT 覆盖的冲突。这里仅讨论立即检查的唯一索引冲突,假设其他约束均通过;外键、表锁、触发器等也可能导致等待或报错。INSERT ... SELECT 的读取部分仍按命令快照执行。

  4. MERGE:先连接源表和目标表,再按匹配状态选择动作。每类动作按书写顺序执行第一个条件成立的动作,没有则不执行。遇到并发修改,必要时先等待;对方回滚则继续原操作,提交则按以下规则处理:
    • UPDATE 或 DELETE:
      • 目标行未被并发修改:在找到的目标行上正常执行。
      • 更新后仍匹配:用新版本从头重选适用的动作。
      • 更新后不再匹配:分别评估 NOT MATCHED BY SOURCE 和 NOT MATCHED [BY TARGET] 两类动作。
      • 目标行被删除:评估 NOT MATCHED [BY TARGET] 动作。
    • INSERT:
      • 无唯一索引冲突,且其他约束检查均通过:插入成功。
      • 唯一索引冲突仍存在:报 23505,不会转为 MATCHED 动作。

    MERGE 不保证一定插入或更新;选中的动作执行报错,也不会继续尝试后面的动作。

3 可重复读(REPEATABLE READ)

记住:整个事务共用一个快照;要修改或锁定的行被别人改过并提交,报错重试。 快照在第一条非事务控制语句开始时建立,不一定是 BEGIN 时。本事务之前的修改仍然可见。

  1. SELECT(不带 FOR UPDATE/SHARE):始终读取同一个事务快照。别人后来提交的修改,后续查询也看不到;自己的修改除外。PostgreSQL 在此级别下既没有不可重复读,也没有幻读。

  2. UPDATE、DELETE、SELECT FOR UPDATE/SHARE:先按事务快照找行。遇到并发修改或冲突的行锁,必要时等待,再判断:
    • 回滚了:其修改被撤销,继续尝试原计划的操作。
    • 提交了:判断目标行如何变更:
      • 仅锁定,没有实际更新或删除:继续尝试原计划的操作。
      • 快照建立后更新或删除了该行:报串行化失败(40001),不改用新版本继续操作。

    即使更新前后列值相同,只要产生了新行版本,也算更新。

  3. INSERT(含 ON CONFLICT):唯一性检查仍不受快照限制。讨论范围与上一节相同,必要时先等待,再检查冲突:
    • 无冲突,或等待后冲突消失了:继续尝试插入。
    • 冲突仍然存在:
      • 普通 INSERT:报 23505,即使冲突行对快照不可见也是如此。
      • DO NOTHING:冲突行对本事务可见则跳过;若是别人写入、快照不可见的行,则报 40001。
      • DO UPDATE:冲突行可见且没有第 2 类的并发修改冲突,则更新(带 WHERE 时还要满足条件);否则报 40001,不能靠 WHERE 为假绕过检查。

    “可见”包括本事务之前插入的行。ON CONFLICT 仍只处理其覆盖的冲突;INSERT ... SELECT 的读取部分使用事务快照。

  4. MERGE:按事务快照连接并选择动作:
    • UPDATE 或 DELETE:按第 2 类处理。遇到快照后的已提交更新或删除,报 40001,不在新版本上重选动作。
    • INSERT:按普通 INSERT 处理。等待并重新检查后,唯一索引冲突仍存在则报 23505,不转为 MATCHED 动作;约束均通过则插入。
    • DO NOTHING,或没有条件成立的动作:不修改该候选行。

记住两个错误码:40001:回滚并重试整个事务,重新读取、重新判断;23505:唯一性违例,先查原因,不是重试就能解决。 稳定快照不等于可串行化,可重复读仍可能出现串行化异常。

4 可串行化(SERIALIZABLE)

PostgreSQL 中可串行化隔离级别的工作方式与可重复读完全相同,只是增加了对串行化异常检测,一旦检测到可能导致串行化异常的条件,就会触发串行化失败。

5 参考资料

本文由作者按照 CC BY 4.0 进行授权