这类问题我又碰上了一次。订单状态机按事件驱动更新,重放历史消息时没按时间排序,12:05 的 done 事件先到,12:03 的 init 事件后到,状态被直接改回 init,后续查询全部认为订单未完成。测试环境用 Kafka 重放必现,线上补偿脚本跑完仍有 3 单卡住。

第一反应是「事件里没带版本号,无法判断新旧」,验证下来不成立。

这类报错背后常见的原因有几处,逐个排除就行。状态更新是无条件覆盖,没有比较事件时间;重放脚本按分区并行拉取,跨分区的顺序乱了;状态机缺少终态保护,允许从 done 回退。

上来先查这单的事件时间顺序

python -c "import json;print(sorted([json.loads(l) for l in open('ev.jsonl')], key=lambda r: r['ts']))"

再往下在更新语句里加上时间条件

mysql -e "UPDATE orders SET status='done' WHERE id=1 AND updated_at < '2026-11-15 12:05:00'" app

给状态机加终态不可回退的断言

python -c "m={'init':['paid'],'paid':['done'],'done':[]};print(m['done'])"

等前面都确认过,再按时间顺序串行重放一次

python -c "print('replay sorted by ts per partition')"

改完先别撤,确认这几项。重放后 3 单都停在 done,事件时间线单调递增,状态机拒绝任何从终态出发的转移。

补一句。状态机要同时防并发和防乱序,只看当前状态不够,事件时间或者版本号必须参与更新条件。如果同一现象隔几天又出现,说明根因没找到,别当偶发处理。

作者 9823189