跳到主要内容
新品云 ERP 正式上线,几分钟开通专属实例

技术分享

Odoo 20 并发三坑:快照幂等、限流计数与序列化失败

「防重复提交」和「接口限流」听起来都是小事,真正做到并发正确却不容易。我们的表单接口在压测里连续暴露了三个并发问题,每个的根因都和 PostgreSQL 的事务语义有关。本文给出错误写法、根因、修法,以及我们最终固化的并发测试方式。

坑一:幂等重试,读到的还是旧快照

接口用「提交键」防重复,自然的写法是"先查后写":

existing = search([('key', '=', key)])
if existing:
    return existing.reference      # 重试:直接返回既有单号
record = create(...)               # 首次:建档

单线程下完全正确。但在可重复读隔离级别下,同一个事务里再查一次,看到的还是事务开始时的快照。序列变成:请求 A 建了档但还没提交,请求 B(重试)在快照里怎么查都看不到 A,于是 B 也走建档——最后靠唯一约束兜底报错。错误码对了,但"先查后写"的优化完全失效,还多走了一次失败路径。

修法是两条:

  1. 正确性下沉到数据库:给提交键建唯一约束,"先查后写"只当快路径;冲突时读出既有记录返回,这才是幂等性的最终保证;
  2. 要读新数据就换连接:需要"再查一次看到别人刚提交的"时,用独立连接/新事务重读——在 RR 事务里再查多少次都是旧快照。

坑二:限流计数 upsert 的序列化失败

限流用一张计数表,按「键 + 时间窗」做 INSERT ... ON CONFLICT 自增:

INSERT INTO rate_bucket (key, window_start, count)
VALUES (%s, %s, 1)
ON CONFLICT (key, window_start)
DO UPDATE SET count = rate_bucket.count + 1;

单线程没问题,并发下开始出现数据库抛出的序列化失败——可重复读里两个事务更新同一行,晚到的一方可能被要求回滚重来。处理分三层:

  1. 计数事务独立提交:限流计数不能挂在业务事务里,否则业务回滚会把已消费的次数"退还",限流形同虚设;
  2. 有限重试:序列化失败是瞬时错误,捕获后做少量重试即可收敛;
  3. 口径明确:宁可多放行一次,不可把限流失败变成请求失败——计数异常按放行处理并记录告警。

坑三:同键并发双请求,谁赢都对

即使有唯一约束,仍要回答一个问题:两个同键请求几乎同时到达,"后来者"该收到什么?

我们的答案:200 + 既有单号,直接依赖唯一约束——插入冲突时读出既有记录返回,"谁先到"不参与结果。

并发测试怎么写

这三个坑有一个共同点:单线程测试全都发现不了。我们的做法是把并发场景固化成测试:

场景 并发方式 期望结果
同键同内容双请求 两个连接同时提交 201 + 200,返回同一单号
同键异内容双请求 两个连接同时提交 201 + 409,明确冲突
限流阈值边界 并发送超阈值请求 不超过阈值放行,其余 429
幂等重试读新数据 A 提交后 B 立即重试 B 看到 A 的记录,不重复建档

关键是真的用两个数据库连接同时打——在同一个事务/连接里模拟并发,等于什么都没测。

小结

把三件事连起来看,规律很清楚:并发正确性靠数据库约束与事务语义兜底,应用层只负责体验。先查后写、进程内锁、事务内重读这些"看起来更省"的做法,在可重复读下都可能是错的。每条约束配一个真并发的测试,比事后修线上数据便宜得多。

相关产品与 ERP 实践文章在宏斋博客。 宏斋云ERP · Blog

返回列表