现场是这样的。支付回调落在 Flask 服务上,处理顺序是先写回调日志再更新订单状态,两段在同一个事务里。回调重放时插入撞了 trace_id 唯一索引,Postgres 抛 23505,服务把异常包成 500 返回,上游认定失败继续重试,日志里同一条堆栈刷了几百行。

中间走过弯路,把「冲突之后没有返回成功,上游认为回调失败」当成主因,改完现象没变。

原因不止一个,得分开看。幂等只靠唯一索引,冲突被当成系统错误抛出去;订单状态更新和日志写入的提交顺序没对齐;重放时关键字段被网关改过,唯一键拼出来不一样。

第一件事是确认冲突发生在哪条语句上

python -c "s=open('app.log',encoding='utf-8').read();print([l for l in s.splitlines() if '23505' in l][:3])"

再往下查库确认订单状态是否已经更新过

psql -c "SELECT order_no,status,updated_at FROM orders WHERE order_no='ORDER-8842';"

随后把唯一冲突捕获成幂等命中并返回成功

curl -s -o /dev/null -w '%{http_code}' -X POST http://pay:8080/callback -d @cb.json

末了重放三次确认只有一次真正落库

for ($i=1; $i -le 3; $i++) { curl -s -X POST http://pay:8080/callback -H 'X-Trace-Id: t-8842' -d @cb.json }

怎么算真的好了,看的是同一批现象。三次重放都返回 200,回调日志只有一行、订单状态只更新了一次,日志里不再出现 23505 的堆栈。

容易忽略的是,唯一索引是最后一道闸,捕获冲突后要按成功返回,否则上游会把正常的幂等命中当成故障一直重试。如果同一现象隔几天又出现,说明根因没找到,别当偶发处理。

作者 9823189