REA建模:用资源、事件、参与者替代状态机,重塑订单与库存系统 最近帮某电商团队做架构评审看到他们把核心订单表设计成了十几个状态字段从待支付、已支付、待发货一路枚举到已取消、已退款然后每加一种营销玩法就往表里塞一列。我随口问了一句“如果有一天想统计‘哪个运营同学在什么时间因为什么活动把什么商品卖给了什么人’这张表能回答吗”对面沉默了。这正是我开始反复研究 REA 建模的原因。REA 的全称是 Resource、Event、Agent也就是资源、事件、参与者。它不是什么新框架也不是某个中间件而是一套用来描述业务领域的建模思想最早源于会计信息系统研究后来被大量用在企业应用、微服务拆分和事件溯源架构里。简单说它把业务系统从“记录当前状态”的思维扭转到“记录业务事实”的思维。这篇文章我会用订单、库存、资金这类最常见业务场景把 REA 的核心概念、建模步骤、表结构设计和落地避坑一次性讲透适合正在做领域建模、微服务拆分或者被状态机表和状态字段折磨过的后端开发同学参考。1. REA 到底在讲什么1.1 先忘掉订单表想一下业务流程本身很多系统设计的起点是“我要一张订单表”然后顺着这张表派生出支付表、发货表、售后表。这种思路不是不行而是容易被业务文档牵着鼻子走业务方说“订单有三种状态”你就建三个状态业务方说“订单可能被拆分”你又加一个拆分表。到最后表结构成了业务流程的复印件业务规则一变表结构就得跟着改。REA 换了一个切入角度不要先问“我现在需要存哪些数据”而是先问“业务发生的时候到底发生了什么事实”。举个例子用户买东西这件事在传统表结构里体现为“订单表插入一条状态为已支付的记录”在 REA 里则体现为一个链式事实某用户Agent在某时刻Time执行了支付Event导致资金从用户账户流入平台账户Resource 转移。这个视角的转变很关键。前者记录的是“现在是什么样”后者记录的是“过去发生了什么”而且通过事件之间的资源流动关系可以随时推导出“现在是什么样”。1.2 三个核心词汇资源、事件、参与者REA 的三个核心概念用生活场景类比特别好理解。第一个叫资源Resource。资源是业务中有价值的东西可以是实物也可以是虚拟物比如商品、库存、现金、积分、优惠券、物流服务。资源的特征是可被计量、可被追踪、有价值。就像冰箱里的食材你能数得清有多少也知道它们被做成了什么菜。第二个叫事件Event。事件是业务世界里发生过的一次动作是你系统需要记录的最核心事实。继续用厨房类比洗菜、切菜、炒菜、盛菜都是事件。在电商场景里下单、支付、发货、签收、退款都是事件。REA 强调一条硬规矩资源不会凭空消失或出现任何资源的进出都必然由某个事件触发。第三个叫参与者Agent。参与者是执行或参与事件的人或组织可以是用户、客服、运营人员也可以是供应商、物流公司甚至可以是一个自动化系统。参与者回答的是“谁干的”这个问题。对应到厨房类比里就是谁做的这道菜。为什么这三个词就够了因为任何业务事实都能用“谁Agent在什么时候Time做了什么Event导致什么资源Resource发生了什么变化”这句话来概括。当你发现某个业务信息没法装进这三类时通常说明它要么是描述性属性要么你们的业务边界还没理清。1.3 REA 最核心的一条结构规律REA 模式不只是三个概念堆在一起它还有一条非常重要的结构规律任何一个经济事件必然涉及“资源流入”和“资源流出”两组关系也就是所谓的 dual effect双向效果。做账的人听到这里应该会很眼熟这不就是复式记账里“有借必有贷借贷必相等”吗没错REA 的学术根基就在会计学。举个例子销售发货这个事件在 REA 里一定是这样的结构商品资源流出库存减少同时现金类资源流入应收增加或现金增加。如果只记录库存减少没有记录资金流入这个事件就是残缺的。不要让系统里出现“只减库存但不产生应收”的发货动作一旦出现往往意味着业务流程有漏洞或者建模有遗漏。这条双向规律还有一个衍生结构叫关联链一个事件依赖另一个事件。比如“支付”依赖“下单”“发货”依赖“支付”这些事件之间的时序关系天然构成了业务链路。传统表结构用状态字段来表达这种链路而 REA 用事件之间的关系来表达两种表达方式的扩展性完全不同。2. 用 REA 重新建模一次完整交易2.1 先从业务规则说起我习惯用一个固定场景某用户在某商城购买一件商品从下单到支付再到发货、签收最后系统完成收入确认。这个场景麻雀虽小却能覆盖资源、事件、参与者之间的绝大多数关系。建模之前先把业务规则写清楚不要直接跳到表结构。业务规则假设如下用户浏览商品后提交订单订单包含一件商品及数量用户付款后平台通知仓库发货仓库发货后库存减少同时产生一笔应收用户签收后这笔应收转为确认收入如果用户申请退款则资金从平台退回用户商品不需要退回按虚拟商品处理。先别急着设计订单表、流水表我们按照 REA 的步骤走一遍你会发现最终的表结构跟传统设计不一样但业务语义更完整。2.2 一步步拆出资源、事件和参与者第一步是找参与者。这个场景里有用户、运营/客服、仓库管理员可能还有自动化的库存系统。参与者很好找只要问一句谁发起了这个动作谁对这个动作负责把每个能独立承担行为的主体都列出来。第二步是找资源。资源要有价值且可计量。这里能抽出来的资源包括商品类目下的实际商品或者更准确的发售库存单元、用户钱包里的余额、平台账户中的资金、应收债权、确认后的销售收入。很多人会把“订单”也当成资源这是不对的。订单是多个事件和关系的聚合文档它不是资源资源是从这个订单里流入流出的那些有价值的东西。第三步也是最核心的一步找事件。把业务规则里的每个动词抽出来提交订单、支付、发货、签收、确认收入、退款。这些就是事件。注意我们这里说的“状态”不是事件“已支付”这个状态只是“支付成功事件发生之后的结果”系统里应该记录的是支付成功这个事实而不是把订单的状态字段翻成已支付。第四步是画事件之间的依赖关系。支付事件依赖提交订单事件发货事件依赖支付事件签收依赖发货确认收入依赖签收。这些依赖关系构成了整个业务的生命周期。看到没有这就是一个天然的事件流图谱跟状态机相比它没有硬编码“订单状态”这个字段而是用事实之间的关系来表达流程。第五步是把事件和资源关联起来。支付事件导致用户余额减少、平台账户余额增加发货事件导致库存资源减少、应收债权增加签收事件导致应收债权转为确认收入。每一条资源流动都是双向成对出现的能对上账。2.3 建模结果长什么样经过上面五步这个场景的 REA 模型可以抽象为下面这张事实关系网参与者包括用户、仓库管理员、系统自动化流程。资源包括库存商品、用户余额、平台资金、应收债权、销售收入。事件包括提交订单、支付成功、发货出库、签收完成、收入确认、退款完成。事件之间的依赖链路是提交订单 → 支付成功 → 发货出库 → 签收完成 → 收入确认。退款完成是一个独立旁路事件它消耗资金资源但需要参考支付成功事件来确认退款金额和退款对象。每条事件与资源的关联都是成对出现的支付事件使“用户余额减少 平台资金增加”发货事件使“库存商品减少 应收债权增加”签收事件使“应收债权减少 销售收入增加”。这就是 REA 模型的核心产出物。它不叫订单表也不叫状态机而是一张“业务事实网络”。后面做数据库设计、做微服务甚至做数据审计都只需要围绕这张网络展开。3. 把 REA 模型落到工程代码和表结构里3.1 物理表怎么设计建模完成后要落库。这里要注意REA 是一种逻辑建模思想不是强制的物理表模板。你可以用关系数据库实现用事件表加资源表的方式实现也可以采用事件溯源方式实现。先说最常见的关系库做法。资源表往往拆成两类一类是相对静态的基础资源比如商品资源表记录 SKU、名称、规格另一类是动态的余额/库存资源比如账户余额表、库存存量表。这两类资源的区别在于是否有“当前存量”的概念。库存存量表要回答“现在还有多少”账户余额表要回答“现在还有多少钱”所以它们都需要实时数值字段。事件表是 REA 实现的灵魂。一个事件不仅是一行日志它要能完整还原业务事实。我给出一个通用的事件表核心结构省略租户、软删除等公共字段CREATE TABLE business_event ( id BIGINT PRIMARY KEY, event_type VARCHAR(64) NOT NULL, occurred_at DATETIME NOT NULL, agent_id BIGINT NOT NULL, agent_type VARCHAR(32) NOT NULL, trace_id VARCHAR(64), payload JSON );这个表只记录“发生过什么”不直接记录“变成了什么状态”。agent_id 和 agent_type 用来定位参与者occurred_at 是业务发生时间不是系统入库时间。payload 用来保存事件发生时的快照信息比如下单事件可以存当时的商品快照、价格快照。接下来是事件与资源的关联明细表。这张表是 REA 落到物理实现的桥梁CREATE TABLE event_resource_line ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, resource_type VARCHAR(32) NOT NULL, resource_id BIGINT NOT NULL, direction VARCHAR(16) NOT NULL, quantity DECIMAL(18, 4) NOT NULL, unit_amount DECIMAL(18, 4), amount DECIMAL(18, 4) );direction 字段用来标记资源流入还是流出配合事件表的 event_type就能完整还原“这个事件让哪些资源发生了什么变化”。例如支付成功事件对应两行一行是用户余额方向为流出一行是平台资金方向为流入。这两行记录共用同一个 event_id这就是双向结构在物理表里的直接体现。3.2 资源和事件谁先落库事务边界问题工程上最容易犯的错是事件表和资源表分开写中间没有事务保护导致库存扣了但事件没记录或者事件记录了但库存没扣。正确做法是在同一个数据库事务里同时写入事件表和资源变动明细表并且以事件表为主记录资源更新作为伴随操作。举个例子扣减库存时先插入一条“发货出库”事件再插入两条 event_resource_line一条指向库存资源且方向为流出一条指向应收债权且方向为流入最后更新库存存量表的 current_quantity。这三步必须在一个事务里完成。如果中途失败全部回滚保证“有事件必有资源变动有资源变动必有事件”。这里有一个实用的设计习惯任何直接修改资源表数值的代码都应该被视为违规操作。所有数值变动必须由事件驱动代码里不允许出现“直接调用 update inventory set quantity quantity - 1”这种语句。这样的约束一开始会让开发觉得啰嗦但它能防止后期出现大量对不上账的脏数据。我在多个项目里实测过这个约束能省掉无数次对账排查的加班。3.3 REA 与事件溯源天然契合关系库方案能跑但最舒服的落地方式其实是事件溯源Event Sourcing。事件溯源的本质是不以当前状态作为系统主数据而是把一系列不可变事件作为事实来源状态只是事件的投影。REA 里的每个事件天然就是事件溯源里的一个追加日志。Resource 是投影结果Agent 是事件元数据的一部分Event 就是日志本身。当你收到一个支付事件后通过重放所有相关事件可以重建出用户余额、库存存量、收入确认数。不需要在建表时考虑状态字段也不用担心状态被并发覆盖。事件溯源落地时要考虑查询性能所以一般会配合 CQRS单独建一套读取模型表。比如“订单查询模型表”它就是通过订阅事件流把订单当前状态聚合出来。写侧是 REA 事件流读侧是普通的冗余表。这套组合拳在多人协作的中大型业务系统里非常好用因为它把写模型的复杂性集中在“事实”上把读模型做成可以灵活裁剪的视图。3.4 微服务边界怎么划分REA 也能给微服务拆分提供非常清晰的方向。很多人拆服务是照着功能模块拆的比如搞个订单服务、搞个支付服务、搞个库存服务。但 REA 的启发是围绕“资源聚落”和“事件链”来划边界。一个服务应该负责一组紧密关联的资源以及作用于这些资源的事件。比如库存服务负责库存资源和对库存的入库、出库、盘点事件资金服务负责用户余额、平台资金和支付、退款事件订单服务负责订单文档、应收债权和下单、签收事件。每个服务内部的表结构遵循 REA 规范服务之间通过事件传递事实。这样做的好处是服务之间不再频繁互相调用查询接口来判断“订单支付了没”而是各自维护事件流通过事件消息同步。库存服务接收到支付成功事件后就知道可以发货了资金服务接收到退款事件后自动执行资金转出。这种模式把系统从“请求-响应”的耦合改成了“事实-反应”的联动扩展性明显更好。当然不是所有系统都适合微服务但如果团队决定走微服务REA 是比按功能模块切更不容易踩坑的划界方式。4. 常见问题与坑点实录4.1 把事件活生生做成了状态机这是新手最容易掉进去的坑。业务方说订单有下单、支付、发货、签收四个状态有人就直接建一张订单状态表然后用事件表记录状态的流转变化。表面看好像也用了事件比如状态变更表但本质上还是在描述状态不是在记录事实。怎么分辨呢看你的事件记录有没有回答“导致这个事实发生的资源流动是什么”。如果一条“支付成功”记录下面没有挂着资金资源的流入流出那它只是一次状态变更日志不是 REA 意义上的事件。要修正也很简单把状态流转的思考放到一边重新问自己这笔业务的资源到底发生了什么变化。想清楚这个问题事件表自然就找齐了资源流。4.2 库存和余额的当前值到底要不要存有一种极端做法是只存事件流不存任何当前资源数值每次查询都从零重放所有事件。这在理论上成立但实际查询性能和实现复杂度都很高尤其是库存、余额这类高频访问的资源每次都重放根本不现实。我的建议是双写既保留事件明细也保留资源当前值表。事件是事实依据资源表是便于查询的投影。关键是资源表的值必须通过事件来更新不能绕过事件直接改。这相当于给系统加了一条纪律开发同学执行起来会不习惯但一旦形成约束后续排查对不上账的问题会轻松非常多。曾经有个业务线因为后台一键修复功能直接改了余额导致好几笔账对不上最后花了两周反查日志。后来我把后门改成“插入一笔调整事件 系统自动改余额”再也没出过这类事故。4.3 多对多关联越来越多查询越来越慢REA 建模跑起来后事件与资源的关系都是多对多event_resource_line 表的数据量会涨得飞快。很多同学一开始不在意等到几千万行的时候join 查询开始变慢就开始怀疑 REA 模式有问题。其实这不是 REA 的锅是查询设计问题。读性能应该交给读取模型而不是直接查事件明细表。正确做法是事件明细表只服务写侧和审计所有线上查询走 CQRS 的读模型。比如查询订单当前状态直接查订单读取模型表这张表已经冗余了状态字段根本不需要 join 事件表。成本是用异步处理器或者消息订阅来维护读模型收益是查询干净、快速而且不同查询场景可以建不同的读模型表各自进化。4.4 什么场景不适合硬套 REA如果只是一张内部管理后台的配置表或者一个给运维用的一次性脚本硬上 REA 就是把简单问题复杂化。REA 的价值在于记录高价值的业务事实常见于电商交易、金融账户、供应链、预订类系统这些场景对账复杂、生命周期长、审计要求高。简单 CRUD 应用直接改字段仍然是最高效的方案。选择标准可以这样判断这个业务是否需要回答“谁在什么时候因为什么让什么资源发生了什么变化”是否经常需要对已发生的事实做追溯、审计、重算如果两个都否那传统状态表够用了如果两个都是那用 REA 模型长期来看收益远大于前期投入。常见症状实际原因处理方式事件表只是状态变更日志没建模资源流动回到流程给每个事件补资源流入流出关系出现账户余额直接修数资源表缺少事件约束所有资源变更必须经由事件事务封死后门查询事件表 join 过多读模型缺失引入 CQRS单独建查询投影表库存扣减对不上账事件和资源更新不在同一事务事件插入与资源更新放同一事务内原子提交服务间状态靠互查接口判断边界划分错误服务之间改走事件消息共享事实流简单后台管理也想用 REA过度设计评估业务是否真的需要事件追溯不需要则保持简洁5. 我的一点实操体会如果你问我去掉所有理论后 REA 到底教会了我什么我的答案是三句话第一先找事件不要先画 ER 图第二资源和参与者都可以后补但事件漏了后面很难补第三所谓状态只是事实投影出来的影子。之前在某供应链系统里做一个入库出库模块团队一开始用传统库存流水表设计字段越加越多还要写一堆触发器来保证一致性。后来重新按 REA 梳理把入库、出库、盘点、报废全部作为事件把商品、货位、批次作为资源整个逻辑瞬间清晰了连仓储同事都能指着事件流跟我说“这一步少了一个损耗事件”。能让业务人员无障碍理解技术模型这是我在其他建模方法里很少见到的体验。如果这篇文章给你一个启发建议你从手头最熟悉的业务场景开始练手。拿一张白纸把你日常处理的某个业务流程从“状态”思维切换成“事实”思维列出事件、资源、参与者再看看原来的表结构哪里设计别扭了。不用急着改代码光是这个思考换框的过程就足够帮你发现自己系统里很多隐藏的耦合和遗漏。