bigywater.tech
PostgreSQL · 5

ERROR: could not serialize access due to concurrent update:PostgreSQL 三个隔离级别、写偏斜、40001 重试和丢失更新实测

“ERROR: could not serialize access due to concurrent update 怎么办、PostgreSQL 的隔离级别到底怎么选” 要靠真实的并发复现才说得清。这篇在本机自己起一个 PostgreSQL 16.15,用 psycopg 同时开多个会话,逐步复现三个级别的行为、写偏斜、40001 重试和丢失更新。

40001
更新别人在快照之后改过的行,或 SERIALIZABLE 检测到冲突时的 SQLSTATE,意思是“请重试”
0 对 1
写偏斜例子里最终值班人数:REPEATABLE READ 是 0(规则被打破),SERIALIZABLE 是 1
89 对 400
8 个会话各做 50 次“先读后写”最终得到的值(应为 400);原子 UPDATE 和 FOR UPDATE 都是 400

READ COMMITTED

实测 · 本机执行输出
    A: begin  →  BEGIN
    A: select bal from acct where id = 1  →  [(100,)]
    B: update acct set bal = 200 where id = 1  →  UPDATE 1
    A: select bal from acct where id = 1(同一个事务,B 已提交)  →  [(200,)]
    A: commit  →  COMMIT
  UPDATE 遇到别人刚改过的行会重新检查 WHERE:A、B 同时执行 update acct set bal = bal + 1 where bal = 200,B 先改并提交,A 等到 B 提交后重新评估
    B: begin; update acct set bal = 300 where id = 1  →  BEGIN
    A:update acct set bal = bal + 1 where bal = 200(被 B 的行锁挡住,正在等)
    B: commit  →  COMMIT
    A: (解除阻塞之后的结果)  →  UPDATE 0
    最终 bal = 300 (A 的 WHERE bal = 200 在 B 提交后重新评估,发现 bal 已经是 300,不再匹配,所以一行都没改)

每条语句一个新快照;UPDATE 等锁之后重新检查 WHERE。

REPEATABLE READ

实测 · 本机执行输出
    A: begin isolation level repeatable read  →  BEGIN
    A: select bal from acct  →  [(100,)]
    B: update acct set bal = 150 where id = 1; insert into acct values (2, 5)  →  UPDATE 1
    A: select bal from acct(同一个快照,看不到 B 的修改)  →  [(100,)]
    A: select count(*) from acct(看不到 B 新插入的行,没有幻读)  →  [(1,)]
    A: update acct set bal = bal + 1 where id = 1  →  SerializationFailure(SQLSTATE 40001): could not serialize access due to concurrent update
    A: commit(事务已经处于中止状态)  →  ROLLBACK
  报错之后这个事务不能继续,只能 rollback 重来:select bal from acct 再试一次 → [(150,), (5,)]

整个事务一个快照,没有幻读;更新别人改过的行直接报 40001。

SERIALIZABLE

实测 · 本机执行输出
    repeatable read:X 看到值班 2 人,Y 看到值班 2 人,于是 X 让 alice 请假、Y 让 bob 请假
    X commit → COMMIT;Y commit → COMMIT
    最终值班人数 = 0
    serializable:X 看到值班 2 人,Y 看到值班 2 人,于是 X 让 alice 请假、Y 让 bob 请假
    X commit → COMMIT;Y commit → SerializationFailure(SQLSTATE 40001): could not serialize access due to read/write dependencies among transactions | DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt. | HINT: The transaction might succeed if retried.
    最终值班人数 = 1

拦下写偏斜:第二个提交被拒绝,提示重试。

重试

实测 · 本机执行输出
  serializable:8 个会话各做 50 次「读出 n、加 1 写回」,每次失败就重试:最终 n = 400,共重试 1180 次,耗时 0.54 s
  repeatable read:8 个会话各做 50 次「读出 n、加 1 写回」,每次失败就重试:最终 n = 400,共重试 1301 次,耗时 0.57 s

热点行读后写时重试次数很多,应用必须有重试循环。

丢失更新

实测 · 本机执行输出
  read-modify-write :最终 n = 89(应为 400),丢了 311 次更新,耗时 0.08 s
  atomic            :最终 n = 400(应为 400),丢了 0 次更新,耗时 0.04 s
  for update        :最终 n = 400(应为 400),丢了 0 次更新,耗时 0.17 s

先读后写在 READ COMMITTED 下丢更新;用原子 UPDATE 或 SELECT FOR UPDATE。

分步演示

隔离级别实测
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / PostgreSQL 16.15(Homebrew);每个实验自己 initdb 一个数据目录,端口 9261 起,只监听 127.0.0.1,不碰本机已有的 5432 上的实例;命令行工具统一用 LC_ALL=C 取英文原文,路径换成 ;计时用 time.perf_counter 或 EXPLAIN ANALYZE 的 Execution Time,数字每次运行会有小幅抖动;实验结束后整个数据目录删除。多个会话是同一个进程里的多个 psycopg 连接(autocommit),按步骤交替执行;并发计数实验用 8 个线程,次数和耗时每次运行会不同
    • 没有验证:SERIALIZABLE 在表很大、谓词锁升级为页锁/表锁时的误报率;可串行化的 DEFERRABLE 只读事务;两阶段提交与隔离级别;连接池(pgbouncer)下事务边界的影响;逻辑复制/热备上的 serializable;PostgreSQL 其他大版本的差异
    京ICP备2026067502号-1京公网安备11010502063487号