首页 / 资讯中心 / 文章详情

用REA模式重构订单与库存:资源、事件、参与方如何落地

用REA模式重构订单与库存:资源、事件、参与方如何落地 ★ FEATURED ARTICLE
过年期间闲着没事我把一个内部代号叫rea的老系统拿了出来。这名字乍看很怪只有三个字母翻遍手头的设计文档也只找到一句“按 rea 模式重构订单与库存”。起初我以为是前人取名偷懒等我真正动手梳理清楚后才发现这三个字母背后是一整套值得重新审视的业务建模思路Resource、Event、Agent也就是资源、事件、参与方。这篇文章不打算写教科书式的术语堆砌。我会直接拿订单、库存、退款这些日常模块当例子把 rea 模式到底怎么落地、每一步如何设计、有哪些坑一定得绕开全部摊开来讲。如果你正被那些越做越乱的“大泥球”业务代码折磨或者想找一个代替传统状态机的建模方案这篇东西应该能帮上忙。1. 为什么我推荐 rea从一张被人嫌弃的架构图说起先说我自己的经历。大概一年多前我把接手系统的架构图发给一位老同事看想让他帮忙提点意见。他回得非常直接你这张图本质是把所有业务对象都画成了一坨“有状态的中心节点”订单表、库存表、账户表各管各的中间拉满箭头看得人头皮发麻。他这句话刺激到我了。仔细想想确实如此传统业务系统里我们习惯先定义订单、库存、商品这一类实体然后给每个实体塞一堆状态字段比如 “订单状态待支付 / 已支付 / 已发货”。每次状态一变就原地 UPDATE 一条记录。短期没问题但时间一长问题全冒出来——状态散落在不同表里业务规则只能用 if / else 硬编码审计时根本说不清“某个余额到底是怎么算出来的”。rea 正好给出了一套相反的答案先关注发生了什么而不是先关注现在是什么。换句话说它把设计重心从“实体有哪些属性”挪到了“真实业务过程中产生过哪些事件这些事件把哪些资源、哪些参与方关联到了一起”。这听起来有点反直觉。我最初也很怀疑都 2020 年代了谁还这么建模但随着我把订单和库存模块按 rea 重画了一遍我发现它解决了我最头疼的几个问题每一笔业务变化都有据可查不会出现“数据莫名其妙变了但没人说得清改了哪一行”订单、库存、账户之间的依赖变得像流水账一样清晰不再靠隐式状态联动做对账和审计时可以直接回答“这个库存数量凭什么等于现在这个数”。所以它并不是老古董而是一种把“账本思想”重新请回业务建模的有效方式。1.1 传统状态机最别扭的地方状态来源丢失我后来总结过一句话状态机只关心“现在走到哪一步”却不关心“为什么能走到这一步”更不关心“中间发生的动作由谁负责”。举一个非常普通的场景用户退款成功。很多系统会直接执行类似“订单状态改成已退款库存加回去账户余额增加”的三段式操作。听着没问题问题是这三条 UPDATE 分散在三个服务里。如果账户服务先成功了库存服务超时重试订单状态遥遥无期最终整个数据看起来就很奇怪线上排查时只能靠猜。rea 在这里的处理方式完全不同它不会一上来就设定“已退款”这个状态而是把“用户发起退款申请”“审核通过”“资金退回”“库存回补”都当作一个个独立事件记录下来。至于最终订单是什么状态可以通过这些事件推导出来而不是由某个 UPDATE 拍板。这个转变看起来很轻实际上背后的思维差异很大传统做法把结果当真相rea 把过程当真相。1.2 rea 适合什么样的团队和项目我不是让所有项目都无脑上 rea。这套模式更适合业务规则复杂、涉及多参与方、需要强审计的场景比如电商交易、财务结算、供应链库存、积分商城这类系统。反过来如果只是一个简单的投票网页或者内容发布后台硬上 rea 属于杀鸡用牛刀反而会平白增加大量代码量。一个可行的判断标准是你当前系统的核心难点是不是在“多个对象共同参与一个业务动作且动作的先后顺序与最终一致性都要严格保证”。如果是rea 大概率会帮到你如果你只需要围绕单个实体增删改查那保持简单 CRUD 反而更合适。2. 资源、事件、参与方三个元素到底在讲什么要用好 rea得先把它的三个基本元素吃透。我不打算从维基百科的英文定义开始念而是直接用业务语言拆开讲。2.1 资源不是“数据库字段”而是被参与方使用或处理的对象在 rea 里资源泛指有价值、被业务活动影响的对象。它可以是看得见的库存商品、现金也可以是看不见的服务工时、积分、租赁时长。我踩过一个典型误区一开始我把商品 ID、订单编号、用户 ID 这些主键全部填进资源表结果系统被搅成一锅粥。后来一个导师级的朋友提醒我资源不是业务流水号而是该业务动作真正消耗或产出的东西。订单编号只是事件的一个引用字段不是资源真正有价值的资源是库存数量、账户余额或者仓储体积。把这一点想清楚后续表结构和小模型设计能省一半的返工。还有一种最容易忽略的资源是“承诺”。比如用户下单付款后商家承诺未来某个时间交付一件商品这件“待交付商品”也是资源。很多库存设计不严谨就是因为没有把承诺资源单独建模结果超卖时才发现系统里压根没有记录“已经承诺掉但还没发货的那部分数量”。2.2 事件是唯一能写进“流水账”的东西也是数据可靠性的锚点事件描述业务过程中真实发生过的一件事比如“用户支付了 100 元”“仓库出库了 3 件商品”“客服提交了退款申请”。事件有两个很关键的特征第一事件一定是与时间绑定的而且是不可变的。一旦记录到日志里它就不该被 UPDATE。纠错的方式不是删除旧事件而是再追加一个新事件。这和企业财务里“不允许涂改凭证只能红字冲销”是同一个思路。第二事件带上了“双向语义”。一个事件通常同时代表“流出”和“流入”。比如“仓库发货出库”对仓库是资源流出对客户是资源流入 “用户付款”对用户是资金流出对商家是资金流入。设计事件表时如果能刻意体现这种双向关系后续统计收入、成本、库存时会非常顺手因为你用同一个事件就能给两本账同时提供数据两边也不容易对不上。我自己做过一个最简单的事件表核心字段大概是字段含义注意点event_id事件唯一标识必须全局唯一别用简单的自增event_type事件类型如支付、发货、退款、入库occurred_at发生时间建议使用业务时间而不是入库时间resource_ref涉及资源指向资源表的引用inflow_agent资源流入方比如客户outflow_agent资源流出方比如商家quantity数量或金额带符号或分双向设计ref_id业务单据号关联原订单号、原流水号这个结构不一定适用于所有项目但它已经能覆盖我见到的很多交易与库存场景也足够作为起步骨架继续扩展。2.3 参与方别只盯着“客户”资源背后的人和组织都要记参与方指与资源流动有关的人或者组织常见有客户、供应商、仓储方、员工、系统马甲账户。如果业务中有分账、代理、分销、内外部协作参与方往往需要额外扩展比如要区分“发起方”和“执行方”“受益方”。为什么参与方建模经常被人忽略因为我们习惯把“用户ID”简单存进订单表。但 rea 会逼你想清楚一件很细节的事同一个事件里谁是从哪边流动出来的谁接收到了谁审批过谁执行了。一个支付事件实际包含“付款人”“收款人”“支付渠道”“发起设备”等多重身份如果数据库里只留一个 user_id后续做收入归属、纠纷溯源、审计合规时只能痛苦地翻日志。所以在我设计的模型里参与方不会塞在事件表里拉一条字符串而是作为独立实体存在事件通过外键和它建立关系。只要参与方的组织或身份信息有变化也不需要去改历史事件非常清爽。3. 从订单到库存用 rea 重构一次完整业务流程有了基础概念我拿一个最典型的电商库存场景完整走一遍。这套流程是我在某分布式库存项目里的复盘记录流程本身具有通用性照着画完全可以复现。3.1 第一步把业务流程看成一条事件链而不是状态转移最忌讳的事情是先在白板上画“订单状态机待支付 → 已支付 → 待发货”。正确做法是从业务动作出发列出一条可验证的事件链。以“用户在商城下单一瓶洗发水”为例事件链大致是用户下单产生“订单创建”事件。用户支付产生“支付成功”事件。系统锁库存产生“库存预留”事件将“可售库存”转换为“承诺资源”。仓库发货产生“出库”事件将承诺资源转为已发出资源。客户签收产生“收货确认”事件完成最终资源流动。注意这时候还没有“订单状态”这个字段。你可以随时通过事件串联计算出某个订单处于哪个阶段支付事件发生了但出库事件没发生它自然就是“待发货”。这样做的好处是商品库存里的“可售数量”也不再由一个数字决定而是可以通过事件实时推导出来可售数量 采购入库累计 - 销售出库累计 - 库存预留累计。每个业务人员看到这个公式都会觉得很舒服因为它的每一步都能追溯到某张单据而不是数据库里拍脑袋改出来的 alter 语句。3.2 第二步设计资源、事件、参与方的表结构我不打算堆一套复杂到没法抄的 SQL而是给出一个最精简的三层结构资源表字段说明resource_id资源唯一编号resource_type商品、现金、承诺库存、服务工时等owner_id资源当前归属方unit计量单位current_status可选用于加速查询来源仍由事件决定事件表字段说明event_id事件唯一编号event_type采购入库、订单创建、库存预留、出库、支付等resource_id受影响的资源inflow_agent_id流入参与方outflow_agent_id流出参与方quantity数量正负号可约定created_at创建时间参与方表字段说明agent_id参与方唯一编号agent_type个人、公司、部门、系统账户name名称这套结构初看不起眼但它有一个非常大的优势业务的每个事实在数据库里都有且仅有一行事件记录对账时不需要 join 七八张业务表只需要查事件表就能把账算平。3.3 第三步用同一套事件同时服务前端展示和后台对账我特别喜欢 rea 的一点是同一张事件表能同时喂饱两条不同的数据链路。前端页面要展示“订单详情”我只需要通过订单号过滤事件再按类型聚合出当前状态、金额、数量后台财务要出报表也是通过事件表按参与方、时间区间、资源类型进行汇总。因为两者底层是同一套不可变事件天然就是一比一平账的状态不再有“前台订单金额和财务报表对不上”的问题。具体落地时我通常会再给事件表加一个冗余字段 event_snapshot存一份“事件发生时的完整业务上下文快照”比如当时的商品名称、单价、收货地址。这个字段大大减少了以后因为商品改名、订单被改而查不到历史的问题。快照本质上是空间换时间非常值得。4. 避坑清单rea 实际落地时会和传统思维正面撞车再好的理论落到代码里都会遇上一堆想不到的问题。我把自己在项目里踩过的坑整理成了几条每一条都希望能帮后来人省点时间。4.1 事件表不能做成“可改的账本”有人觉得我把事件表设计得太死板客户想改收货地址不就应该 UPDATE 订单吗事件不动那地址存哪我的答案是与交易结果直接相关的属性放进事件不可改可以改的辅助信息放在参与者或配套资料表里通过业务编号关联。比如“收货地址”本质上是商品流向的投递目标可以放在订单扩展信息里更新但“金额”“数量”“审批结果”这类会直接影响资金和库存的属性只能通过追加更正事件来解决。这个习惯一开始会让人别扭尤其是团队成员习惯了 UPDATE 一切的时候。但只要坚持几周大家都会感受到好处排查线上问题的时候事件链就是一条完整的“监控录像”而不是几张被反复涂改过的“便签纸”。4.2 并发和幂等两个“同一事件”只能进去一次rea 的一个大前提是事件记录必须是幂等且唯一的。但在分布式系统里支付回调、库存回补很容易因为网络重试被触发两次导致库存被重复加回。我的处理办法并不复杂给每个事件一个全局唯一幂等键这个键可以由业务单据号加动作类型组合产生比如 order_id “warehouse_outbound”。写入事件表前先按幂等键查重唯一索引兜底。一旦发现重复触发直接返回已成功而非再写一条事件。这套逻辑单说很简单但真正实施时一定要落到数据库约束上不能只靠应用层 if 判断。应用层判断面对两个并发请求同时进来时拦不住问题的。4.3 别把“资源余额”硬实时算到生产主链路里我见过一些人把 rea 理解成“每次查询库存都要把全年事件 SUM 一遍”结果上线第一天数据库查询就扛不住。纯事件推导、无任何冗余的架构适合小数据量内部系统生产环境还是需要有余额快照、物化视图或周期汇总额外辅助。我项目中采用的做法是事件表负责记录真实事实冗余库存表或余额表负责承载实时查询。平时通过消费事件流将事件聚合结果写入冗余余额表。如果余额表异常可以再从事件表重新回放一次这本质上是一种“事件溯源 读模型”的组合。这样做既保住了 rea 的可靠性和审计性又不会牺牲页面性能。4.4 外部系统对账不能只靠事件重放假设你在对接支付渠道渠道侧会出现“支付成功但回调长期中断”的情况。此时事件表里没有回调记录本地请求却已经发出去了。这种情况下靠本地事件表推导并不保险必须依赖外部渠道的主动对账接口。我会给 event 表增加一个 reconciliation_status 字段作为对外部状态差异的标记。每天跑定时任务拿出 “已发送但未确认” 的事件批次调用渠道查询接口核对结果根据差异自动补齐或告警。这算是 rea 之外的工程兜底但也说明任何模型都不能替代基础的对账机制。5. 落地路线图如果现在让我从零接一个 rea 项目我会按这个顺序走如果你也想尝试用 rea 重构已有系统我的建议是不要一上来就推倒重来。更稳妥的路线是把现有系统当作“黑盒子”在它的旁边平行建立一层事件记录。5.1 第一阶段先打地基只记不改第一阶段的目标非常简单在所有关键业务入口比如创建订单、支付回调、库存变更、退款审批处平行记录一条事件日志。哪怕这一步没有改变任何现存代码逻辑也不算白做因为你已经有了一本可供审计的账本。具体实现上我习惯写一个轻量级的事件中间件放在服务层或 MQ 消费者里往事件表异步写入原始凭证。这个阶段不要去调整历史数据只对增量数据生效。两周后你就会积累一批真实事件这比任何专家理论都更能帮助你设计后续模型。5.2 第二阶段扩展出资源和参与方视图事件跑通后再建立资源表和参与方表逐步把库存、账户等核心资源的当前数量迁移为“由事件表聚合计算出的结果”。这里要特别留意存量余额的初始化。我会在事件表里插入一条“期初初始化”事件把种子数据统一放进事件流而不是直接写死余额表。之后所有余额变化都从这个种子事件开始延伸。这样做的好处是任何时候你都能回答“当前余额从何而来”而不是只看到一串数字。5.3 第三阶段把部分读接口切到“事件推导 快照缓存”等到新模型成熟后再把前端订单列表、库存查询等只读接口逐渐切换到新数据链路。切换时先做灰度对比新旧两套接口结果是否一致。由于事件表天然包含完整历史排错会非常快。最后等系统和团队都适应了再逐步把写流程从原来的状态机模型迁移为“追加事件 投影状态”的方式。到这个阶段你已经不是“用 rea 模式”而是在跑一套成熟的领域事件流水账了。6. 根据实际经验需要补充的几个细节结尾再写点没什么理论味道但实战很关键的细节。如果你已经准备动手下面这些小技巧能帮你少踩雷。第一给事件表加“事件类型字典”很必要。事件类型不是一张自由填写的文本标签否则积累一段时间后会出现 “pay_success”“支付成功”“PAY_SUCC” 三种写法。建议专门维护一张事件字典表代码里所有事件类型都走常量或配置中心不允许直接硬编码字符串。第二同一事件在业务链路上可能会触发多个资源变动。比如“订单完成”既让库存减少又让账户收入增加。建议事件表主记录一条核心事实同时加一张事件资源扩展表来承载一个事件关联多个资源、多个参与方的情况。这种一对多的设计能让你处理复杂业务时不至于被迫插入多余的“伪事件”。第三不要试图第一版就把所有统计报表切换到事件表。这是个很实际的教训我第一次推进 rea 时想一步到位把所有经营报表全部迁移结果各种口径差异让团队炸了好几周。更好的做法是先在 2-3 个关键模块验证比如库存流水和订单收入这两个最敏感的地方等稳定后再慢慢扩大范围。第四初始化事件需要一个傻白甜的约定。任何 sys 表都可能因为重复导数据被污染我采用的保险做法是给 seed 事件一个超级大的负数 event_id并且附上独有的备注字段后续校验时能被一眼识别出来。多花一分钟防一手能避免后期对账时对着几万条初始化数据发愁。第五要留好“时间戳语义”的坑。事件发生时的时间戳和事件写入数据库的时间戳不是一个概念。分布式架构下用户支付发生在 23:59:59事件落库却可能是第二天 00:00:01如果你直接拿数据库入库时间做财务日切每个月都会对不平。我的经验是事件表里两个时间都存一个 occurred_at 存业务发生时间一个 recorded_at 存事件写入时间所有账务逻辑一律以业务发生时间为准。把我个人从这件事里学到的最核心的一句话总结给你建模时不先问“对象有什么状态”而是问“发生了什么事实、谁和谁产生了一次资源交换”很多陈年顽疾会自己消失。如果你现在正对着那些反复 UPDATE、状态满天飞的旧代码头疼不妨挑一个业务模块先按事件链的方式建一张账跑上两周再回来看整张业务图视角会很不一样。
阅读完成 · 觉得有帮助?
咨询建站