← 中文资源

简体中文版本

PostgreSQL 序列化失败 40001 — 需重试事务

通过在 SERIALIZABLE/REPEATABLE READ 隔离级别下重试整个事务,处理 PostgreSQL 40001 序列化失败。

阅读英文原文

症状

错误“PostgreSQL 序列化失败 40001 — 需重试事务”指向一个明确的失效模式。下面依次给出症状、定位该原因的检查命令、根本原因、修复方案,以及证明修复有效的验证步骤。

可观察到的症状包括:

  • ERROR: 因事务间读写依赖无法序列化访问(SQLSTATE 40001)
  • 仅在对同一行的并发写入下出现失败
  • 重试同一事务即成功

诊断

先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。

步骤 1

psql -c "SHOW transaction_isolation"

SERIALIZABLE 或 REPEATABLE READ 即解释了并发下为何会出现 40001。

步骤 2

psql -c "BEGIN ISOLATION LEVEL SERIALIZABLE; ..."

并发运行两次同一事务可稳定触发序列化错误。

常见诱因(按出现频率排序):

  • SERIALIZABLE 隔离会中止无法排序的事务
  • REPEATABLE READ 检测到对已读行的并发更新
  • 应用把 40001 当作永久错误而非重试

根因

在 SERIALIZABLE 与 REPEATABLE READ 下,PostgreSQL 会中止会破坏可串行性的事务,返回 SQLSTATE 40001。这是正常的并发信号,正确应对方式是从头重试整个事务。

修复

用带退避的重试循环包裹事务,限制次数,重试整个事务(而非单条语句),并通过一致地排序访问来降低争用。

注意:在事务外执行的副作用重试会重复;应把所有效果放在事务内。

async function runSerializable(fn, tries = 5) {
  for (let attempt = 1; attempt <= tries; attempt++) {
    const client = await pool.connect();
    try {
      await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
      const result = await fn(client);
      await client.query('COMMIT');
      return result;
    } catch (e) {
      await client.query('ROLLBACK');
      if (e.code === '40001' && attempt < tries) {
        await new Promise((r) => setTimeout(r, 50 * attempt));
        continue; // retry the WHOLE transaction
      }
      throw e;
    } finally {
      client.release();
    }
  }
}

验证

运行并发写入并确认重试循环成功、不把 40001 暴露给调用方;在指标中统计重试次数。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。