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

REA模型:以事件为核心的业务建模,重构订单与库存系统

REA模型:以事件为核心的业务建模,重构订单与库存系统 ★ FEATURED ARTICLE
说起来第一次在项目里见到“rea”这个缩写时我以为是某个内部平台的代号。直到翻完业务方的需求文档才反应过来这里说的是 REA——Resource-Event-Agent资源-事件-代理模型。这套理论最早来自会计信息系统领域但现在对做订单、库存、账务、供应链这类核心系统的工程师来说它几乎是重构存量业务时绕不过去的一把尺子。简单概括REA 是一种以“事件”为中心的业务建模方式。它不关心你的订单表里“状态”字段目前长什么样而是关心“这笔订单是什么时候被创建、由谁创建、涉及了什么资源、后续发生了哪些事实”。如果你正在设计一个需要审计、可追溯、能回答“数据为什么变成这样”的系统REA 值得认真研究如果你正被一堆状态字段和跨表联动的业务逻辑搞得焦头烂额这篇文章应该能给你一个全新的拆解思路。1. 先把REA的三个字母讲透模型到底在描述什么很多团队上手做业务建模第一反应是画 ER 图用户表、订单表、订单明细表、支付表然后靠外键把它们串起来。这种以实体为中心的思路在简单业务里很好用但一旦业务复杂起来问题就出现了——状态到底存在哪里一张订单从下单到支付到发货每个环节都要改“状态”字段这一改历史信息就蒸发了。你只看到订单现在是“已完成”但不知道它是怎么走到这一步的也说不清楚中途经历了哪些波折。REA 想解决的正是这个问题。1.1 资源不是数据库表而是系统真正关心的资产REA 里的 Resource资源指的是企业拥有或控制的、在业务过程中被交换、使用或消耗的资产。它不单指有形的货物也包括现金、服务工时、积分、电子券、知识资产甚至车队的一趟运力。建模时的关键点在于资源应该被建模成“存量”与“流量”两部分而不是一个简单的“当前数量”字段。举个例子很多订单系统的库存表长这样sku_id、stock_qty。下单减库存、退款加库存。听起来没问题但某天运营发现库存对不上了你只能摊手——因为没有任何一张表记录了“库存为什么从 100 变成 97”。资源视角则要求拆成“库存存量视图”和“每一次入库/出库事件”库存数量永远可以由事件流推导出来。这样对账的时候就能逐条核对。1.2 事件真正不可删除的事实这是 REA 最精髓的部分。Event事件表示一次实际发生的经济行为是系统的唯一事实来源。订单被提交了一次、付款到账了一笔、仓库发出了一箱货这些都是事件。事件必须满足两个特点不可变更、可追加。一旦发生就永久保留不能删除、不能默默修改只允许通过新事件来修正。这里有一个容易混淆的点。研发常说的“事件”可能是消息队列里飘过的一条 JSON消费者消费完就再也不见了。REA 里的事件是领域事实是会计账本里的一笔分录。对比一下就清楚了消息事件关心“下一步触发什么”REA 事件关心“过去发生了什么”。前者是流式协作后者是事实沉淀。建模的时候要明确每个事件到底承载哪个职责。很多项目把 MQ 消息直接落库当事件表结果业务方要求追溯时发现内容不够还得回源翻日志。事件命名也值得注意。我见过不少团队用“订单状态流转事件”“状态已更新”这种抽象名字复现业务场景时根本看不出来发生了什么。REA 里建议用业务语言命名比如 OrderSubmitted、PaymentReceived、GoodsShipped。即使不引入事件溯源框架单看这个事件表的名字业务方也能读懂。1.3 代理谁对这件事负责Agent代理是参与经济事件的个人或组织。这个概念看着简单实际很容易被画丢。不少建模文档里订单表只有 customer_id付款表只有 payment_tx_party真正“谁下了这笔单”“谁审核通过了这笔退款”是没有建模的。一旦发生纠纷就查不到操作人。REA 强调将内部代理与外部代理区分开。外部代理是客户、供应商这类交易对手方内部代理是销售员、仓管员、客服等组织内的角色。事件与代理的关系也分成两类内部代理通常代表企业行使权力外部代理承担对应的责任。比如“付款”这个事件至少涉及两个代理客户付钱外部财务确认收款内部。建模时如果漏掉内部代理后续做绩效归属、责任追溯、权限审计都会缺一条腿。2. 一个订单场景的REA建模全流程三步搭出可落地的模型理论说再多不如直接拿订单业务走一遍。我拆一个最常见的 B 端小程序商城场景客户浏览商品、下单、付款、仓库发货、客户确认收货。目标是让这套模型能支撑财务对账、业务追溯和后续的售后拆单。2.1 第一步圈定资源与代理先不碰状态字段一开始什么都不要想先把业务中“有什么资产”列出来。这个场景中资源至少有商品、现金、库存、物流服务。接着列代理客户、销售员、仓库管理员、财务人员、快递公司。先列全再精简。记住一个原则只有参与事件交换或责任归属的才需要进入模型。快递公司虽然在物理上搬运了货物但如果系统不需要追踪它内部的动作那它只作为“承运方”属性挂在发货事件上即可不一定要建模成全局代理。我见过很多建模小白在这一步把所有的“角色枚举”都塞进 Agent 表结果一对多关系搞得异常复杂。其实代理是“参与事件的经济主体”不是“所有业务动作的执行者”。某条业务规则要不要单独建模成代理取决于未来是否要按它做归属统计或责任回溯。不需要就别建。2.2 第二步把业务操作翻译成事件并补全事件之间的依从关系这一步是整个建模的核心也是最容易漏的地方。业务方往往只告诉你“下单、付款、发货”但真正建模时要把“经济事件对”补出来。REA 里有一个非常重要的思想每笔资源流入都要有对应的资源流出二者构成一个完整的经济交换链条。按这个思路整理出来客户提交订单商品资源未来流出承诺现金资源未来流入承诺。客户完成付款现金资源流入实际。仓库发出商品商品资源流出实际。客户确认收货/系统自动确认收货交易闭环确认。注意这里把“下单”也建模成事件但它与“付款”相比是“承诺”而非“实际”。如果业务系统不打算处理承诺可以只建模实际事件但订单提交本身作为系统事实还是应该落事件。后续做未支付订单统计时就能直接在事件表上算。接下来要画关系事件与事件之间的依从关系也叫触发关系。Pay 并不是独立发生的它必须依附于 OrderSubmit 才能解释“这笔钱是为什么付的”Shipped 也必须关联 OrderSubmit。这一层关系建立起来之后财务对账就有了天然链路现金流入与商品流出互相印证不存在“钱货两清”的模糊说法。事件与资源之间的关系也要明确每个事件至少涉及一个资源增量或减量。付款对应现金的 increment发货对应商品的 decrement次品报废对应商品的 decrement且没有增量配对。这一步做完基本上一个可追溯的业务模型就成型了。2.3 第三步形成建模产出物并做一次合规校验最终产出物一般包括三部分事件表含事件时间、类型、关联代理、资源流表含每次事件的资源增减明细、关系图谱事件-资源-代理及事件-事件的触发链路。我建议不要只画 ER 图ER 图表达不了事件链路的时序含义最好额外画一张事件关系图用箭头标明“谁触发谁”“谁依赖谁”。校验时可以拿三条业务问题来反查模型能不能回答“某个订单的每一笔资金变动分别是什么事件、哪个时间、对应哪条资源记录”能不能回答“某类库存现在的数量向下追溯是由哪些事件加加减减得到的”能不能回答“某个客服撤销了一笔订单系统里有没有一条可以追溯到操作人并说明原因的新事实”如果三条都能清晰回答这个模型基本是健康的。任何一条含糊就说明事件没建全或者代理没挂对。3. 把REA模型落地到代码三种实现路径与选型建议模型画得再漂亮最后都要跑在代码里。根据团队的技术栈和业务强度REA 可以有不同的落地方式我分别说下优缺点和实操要点大家按需取舍。3.1 最稳妥的路径关系型数据库中的“事件表投影查询”不一定要上复杂的事件溯源框架。很多关系型数据库项目只需把核心表结构改成 REA 风格一张 event 表事件主表、一张 event_resource_flow 表事件对应的资源增减、一张 resource_master 表资源主数据、一张 agent 表代理主数据。业务操作发生时事务里插入事件记录和资源流记录再同步更新一张“资源现状表”用于快速查询。这条路径的技术门槛很低现有系统改造起来也相对平滑。它解决的核心问题是把“事实”和“状态”分离开状态表可以被重建、可以修正事件表只增不改。比如某天发现库存被算错了可以重放事件流去核对而不是人肉改库存字段。这里有一个非常关键的工程细节事件插入和资源现状更新必须在同一个数据库事务里完成否则一旦写入事件成功但更新资源现状失败后续所有的重放计算都会对不上账。我见过团队图省事先把状态更新了再发事件结果进程崩溃时事件丢了状态已经改了整个对账就乱了。3.2 更彻底的路径事件溯源Event Sourcing让事件成为唯一事实如果业务对审计要求极高且需要支持任意时间点的状态回溯可以考虑完整的事件溯源方案。事件表就是系统里唯一的真相来源没有独立的“库存现状表”可以被修改所有的现状视图都是通过事件流投影projection算出来的。查询时从投影表读写时只往事件表追加。落地时要注意事件溯源最大的敌人是“事件结构变脏”。第一版事件字段是 payment_amount第三版要拆成 principal_interest_rate历史事件怎么兼容一个补救措施是历史事件不允许改但应用层可以引入事件升级器读取老版本事件时解释成新结构。不要试图用“某个字段废弃了、新字段补充上”这样的简单方案随着时间推移事件数据的解释复杂度会指数级上升。经验是把事件抽象成“业务动作 发生时间 操作上下文”结构尽量稳定扩张字段时给默认值兜底。另外完整的事件溯源需要配套一个可靠的事件存储和投影构建机制。好消息是主流云厂商的消息中间件和数据库产品都能胜任但项目内部要制定好版本管理规范。没人管理的事件流两年之后就是一团乱麻。3.3 现实折中REA事件模型 CQRS分离读模型与写模型大部分团队既不希望破坏现有服务结构又想要 REA 的追溯能力这时 CQRS命令查询职责分离是很好的折中方案。写入侧走 REA 事件每次业务动作只追加事件读取侧维护一个或多个专门的投影模型比如“订单列表视图”“库存视图”“财务对账视图”。写模型追求事实准确读模型追求查询效率各干各的互不拖累。这套方案实施起来要比纯事件溯源轻很多也不要求所有读路径都从事件重建。你可以把核心的高价值写操作做成事件溯源其他附属业务继续用传统 CRUD。比如订单和支付走事件流用户标签类业务继续用普通表。边界划清楚就好不要一刀切全上事件化。3.4 三种路径的参考对照给一个我常用的选型对照表方便不同团队对号入座实现路径技术门槛追溯能力开发效率适合场景事件表投影更新低中高存量系统改造、中小业务完整事件溯源高高中低金融、账务、审计严格场景REACQRS 折中中中高中新系统设计、复杂业务领域性能这块给个朴素的估算参考一台普通的关系型数据库服务器每秒插入几千条事件记录是很轻松的投影表更新如果做成异步的再叠一层缓存日常业务查询基本无压力。真正的瓶颈一般不是事件写入而是事件流重放时的计算量和存储膨胀后者要靠快照机制缓解。4. 实战中绕不开的三个关键问题与解法建模型的时候容易纸上谈兵落到真实环境里翻车往往就翻在下面这几个环节。我把踩过的坑和排查思路集中写一写。4.1 问题一把“当前状态”当成事实导致追溯链断裂最常见的错误是事件表里记录了“订单状态变为已支付”但没记录“支付金额”“支付流水号”“支付方式”。表面上有事件但细看全是状态快照不是事实。等财务要逐笔核对“某个订单的钱从哪来的”根本答不上来。排查技巧很简单对每一条事件连续追问“这个动作如果发生纠纷需要哪几条证据支撑”。能答上来的字段才算事实答不上来的就说明事件还不够原子。另外不要用一个“状态”字段去表达业务动作业务动作必须动词化。应该是 PaymentReceived收到付款而不是 OrderPaidStatus订单已付款状态。4.2 问题二投影表和事件流总是对不上既怕丢也怕重在事件表 投影更新的方案里最头疼的就是投影状态更新失败。比如插入事件成功了但库存减量那一步因为唯一键冲突回滚了库存视图就没减掉。长期下来账面数和实际数就出现偏移。我习惯的解法是给事件表加一个 consumption_offset 字段记录投影任务处理的最后一条事件 ID投影任务重跑时只处理 offset 之后的新事件。每个投影任务独立维护进度互不干扰。如果发现投影表乱了直接清零 offset从事件表的第一个事实开始重放一遍。服务停机窗口通常都在半夜重放几百万条事件在现代硬件上也就几分钟到几十分钟的量级完全可接受。另一个经验是事件写入必须做好幂等。网络超时、重试、消息重复投递都是现实的常态。每条事件生成时带上全局唯一事件 ID投影侧记录已处理事件 ID 集合遇到重复事件直接跳过。这一层不做后面所有数字都会失真。4.3 问题二老系统迁移时历史数据怎么算账老系统的订单表、流水表堆了几年数据直接切换成 REA 风格新事件和旧表数据怎么融合我的建议是不要试图逐行回放历史的每一条业务操作成本和收益不成正比。更务实的做法是在某个切换时间点做一次“基线快照”。把老系统的最终状态如当前库存数、当前未完成订单作为一批初始化事件写入事件流并在事件上标记为“迁移基线”。这笔基线快照虽然无法还原历史过程但能保证新系统从切换日起追溯能力是完整的。业务方需要历史过程数据时直接查老系统报表即可两套体系并行一段时间等新系统数据攒够再正式下线老库。这个方案在严格审计场景下可能不够完美但绝大多数业务场景已经够用。5. 一点个人经验REA是建模思维不是银弹框架最后多聊几句。很多团队把 REA 当成一套具体框架去引入反而搞得项目很重。实际上REA 是一种建模视角——它提醒你把注意力从“存储状态”转到“记录事实”。哪怕最后代码里没有用任何事件溯源组件只是把核心表结构改成事件风格就已经能获得大部分收益。我个人的习惯是这样的新项目启动前先约业务方花一个小时把核心业务链路翻译成“资源、事件、代理”的词汇表。这一步往往能倒逼业务方说清楚很多模糊口径比如“退货”到底退的是什么资源、金额如何计算“取消订单”算不算一件事实、需不需要责任归属。建模阶段多澄清一个模糊点开发阶段少改一版需求。如果你打算在下一个项目里尝试不用一开始就把所有模块都 REA 化。选一个最看重对账和审计的模块比如订单、资金、库存拿它先做试点。等团队理解了这套思考方式再逐渐往财务、客户、供应链等其他领域推广。踩过几次坑之后你会发现很多曾经让人头疼的“历史原因”“数据对不上”“谁操作的”其实在建模阶段就能被消解掉。
阅读完成 · 觉得有帮助?
咨询建站