先聊个现象我见过太多企业系统从订单到库存到财务每个模块都是独立的几张表各管各的联查全靠接口硬拼。上线头半年还好等业务一变哪里都透风。后来我回头看项目里最稳的那套核心系统发现它无意中长成了“REA”的样子——不是刻意用的框架而是三个基本实体摆出来之后整个模型自己就顺了。REAResource-Event-Agent资源-事件-参与者不是新概念很多搞财务信息系统的人早就在用但放到今天做通用业务系统、中台设计、甚至领域建模它依然是一把很少被提起的利器。这篇文章我想把“怎么把一个混乱的业务描述变成清晰的REA模型”这件事讲透。它会包括三实体到底是什么、为什么能替代传统借贷记账的思路、建模时容易犯的五个错以及一套可以直接抄作业的从订单到库存的建模演练。适合谁看正在设计交易类、库存类、账务类系统的架构师和资深后端以及被复杂业务规则折磨、想找一个稳定建模框架的产品和技术负责人。不是教科书是实操之后回头总结的那种笔记。1. REA模型的核心三实体如何让业务描述回归本质1.1 Resource、Event、Agent 到底是什么REA 最早是会计信息系统领域提出的概念它的核心诉求是不要以“借”“贷”这种记账符号去理解业务而是用业务的本来面目去记录业务。三个实体分别是Resource资源业务中涉及的有价值的东西比如现金、库存商品、积分、服务工时。资源的特征是“有价值、可量化、能被交换或消耗”。Event事件业务过程中实际发生的、改变资源状态的行为比如销售出库、采购入库、发放积分、支付货款。Agent参与者参与事件的人或组织比如客户、供应商、内部员工、门店甚至系统里的自动化程序。三者的关系也很固定每个事件至少关联两个参与者事件会流入或流出资源多个事件通过资源构成一个完整的经济链。这个“至少两个参与者”是REA非常关键的地方后面建模时会反复碰到。如果你做过订单系统看到这三者的第一反应可能是“这不就是订单头、订单行、客户表、商品表嘛。” 表面上像但REA的抽象粒度比传统订单模型要深。传统订单模型把“下单”和“发货”塞进一个订单实体里REA则把它们拆成两个事件各自记录自己的资源流向。这样拆的好处在你需要回答“哪些订单发了货但没收款”时立刻体现出来。1.2 为什么传统借贷记账思路不再够用老一套财务系统喜欢用凭证表、科目表每笔业务都要写成借和贷。这个模式对财务报告很友好但对业务系统非常不友好。原因在于一个业务事件往往要拆成多笔借贷记录而借贷记录丢失了事件本身的语义。比如一笔销售订单财务上要记应收账款增加、库存减少、收入增加拆完以后“这批货是哪个客户买的、谁审批的、用的哪次促销”全部被抹掉了。事后想还原只能靠额外的流水号硬关联。REA的立场是先把业务事件完整记录下来借贷和科目只是视图层面根据事件推导出来的结果。也就是说REA不是取代财务记账而是把记账变成一种可推导的投影。一旦底层模型是REA出应收报表、出库存台账、出收入确认表都是从一个业务事件日志上做不同角度的聚合而不是维护一堆容易对不上的科目余额。这个思路放到今天其实很超前。你想想事件驱动架构、事件溯源这些东西为什么这几年火本质上和REA是一致的事实不可变状态靠推导。REA只是把这个原则用到了业务建模层面而已。2. 建模前的关键决策别急着画表先想清楚这两件事2.1 系统究竟在记录“事件”还是在记录“状态”这是建REA模型时最核心的一道分水岭。很多团队建模第一反应是把现状拍下来商品有一个库存数量字段客户有一个余额字段订单有一个状态字段。这就是典型的“状态建模”——系统里存的是某个时刻长什么样。问题在于一旦并发发生两个操作同时改同一个状态字段谁先谁后说不清对账依赖最后一次更新数据回滚基本靠手工补偿。REA要求你先忍住不建状态表而是问一句这个状态是从哪个事件推导出来的库存数量不该是商品表上的一个字段而应该是“入库事件”累计流入量减去“出库事件”累计流出量的结果。客户余额不该是客户表上的字段而应该是“充值事件”流入总额减去“消费事件”流出总额的净值。这个转变是整个建模过程中最需要克制力的一步也是最容易走偏的一步。实际操作中我通常用一个检查方法来判断是否跑偏如果某个字段会被多条业务链路更新那它八成应该是一个可以推导的派生值而不是存储值。比如“订单已付金额”可能被支付回调更新、被退款更新、被人工调整更新你把它存成字段每多一个更新入口就多一分出错风险。把它改成“支付事件汇总调整事件汇总”所有更新都变成插入事件逻辑就单一了。2.2 业务边界的范围一个REA模型覆盖多大领域REA模型可以很大从采购到销售到人力全放进去也可以很小只覆盖一个库存域。但建模失败往往不是模型本身错而是边界圈错了。边界的判断标准是同一个经济链上的事件尽量放一起孤立的资源单独成域。举个例子。一个订单闭环包括客户下单销售事件、仓库发货出库事件、客户付款收款事件、财务确认收入收入事件。这四个事件共享一个资源链商品、现金。它们应该在一个模型里不能拆成订单子系统管前半截、财务系统管后半截否则永远要对账永远对不平。另一个典型的边界错误把物流轨迹事件放进库存域。物流轨迹和库存资源没有直接资源流关系它只是订单执行过程的辅助信息。硬放进REA模型里会让模型变成一个大杂烩。我的做法是物流状态单独建领域表通过事件ID和REA模型做弱关联不参与资源推导。分清哪些是核心经济事件、哪些是辅助状态是建模之前最值得花时间的判断。3. 实操演练从订单到库存的完整建模过程3.1 第一步列事件清单而不是画ER图很多团队的建模会议一开始就打开画图工具开始画表和关系这是最大的错误。REA建模的正确起点是列事件而且是用业务语言列不是用技术语言列。以一套最简单的B2C下单库存场景为例业务描述可能如下客户在前台下单系统锁定库存。仓库发货库存实际减少。客户付款资金入账。客户申请退款资金退回。采购入库库存增加。从这里提取事件清单事件流出资源流入资源主要参与者下单锁定商品锁定态-客户、系统发货出库商品可用态-仓库员、订单支付收款-现金客户、商户退款现金-商户、客户采购入库-商品供应商、仓库员注意“下单锁定”和“发货出库”是两个事件而不是一个。原因在于它们发生的时间点不同资源状态也不同。如果把两个事件合并你就没法回答“已锁定但未发货的货有多少”这个问题。这个拆分粒度是REA建模的难点也是价值所在。事件清单列完之后再检查一个约束每个事件是否至少有两个参与者。如果某个事件只有一个参与者通常意味着你漏了内部参与者或者把系统当成了参与者但没明确出来。比如“发货出库”常见建模错误是只记录仓库员其实订单或对应的客户也是参与者因为发货是针对订单发生的没有订单就没有发货。3.2 第二步定义资源及资源流向资源是事件之间的“货币”定义资源时要做三件事明确资源类型、明确资源状态、明确计量单位。资源类型分为存量型和流量型。商品库存是存量型它在某个时间点有一个余额销售收入是流量型它只在一段时间内有汇总意义。建模时要注意同一类资源在不同事件中状态不同比如商品在“锁定”状态下和“在库”状态下虽然资源类型一样但业务意义完全不同。比较实用的做法是给资源增加一个“状态维度”而不是拆成两个资源类型。比如商品资源的键是“商品ID库存状态”其中库存状态包含在库、锁定、在途、残次。这样设计后所有库存操作本质上是把一个状态的资源通过事件转成另一个状态的资源——出库事件的结果是“在库态商品”减少“已售态商品”增加。这个思路让库存模型变成一个状态机会而不是散落的一组数字。计量单位方面最简单的错是把“个”和“箱”混在一个字段里。REA要求每个事件记录资源流量时必须带计量单位ID不能默认全局统一。因为一旦业务扩展出“按箱采购、按个销售”的场景字段设计不预留单位就会变成一场噩梦。3.3 第三步落成数据模型直接可用的表结构参考事件清单和资源确定后才可以开始设计表。这里提供一个我在类似模拟项目X里验证过的、可复用的基础表结构。事件表的核心设计是尽量通用用单一事件表承载大多数业务事件。字段不需要完全固定但骨架是可复用的事件表 tb_event - event_id 事件ID主键 - event_type 事件类型order_lock / shipment / payment / refund / stock_in - event_no 业务单号下单号、出库单号等由业务生成 - occurred_at 发生时间业务时间不是入库时间 - agent_buyer_id 参与者-买方ID客户、供应商等 - agent_seller_id 参与者-卖方ID商户、仓库等 - source_event_id 来源事件ID比如支付事件关联到订单事件 - extra_data JSONB存放各事件特有属性资源流表记录每个事件对资源的影响。单独拆出来的理由是一个事件可能影响多个资源例如发货同时影响库存和履约状态也支持同一事件多次资源变动资源流表 tb_resource_flow - flow_id 流水ID主键 - event_id 关联事件ID - resource_type 资源类型商品、现金、积分 - resource_id 资源标识商品SKU、现金账户ID - resource_status 资源状态在库、锁定、在途、已售 - uom_id 计量单位ID - quantity_delta 数量变化流入为正流出为负 - unit_value 单位金额可选用于金额资源的聚合参与者表按实际需要拆客户、供应商、员工表但都保留统一的agent_id作为跨域关联键。不要用同一个表存所有参与者因为客户和供应商的属性差异太大强行归一化只会让表变成大宽表。这个结构的核心逻辑是你永远不需要UPDATE资源余额字段只需要INSERT事件和资源流。需要查库存时对资源流表做SUM聚合需要查客户余额时对现金资源流做SUM聚合。这带来的附加好处是数据天然带时间轴任何时点的快照都可以通过“统计某个时间之前的累计流量”重建。3.4 对应的关键接口设计表结构定了接口设计就顺了。两个最核心的接口提交事件接收事件类型、资源变更清单、参与者信息在一个数据库事务里插入事件表和资源流表。查询资源余额输入资源类型、资源标识、时间范围聚合资源流表并返回净变化。这里有一个我在实际项目中踩过的坑提交事件接口必须做幂等。因为客户端重试、MQ重投同一个业务事件可能被提交两次。我的解法是在事件表上建一个biz_key唯一索引用业务单号加事件类型的组合做幂等键。第二次提交遇到唯一键冲突时直接返回已有事件ID不做任何重复插入。这个设计的优先级比加缓存防腐层更高数据库唯一索引永远是幂等最可靠的兜底。4. 常见问题与排查技巧实录4.1 五个让REA模型变形的建模错误这个模型看着简单但落地产出了很多共性错误。我把它们集中列出来每条都标注了表现、原因和修正方式。错误表现根因修正思路把订单头当成事件表所有操作都挂在订单上混淆业务单据与业务事件单据是事件的聚合载体不是事件本身。订单状态由多个事件推导不单独存储某个事件只关联一个参与者漏掉系统内部参与者或业务隐含参与者回到业务描述找出谁发起、谁接收、谁审批参与者至少两个资源表直接存余额字段走回状态建模老路余额改为资源流汇总字段用视图物化或查询时聚合库存扣减和订单锁定混在一个事件事件粒度太粗按时间点和业务影响拆开锁定事件只改变锁定状态出库事件才扣减可用库存事件表加了一堆专用字段把事件独有的属性全塞进通用表通用字段只放所有事件共享的属性独有属性放附加事件明细表这些错误我在不同团队Review时反复看到。坦率说第一次接触REA建模的团队前几次都会踩其中一两个正常。真正重要的不是背下来而是建完模以后做一个“回放验证”“如果要回答‘上周五下午三点的库存是多少’系统能不能算出来” 如果答案是不能那说明某个状态被存储而不是被推导了模型里有漏事件或错事件。4.2 排查“对不上账”问题的通用方法REA模型上线最典型的症状不是崩而是和旧系统的历史数据对不上。排查顺序很重要第一先核对事件表和资源流表的数量关系。一个事件必须至少对应一条资源流如果存在没有资源流的事件那这个事件是空操作往往说明资源流漏建了。第二核对资源流的方向。我自己的排查脚本里会做一个方向合法性检查——哪些事件类型允许哪些资源方向比如销售事件不允许现金流出。第三核对参与者关系。用SQL查每个事件关联的参与者数量如果查出少于两个先别急着补数据回到业务流程描述里去确认是否有建模遗漏。大多数对不上的根源都在事件拆分粒度不对而不是数据录错了。4.3 性能瓶颈的一个有效处理方式有朋友问过我“资源流表一直追加查余额越来越慢怎么办”先说结论不要提前优化但也不能放任不管。我的做法是分层短期查询走资源流表实时聚合加一个适度的时间范围限制中长期跑一个物化视图或定时汇总任务按“资源ID状态天”粒度做预聚合。做预聚合时注意一个细节必须支持“时间范围回溯”的能力不能只保留最终余额。我的方案是预聚合表保留每个资源每天的净流量及累积余额这样任意一天的快照都可以通过查预聚合表拿到同时单日内的实时查询走明细表。两层结合兼顾准确和性能。5. REA和主流设计思想的关系它不是替代品而是底层语言5.1 与领域驱动设计互补而非冲突很多做DDD领域驱动设计的团队说“我们有聚合、有领域事件不需要REA。” 这个理解不准确。DDD解决的是如何划分限界上下文、如何组织业务逻辑而REA解决的是在交易和账务类领域内实体关系应该长成什么样。两者不在一个层面。具体来说DDD里的“订单聚合”如果直接实现成“订单头关联订单行 状态字段”那只是把传统模型翻译成DDD语言而已核心关系并没有升级。而如果用REA来指导聚合内部的建模订单聚合的核心就不再是“订单头”而是一组围绕“下单锁定、发货出库、支付收款”三个事件及其资源流。DDD提供战术模式REA提供关系范式两者结合之后聚合的边界划分会自然得多。我在设计模拟项目X的订单域时最终落地的结构是订单作为应用层聚合入口保留但内部的数据载体从订单状态表迁移到了事件和资源流表。外部调用方依然用下单、发货、支付这些命令式接口不感知底层的REA结构但所有对账和财务逻辑都切换到推导模型上。这是一个能让团队平滑过渡的方式——接口不变模型变。5.2 与事件溯源的关系REA可以视为“业务语义化的事件溯源”事件溯源Event Sourcing强调把状态变化记录成不可变的事件流而REA强调事件需要包含参与者、资源和资源流向。两者结合的效果是每个事件不仅有技术上的前后差异还有业务上的完整语义。我见过一个项目直接用事件溯源存的是“订单状态从A变为B”这种事件虽然可以回放状态变化但回答不了“这单是谁卖的、流出的是哪批库存、用的是哪个价目表”。而REA建模出来的事件天然包含了参与者、资源流、时间再做事件溯源时不需要把每个业务动作都映射成状态字段的变化而是记录业务事实本身。实操中我的建议是小团队、业务逻辑复杂、需要频繁对账的系统优先用“REA模型关系数据库”落地事务可靠、工具链成熟只有读多写少、需要完整审计回放能力的系统才考虑“REA模型事件总线”的形态。不要因为REA和事件溯源契合就一上来给自己上分布式复杂度。6. 实操心得团队引入REA的节奏和个人建议先说团队节奏的事。一次性推全量REA重构风险极高尤其是存量系统。我的建议是挑一条最痛的链路入手比如对账频繁出错的库存链路用REA重新建模做成新老双写。新模型稳定跑三到四个迭代确认报表、对账全部一致之后再切换读流量。双写期会有一些对不平的额外工作量但这笔投入是值得的它能让你在业务面前保持可解释性而不是上线之后半夜查数据。再给一个具体技巧建模时给每个事件加上“业务意图”的描述。这不是形式主义它是后续排查问题时的关键索引。比如一个退货入库事件纯技术视角它只是“库存流入”业务意图是“客户退货”。加上这个意图字段后分析退货率、质检合格率、退款原因时才不需要靠单据编号去猜。按照我个人的观察能不能用好REA往往不是技术问题而是团队愿不愿意挑战自己“从表结构出发”的思维惯性。表驱动的建模方式在业务简单时很高效但业务一旦复杂最缺的恰恰是一个稳定的关系范式。REA最大的价值在于它能逼着你先想清楚业务事实再谈表结构。我这些年做过的所有交易类系统凡是数据一致性混乱的深挖下去几乎都是事前没有做事件层面的建模只做了表层面的设计。这一课补上之后很多问题会消失于无形。最后想分享一个我判断模型是否合格的小测试让一个不了解系统的新同学只看事件表和资源流表能不能还原出完整的业务过程。如果能模型是健康的如果不能说明模型里塞了太多只有开发才懂的状态字段和中间标记。REA天然倾向于让数据表“可朗读”这一点的价值在系统维护三年之后会越来越明显。我个人建议你下次做任何涉及资金、库存、积分这类带资源属性的系统时先花半天列一份事件清单不要打开数据库画表——你会上瘾的。这个习惯我认为是建模这件事里回报率最高的一笔前期投入。
阅读完成 · 觉得有帮助?