如果你在技术群里扔出一个只有三个字母的词rea大概率会被追问一句“什么怎么拼”今天我想把它安在一个很多后端团队都绕不开的话题上给业务系统建模。这里说的 REA是 Resource-Event-Agent 的缩写也就是资源、事件、代理。我第一次真正被这个框架击中是在一个电商交易与结算系统的重构现场。订单、支付、账单、退款、库存每个模块都有自己的状态机状态多了以后需求一来大家就变得非常敏感因为牵一发动全身。后来有个同事不经意说了句你先把资源、事件和参与的人都分清楚别一上来就画表。这句话听起来很简单但往回看很多模型的坑恰恰就是在这个问题上栽的。这篇内容适合正在做交易、订单、账务、库存、仓储这类系统的后端同学思考也适合接触过领域驱动设计DDD、事件溯源Event Sourcing但总觉得落地无门的团队。如果你只是写个简单的 CRUD 系统REA 不一定用得上但其中“先定义事实再考虑存储”的思考方式能帮你少走很多弯路。1. 先说清楚 REA 到底在讲什么1.1 三个字母背后的业务骨架REA 的整套思想可以压缩成一个画面一个事件Event连接了两个资源Resource以及两个代理Agent。Resource资源。指的是企业里需要管理、分配、交换的稀缺之物。常见的有现金、商品、库存、积分、服务时长、使用权。判断一个东西是不是资源标准很简单它有没有数量和价值属性能不能被交换会不会因业务活动而增减Event事件。指的是让资源发生换位或者变化的行为。比如销售、采购、支付、退货、领料、入库。这里的关键词是“变化”事件必须是真实世界的一次事实变更而不是系统里的一个点击动作。Agent代理。指的是参与事件的个人、组织甚至系统角色。在下单场景里客户是代理商家是代理自动审批机器人也可以是代理。代理的核心作用是把责任链条拴在事件上。三者的关系也很简单代理参与事件事件同时改变两边的资源。用一个财务视角来理解会特别舒服事件相当于一张凭证资源相当于借贷科目代理相当于往来单位。“有借必有贷、借贷必相等”说起来很玄但放到 REA 里非常直观——任何一次商业活动一定有一个出资源方也一定有一个入资源方。把一个订单扣掉库存的同时必然有一笔应收或现金流入形成。没有哪次事件只有一边资源在动那叫自说自话。1.2 别把 REA 当成表结构设计规范很多团队第一次接触 REA会以为就是把表分成三类资源表、事件表、代理表。我能理解这种直觉但这恰恰是最坑的理解方式。REA 改变的不是表长什么样而是你观察业务的角度。传统的系统设计路径是先看有哪些实体然后画 ER 图再对着实体建表。于是订单、订单明细、支付流水、退款单、库存记录堆了一大堆每个实体都自带一个“当前状态”字段。这种设计先天会有一个问题状态只是快照快照之间是靠什么串联起来的容易被忽略。比如订单状态从“待支付”变成“已支付”它缺了一个信息是哪一次支付事件完成了资金换位是哪位客户、哪个支付渠道代理参与了这次换位把“已支付”当状态存下来本身没错但如果整个系统都依赖这些状态而忽略背后的资源换位事件那对账时就是无源之水出了问题你只能猜数据。REA 的思路是反过来的先问这个系统里真正值得记录的事实是什么再决定这些事实怎么落库。事实就是事件状态反而是事件的影子。1.3 它和 DDD、事件溯源是怎么对上号的很多人玩 DDD 时会纠结到底是先设计聚合还是先设计事件。如果带着 REA 的背景这个问题其实早有答案事件定义业务语义聚合只负责维护当前状态。事件溯源Event Sourcing的核心是“事件即事实状态只是投影”。REA 的核心是“业务本质由资源换位事件表达”。你说它俩是不是一件事可以说是同一种世界观在不同年代、不同技术圈子里长出来的两种表达。REA 从会计信息系统那边世Event Sourcing 从软件工程这边起家最后在“事件是事实、状态可推导”这一点上碰头了。至于 DDD 里的聚合你可以把 Agent 想象成聚合的锚点把 Event 当成聚合之间的领域消息把 Resource 当成聚合内部的核心实体。比如“订单”这个概念在 REA 里它不是一张表也不需要被钉死在一个状态栏里。它更像是一条被 Process 串联起来的事件链从预订、支付、发货到收货每一步都有明确的资源换位和代理参与。订单的“当前状态”只是这些事件链投影出来的一个结果。2. 用 REA 重构一个真实交易系统的关键步骤2.1 先给个具体场景订单、库存、支付凑在一起假设你在做一套典型的电商零售系统业务上有商品、库存、订单、支付、退款。传统方案下你大概率会有这样几张表订单表status、paid_time、closed_time、订单明细表、支付流水表、退款单表、库存变动表。这套表本身没有毛病问题出在需求一复杂就靠打补丁。比如用户申请退款可能同时涉及支付原路退回、库存回补、订单状态改回“退款成功”。其中任何一个环节失败状态就卡住。于是你开始加定时任务、加补偿脚本、加人工运维后台。与其说你在做系统不如说你在给一团理不清的资源换位关系做善后。如果换成 REA 的视角事情会变得清晰很多。2.2 第一步先把资源换位全部列出来面对一个交易场景第一件事不是画框、建表而是拿出一张白纸把所有“谁的东西通过什么行为流向了谁”写下来。举几个这个场景里逃不掉的事实客户拍下商品约定成交库存从“可售库存”移动到“锁定库存”。客户通过支付渠道完成付款资金从客户资金账户换到平台结算账户。订单履约、打包出库商品从仓库库存换入“履约中库存”。物流签收商品从“履约中库存”换入“客户自有资源”范畴。客户申请退款资金从平台结算账户回流到客户账户库存再视情况重新入库。你会发现所谓“订单状态”不过是这串资源换位事件在某个时刻的拉平视图。你已经支付但还没发货本质上是“资金换位完成商品换位未开始”。与其在订单状态字段上写一堆 if/else不如承认系统真正的核心资产是这串事件状态只是给查询机和人类看的一个缓存。2.3 第二步定事件边界别把动作当事件这是新手最容易绊倒的地方。我曾经见过一张“事件表”里面塞了用户点击日志、按钮触发记录、接口调用记录。这些东西不是事件它们是动作痕迹跟业务事实完全不是一个量级的。判断标准很简单这个记录是否代表了一次真实的资源换位如果答案是“没有任何资源数量、质量、归属发生变化”那它就不是 REA 意义上的事件。举例用户点击了“提交订单”按钮资源没有换位此时不能算业务事件。只有当系统完成库存锁定可售库存减一、锁定库存加一才构成一个事实。支付的回调通知也不是事件真正的事件是“资金账户余额发生了一笔减少平台结算账户余额发生了一笔增加”。把回调当事件你会被网络抖动、超时重试、渠道对账这些非业务信号冲昏头。2.4 第三步用代理把责任链条拴住代理建模比看上去要复杂一点。同一个自然人在一笔交易里可能同时是买家、退款请求方、售后发起人。同一个公司可能既是供货方也是承运方。如果你在事件表里只留一个 buyer_id/seller_id 字段一开始没问题业务复杂后会后悔。REA 里的代理是“参与关系”不是一个角色字段。我的建议是每个事件至少保留两个代理维度主动发起方Agent from和被动接收方Agent to。下单锁定库存时发起方是客户接收方是库存管理方。支付成功时发起方是客户资金账户接收方是平台结算账户。退款时则反过来。代理的粒度也要小心。客户、店铺、仓库、支付渠道它们在不同事件里处于同一个层级吗不一定。你可以在具体场景里把代理拆成账号级、人员级、组织级但要注意层级过多会让事件表膨胀且难以追溯。落地时按系统最小管理单元来建模不用强行对齐行政组织架构。2.5 第四步物理存储和查询怎么做REA 是思维方式不强制规定你用什么数据库、几张表。但我个人实践下来物理上至少要有两类数据落点事件事实表和当前状态投影表。事件事实表的核心结构可以用一个简化例子来表达CREATE TABLE business_event ( event_id BIGINT PRIMARY KEY, event_type VARCHAR(64) NOT NULL, occurred_at TIMESTAMP NOT NULL, resource_out VARCHAR(128) NOT NULL, resource_in VARCHAR(128) NOT NULL, qty_out NUMERIC(18,4) NOT NULL, qty_in NUMERIC(18,4) NOT NULL, agent_from VARCHAR(128) NOT NULL, agent_to VARCHAR(128) NOT NULL, ref_process_id BIGINT NOT NULL, payload JSONB );这里有几个值得解释的字段。resource_out 和 resource_in 分别表示流出资源标识和流入资源标识。qty_out 和 qty_in 是流出和流入数量不一定相等比如发生损耗时。ref_process_id 是流程标识把一次交易涉及的所有事件串联起来相当于订单号的角色但它的语义更接近“一组相关事件的业务根”。状态投影表则是一个随时间更新的事实快照比如订单的当前状态、商品的当前库存。它的数据来源是事件表而不是业务系统直接写入。平时接口查询走投影表审计、对账、重放再回到事件表。这样设计最大的好处当某一笔对不上账时你可以沿着 ref_process_id 把所有事件按时间重放一遍不用在十几个状态字段里猜。数据一旦写进事件表就是不可变的没有 update只有新增事件。这能省掉的排查时间远超你付出的额外存储成本。3. 落地 REA 时最容易踩的四个坑3.1 事件表变成“动作流水账”最典型的失败案例是团队理解 REA 之后热情高涨把所有动作都塞进事件表。用户登录、商品浏览、按钮点击、弹窗展示全部当作事件写入。最后事件表爆炸式增长业务事实反而淹没在噪音里。我自己后来立了一个规矩写入事件表之前必须回答三个问题。第一这个行为是否改变了某个资源的属性、数量或归属第二改变是否可以通过数量与价值来衡量第三如果这个事件被删掉系统能否完整解释业务变化三个问题连续答不上来就不要进事件表。3.2 资源和代理的粒度没有提前统一另一个高频问题是资源定义得过粗。比如“库存”两个字在不同事件里可以是可售库存、锁定库存、在途库存、残次库存。如果你把库存当成一个笼统资源标识来建模那么库存锁定事件和库存扣减事件根本没法精确对账。我建议在动手之前先建立一套内部的“资源字典”每个资源类型要有明确的生命周期、计量单位、归属域。积分和人民币不要放在同一个资源聚合里哪怕是同一个数字字段也要加上资源类型维度。代理同理账号、门店、运营人员、系统机器人模型里的角色必须有全局可识别的唯一标识。不要用“1用户、2商家”这种脱管枚举一旦业务复杂起来没人记得清。3.3 只建正向事件不管逆向与补偿很多团队设计事件类型时天然只想着下单、支付、发货这些正向动作。可真到退款、取消、拒收、超时关闭时就不知道怎么写。正确的做法是逆向动作不是修改正向事件也不是把事件表里的旧记录置为失效而是追加一个新的资源换位事件。退货这个事件它照样有自己的 resource_out客户手上的商品、resource_in仓库的回补库存、agent_from客户、agent_to仓库。正向和逆向事件之间通过 ref_process_id 建立关联。这样整条链路的余额计算才是自洽的审计时也能看到完整的气候变化而不是盯着一张被 update 过的状态行猜来猜去。3.4 性能焦虑和团队思维转换阻力团队里最常见的反对声是每次都要解析一串事件才能拿到订单状态这不是性能灾难吗这其实是用误解代替了思考。REA 落地不是让你把所有查询都推到事件重放上。你仍然有当前状态投影表那是应付日常查询的事件表是负责还原真相的。两者并存各司其职。人脑从表结构设计切换到事件建模确实需要时间。尤其是习惯了状态字段的工程师会觉得“没有 status 就没法开始写业务”。我的经验是不要全盘推翻先挑一个最容易露出问题的模块做试点比如库存流水的重构。试点跑通后团队对“事件是事实、状态是投影”这句话的感受会很不一样。4. 常见问题排查速查表有些问题几乎每个尝试 REA 的团队都会问我整理成一张表方便你按图索骥。问题背后原因落地建议老系统已经上线还能往 REA 转吗历史表结构绑定业务迁移有风险不建议一次性迁移。先对最核心的库存或资金流水做事件化改造老表继续作为投影服务查询增量数据走新链路跑通之后再逐步收敛没有可靠的消息中间件能做事件化吗误以为 REA 必须依赖消息队列实现事件分发REA 只定义建模方式不绑定消息组件。事件事实表本身就可以承担存储职责你可以用定时任务扫描新增事件来驱动下游投影也可以在应用内同步写事件表和投影表事件表一直在膨胀怎么控制数据太大会影响查询和运维成本按业务域拆分事件表按时间分区老事件归档到冷存储。核心保证是近期数据可查可对账历史数据不参与高频查询报表需求五花八门总是需要临时查事件原始记录投影表覆盖不了全部口径不要试图让事件表直接承接所有报表。基于事件表重建一批专用统计投影表比如按日销售汇总、按渠道资金汇总报表只查统计结果多系统协作时事件对不上各系统对同一业务事实的定义不一致建立共享的“业务事件词典”统一事件类型、资源标识、代理标识和计量单位。宁可前期多开几次评审会也不要等对账失败再救火第5条尤其重要。REA 不是某个系统内部的自嗨设计跨系统时如果事件定义各说各话那这套框架的优点会立刻变成灾难。我们在实际项目里要求每个事件都必须在事件字典里有唯一编号包括它的前置条件、资源流向、代理角色。字典不完善代码不合并。5. REA 不止能做交易系统别把 REA 只绑在电商或支付系统上。我实际试过把它套到好几个非交易领域效果都出奇地稳。先说审批流。OA 里的请假申请本质上是“时间额度”这一资源的占用和释放。申请单不是实体Process 才是链路的根。请假申请事件把员工的年假额度从总额度中扣减审批通过事件批准了这次占用销假事件又释放额度。这种视角下各种请假状态根本不靠 status 字段只需要根据事件链计算当前占用了多少额度。再说促销活动中的优惠券和积分。积分的发放是运营活动名额这一资源向用户账户转移积分的消耗是用户账户向商家结算池转移。如果用普通表结构运营经常要为“今天发了多少积分、用了多少积分、过期了多少积分”出一堆临时报表。有了事件事实表这些口径全部可以从事件链条里自然算出不用再手工抽数、反复对齐。还有一个容易被忽略的场景权限与审计。一次后台配置变更涉及的不是资金库存而是“权限点”这个资源。谁在什么时间把某个角色的权限从一个账号转移到了另一个账号这本身就是一个标准的 REA 三元组。用于安全审计的场景事件表具有不可变性这一点比普通操作日志靠谱得多。聊聊我自己踩坑后的体会前两次尝试 REA我都做过半途而废的事。第一次是把它当成必须全量落地的银弹结果事件表和数据字典越建越庞大最后连存量系统怎么映射都没想清楚。第二次是太守旧只在文档里写了一套“愿景”代码里还是老式的状态字段结果当然是形同虚设。第三次的时候我只守了一条纪律任何涉及资源变动的接口必须明确写出它对应的事件、涉及的资源标识、以及代理双方。想画表之前先写事实写事实之前先确认资源字典里有没有对应项。就靠这一条那套系统的核心模型在后续新增了五六个子模块之后依然稳得住没有发生那种牵一发而动全身的状态大战。事后复盘REA 给我最大的改变不是表结构变好看了而是动手设计前的思考顺序变了先问发生了什么再问怎么存。哪怕你最后一张事件表都没有建只要在设计接口、字段、消息流时愿意先问一句“这次资源换位是谁发起的、谁接收的、改变了什么”你的业务模型就已经比大多数系统结实了。