订单系统:状态机、逆向流程与一致性 — vibrant cover for technical blog article

订单系统:状态机、逆向流程与一致性

一个订单从创建到完结,要穿越十几个状态,每个状态的流转都是一致性博弈。

2025年6月27日·返回文章列表
文章大纲

订单是电商系统的中枢。用户下单、支付、发货、确认收货、评价、退款,这些动作全都围绕订单展开,库存、支付、营销、物流各自有自己的系统,但它们都通过订单串在一起。订单系统的复杂度不在于把数据存起来,而在于它要管理十几个状态的流转、联动一堆外部系统、还要兜底处理各种退货退款的逆向场景。这些复杂度最终都压在一个词上,一致性。

很多人觉得订单系统就是 CRUD,把订单增删改查做好就行了。做过的人都知道这是天真的错觉。订单系统的真正深水区在逆向流程,退款退货售后那一片。正向流程,下单到支付到发货,相对清晰,因为有明确的主线。逆向流程才是真正的考验,因为涉及的金额计算、库存回补、券处理、多方审核,每一种场景都有自己的规则,错一个就是客诉甚至资损。这篇文章把订单系统的核心矛盾拆开来讲,状态机、并发安全、联动一致性、逆向流程,这几块是订单系统能不能稳的根基。

状态机,订单系统的骨架

一个订单从创建到完结,会穿越一长串状态。待支付、待发货、已发货、已完成、已取消、退款中、已退款,每个状态代表订单生命周期里的一个阶段。状态机的核心规则是合法流转,每个状态只能转到特定的下一状态,非法流转必须被拒绝。

这个规则听起来理所当然,没做好的系统却会出各种诡异问题。已发货的订单被改回待支付,已退款的订单又显示待发货,已取消的订单还在发货流程里。这些不是偶发的 bug,是状态机设计不全的必然结果。当任何代码都能随意改订单状态时,数据混乱只是时间问题。

状态机的合法流转要靠数据库层强约束,而不是应用层判断。应用层判断可以被绕过,并发请求可以钻空子,只有数据库的带条件更新才是真正可靠的第一道防线。

PlantUML
正在加载图表…
PlantUML diagram

图:订单正向状态流转——每个状态只能转向特定下一状态,靠带前置条件的 CAS 更新保证合法流转与并发安全

-- 带前置状态的 CAS 式更新,只有当前状态为期望值才能改
UPDATE orders
SET status = 'PAID', paid_at = NOW(), version = version + 1
WHERE order_no = #{orderNo} AND status = 'WAITING_PAY'

这条 SQL 的精髓在 WHERE status = 'WAITING_PAY'。只有当前是待支付状态的订单才能被改成已支付。如果两个并发请求同时来改,比如支付回调同时到达两次,数据库只让其中一个的 affected=1 成功,另一个拿到 affected=0 自然就知道状态已经被改过了。这是天然的并发安全,不需要额外的分布式锁。

我接手过一个老系统,状态更新没有用这种带条件的写法,直接 UPDATE orders SET status = 'PAID' WHERE order_no = ?。上线两个月就出了事,一笔订单因为支付通道重试回调了两次,两次都把状态改成了已支付,后续发货流程被触发了两次,用户收到两个包裹,平台亏了一份货的钱。排查时翻日志才看到两次回调间隔不到一秒。从那以后我立了一条死规矩,订单状态变更一律用带前置条件的 CAS 更新,没有例外。

状态机的并发与原子性

状态变更并发安全靠的是数据库的原子更新,但只靠数据库还不够。有些状态变更涉及多个动作要一起做,比如支付成功时要改订单状态、记支付流水、触发库存扣减,这几个动作要么全成要么全败,不能做一半。

在单个数据库内,用本地事务包起来就行,BEGINCOMMIT 之间把这几步一起做,任何一步失败整体回滚。但订单系统往往涉及多个库或多个服务,订单库、库存库、营销库各自独立,本地事务罩不住跨库的操作。

跨库事务的方案有几种。强一致的方案是 TCC 或者两阶段提交,保证多个资源要么全成功要么全回滚,代价是性能损耗和实现复杂度高。更多订单系统选的是最终一致,订单创建成功后,通过本地消息表或者事务消息异步去锁定库存、锁定优惠券,如果异步动作失败再做补偿。这种方案牺牲了一小段时间的强一致,换来的是性能和可用性,前提是补偿逻辑要可靠。

支付成功后的联动,核心同步辅助异步

支付回调是订单生命周期里最关键的节点。用户支付成功后,系统要做一连串联动,订单状态改成已支付、库存从锁定变成扣减、优惠券核销、发支付成功通知。这些动作哪些必须同步做,哪些可以异步做,是设计的取舍点。

核心原则是核心状态同步、辅助动作异步。订单状态更新必须同步,因为它是后续所有动作的前提,库存扣减和券核销也建议同步,因为它们直接关系资金和库存安全。发通知这类辅助动作可以异步,失败了重试就行,不影响主流程。

PlantUML
正在加载图表…
PlantUML diagram

图:支付回调联动——订单状态、库存、券核销走同步事务保障资金安全;通知走异步消息可失败重试

异步动作必须配合重试和幂等。库存扣减的消费者可能因为网络问题处理失败,MQ 会重投递,消费者必须保证同一条支付成功事件只扣减一次库存,靠幂等键或者状态判断来去重。这块在消息队列那篇已经详细讲过,这里不重复。

超时未支付的取消

订单超时未支付要自动取消,释放锁定的库存和优惠券,否则这些资源会被一直占着。这个动作靠延时任务实现,下单时投递一个延时消息,比如 30 分钟后触发,到点了检查订单状态。

这里的关键是检查这一步。延时任务触发时,要先尝试用 CAS 更新订单状态为已取消,UPDATE orders SET status = 'CANCELLED' WHERE order_no = ? AND status = 'WAITING_PAY'。如果用户在超时前一刻完成了支付,订单状态已经变成已支付,这条更新会返回 affected=0,取消动作自动跳过,不会误取消已支付的订单。这就是 CAS 更新在边界场景的又一价值。

如果不用这种带条件的检查,而是先查再改,查的时候是待支付,改的时候已经被支付回调改成已支付了,就会把已支付订单误取消。这种并发竞争在高频下单场景里几乎一定会发生,不能靠运气躲。

逆向流程,订单系统的深水区

逆向流程比正向复杂得多,因为它要处理各种异常路径。未发货的退款相对简单,订单还在待发货状态,直接走退款流程。已发货的退款就复杂了,要用户退货、商家收货、质检、退款,中间还有商家拒绝、质检不通过各种分支。部分退款更麻烦,订单里部分商品退款,优惠分摊要重新算、金额要按比例回退。售后退款涉及已完成订单,可能有时间限制和额外的审核。

PlantUML
正在加载图表…
PlantUML diagram

图:退款逆向流程状态机——穷举所有状态与流转,每个异常分支都有明确出口,避免死状态和断头路

每种场景都有独立的子状态机。退款申请、商家审核、用户退货、商家收货质检、退款执行、退款成功,这是一条完整链路,中间任何一步都可以分流到拒绝或者退回上一步。设计逆向流程时,要把这条状态机画完整,穷举所有状态和流转,不能有死状态或者断头路。

退款的金额计算尤其要严谨。一笔订单享受了满减优惠,部分商品退款时,优惠要按比例分摊回退,不能让用户退一个商品却拿回原价。优惠券退不退回也有规则,有些券退款后失效,有些可以退回账户。运费怎么算,是全额退还是按商品比例退。这些规则要产品定清楚,代码严格执行,模糊地带就是客诉和资损的来源。

退款执行要走支付通道原路退回,必须幂等。退款请求带唯一流水号,通道重复回调不会导致重复退款,靠幂等去重。

订单的拆分

复杂业务里订单常常要拆分。一个订单含多个商家的商品,要拆成子订单让各商家独立发货。不同商品从不同仓发,要按仓拆分。预售和现货混合,要拆开让现货先发。

拆分的核心是金额按比例分摊。父订单的总金额、优惠、运费,要合理分摊到各子订单上,分摊规则要保证子订单金额之和等于父订单金额,这是金额守恒。分摊算法常用按商品金额比例分,最后用最大值兜底吸收舍入误差。这块算错,对账时子订单金额加起来不等于父订单,整个账目就乱了。

历史数据归档与对账兜底

订单数据持续增长,热表越来越大,查询越来越慢。分层存储是常见解法。近三个月的热数据放主库,三个月到一年的温数据迁移到归档表或者读库,一年以上的冷数据归档到对象存储或大数据平台。查询时按时间路由,用户看历史订单走归档库,不拖累主库性能。

订单和库存、支付、营销的对账是最后兜底。已支付的订单必须有对应的支付流水,已支付的订单库存必须已扣减,每天跑对账发现不一致就告警。前面所有的联动、补偿、CAS 做得再好,也挡不住所有边界 bug,对账是发现这些漏网之鱼的最后手段。订单系统的成熟度,很大程度上体现在对账差异的收敛速度上。

逆向流程要先把状态机画完整

退款退货售后的状态机是最容易留死穴的地方。设计逆向流程时,第一步是穷举所有状态和流转,画一张完整的状态图,每个状态都有明确的入口和出口,没有死状态没有断头路。状态机不全就写代码,上线后一定会遇到这种订单怎么到了某个状态的诡异 bug,排查时痛不欲生。

状态机是设计的第一步

订单系统的复杂度全在状态和一致性上。正向流程相对清晰,逆向流程是深水区,每一个退款退货场景都有自己的规则。但无论正向还是逆向,设计的起点都是同一个,把状态机画完整。

我带新人做订单系统时,第一周不许写一行代码,只许画状态机。把所有状态、所有流转、所有触发条件、所有兜底分支都列出来,反复评审到无懈可击,再动代码。这个习惯看起来慢,实际上省下了后面无数的排查时间。状态机设计不全的系统,上线后遇到诡异 bug 的概率是百分之百,区别只在于早晚。

状态机驱动加 CAS 并发安全、核心同步辅助异步、逆向流程覆盖所有场景、对账兜底一致性,这是订单系统稳的四根支柱。其中状态机是重中之重,画完整再写代码,这一条做不到,后面三根都立不住。

Continue Reading

关联文档推荐

查看全部
库存系统:预占、扣减与一致性保障 — editorial cover photo读写分离与主从延迟:DB 性能优化的基石 — vibrant cover for technical blog article接口幂等性:从去重到状态机的完整设计 — editorial cover photo
作者:archy.shawn
声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。