有人看到rea这三个字母第一反应可能是React生态里的某个小工具或者哪个开源项目随手起的缩写。我最初也这么以为直到把一个进销存系统推到重构边缘才真正搞明白REA模型——Resource、Event、Agent是业务建模里最容易被低估的一张底牌。它不是什么新框架也不需要装依赖它是一套让你不再被改需求就改表反复折磨的领域建模思路尤其适合电商、供应链、财务系统、通用业务中台这类强业务场景。无论你是后端工程师、系统分析师还是架构师只要你的系统里躺着大量状态字段和类型字段这篇文章就值得读完。REA模型最早来自会计信息系统领域一帮做企业建模的老前辈发现用借-贷会计科目去描述业务模型会变得又僵又绕但如果你只记录资源发生了变化、发生了什么事件、谁参与了这次事件业务语义反而稳定得多。这套思路后来被用到数据库设计、领域驱动设计、事件溯源里很多系统看似复杂拆开以后都是同一个骨架。下面我结合自己重构某个订单结算模块的真实过程把REA掰开揉碎讲一遍文末还会把踩过的坑原样摆出来。1. 先搞懂REA的核心三要素很多人第一次接触REA容易被三个英文术语吓到。其实把这三件事翻译成大白话就是你到底有什么资源、发生了什么事件、谁干的代理。听起来很简单但绝大多数业务系统恰恰把这三点绞在一起存进了一张巨大的订单表里。1.1 资源Resource业务真正关心的有价值的东西资源不是数据库字段而是业务上有价值、能流入流出、能被消耗或被创造的对象。现金、库存商品、积分、优惠券额度、人力工时都是资源。判断一个东西算不算资源我一般问自己两个问题第一业务上是否需要对它做数量或金额的核算第二它是否会在不同代理或事件之间发生转移。两者都是就应当建模为资源。举个例子一个电商订单里商品是资源订单状态不是资源。因为商品有库存数量、有成本、有转移过程而订单状态只是对事件发生后结果的一种标记。把资源和状态混在一起是后续所有表结构爆炸的源头。很多系统里订单状态字段有十几种枚举每种枚举背后都藏着一次完整的事件最优做法不是去枚举状态而是把那些事件显式建模出来。1.2 事件Event业务真正发生的事实而不是单据状态事件是REA模型里最核心的部分也是它和传统关系模型最大的区别。销售发生、采购到货、货款支付、商品退货这些都叫事件。事件是过去已经发生的事实具备不可变性。你不需要去更新一个销售事件只需要新增一个退货事件来表达对它的冲销。这里有一个关键认知状态是事件累积的结果。订单处于已完成状态是因为下发、出库、收款、发货等事件都发生了订单处于待支付状态是因为支付事件还没发生。所以最好的建模不是维护状态字段而是把每一个造成状态变化的事件记录下来。这样做的好处是任何时候你想知道一个订单为什么变成现在这样只需要把事件回放一遍逻辑清清楚楚。1.3 代理Agent谁参与了这件事代理就是参与事件的个人或组织通常分为内部代理和外部代理。内部代理是公司内部的员工、部门、仓库外部代理是客户、供应商、承运方。刚开始做REA建模时很容易忽略对代理的建模只在事件表里存一个客户ID字符串结果后续要统计哪个销售员负责的客户产生了多少收入时不得不去别的表里来回join非常痛苦。正确做法是先把代理统一建一张表事件与代理通过参与关系表连接每条参与关系还可以带上角色字段。例如客户下单事件里客户扮演外部代理角色接单的销售员扮演内部代理角色。角色字段非常有用它比在事件表里塞一个关联人ID灵活得多因为一个事件本来就可能涉及多个参与者。2. 为什么传统订单表加状态机不够用我在重构之前系统的核心表就一张订单表。字段包括订单号、商品名称、数量、单价、客户、销售员、状态、备注以及后来为了兼容赠品、退款、押金不断追加的十几个扩展字段。你大概能想象后来发生了什么报表统计对不齐、流程改动牵一发而动全身、新来的同事看着三十多个字段一脸茫然。传统模型的问题不是订单表本身而是把事件、资源、代理全部压平到了同一行里。2.1 传统模型的四张表魔咒很多业务系统最终都会走到相似的结局围绕一张主单据建订单表、订单明细表、收付款表、状态流转表四张表互相依赖查询靠一堆join修改需求靠新增字段和新增状态。这套模式在前三年业务不复杂时还好用一旦涉及跨部门协作、财务对账、供应链追溯就会开始处处别扭。问题根源在于主单据表把一个业务流程当成了建模的最小单位可真实的业务是由多个子事件组成的。销售、出库、收款、开票这些事件发生的时间、参与人、对资源的影响都不同硬把它们放在一张由订单主键关联的表里事件之间的独立性就丢了。结果是业务方每提出一个新流程你就要在主表上增加一个新状态或新标志位最终变成四张表魔咒谁都不敢动表结构但谁都觉得表结构不够用。2.2 REA的核心关系资源流与事件对REA模型里有两组关键关系理解了它们整个模型就活动起来了。第一组叫资源流关系Stockflow描述事件让资源发生了流入还是流出。比如销售出库事件让商品资源流出采购收货事件让商品资源流入收到现金事件让现金资源流入。资源流关系决定了每个事件对资源的增减影响这就是REA里资源-事件的绑定。第二组叫事件对关系Duality描述两个事件如何成对完成一次经济交换。比如发货事件对应收款事件客户得到商品的同时企业得到现金两个事件形成一对一或一对多的配对。这个关系特别重要因为它让系统可以回答这笔订单的钱到底收齐了没有这类财务问题。你不再需要去算某个字段而是顺着事件对关系把相关事件拿过来比对结果一目了然。2.3 一个送赠品需求如何把传统模型打崩我拿一个真实需求来说明。业务方某天提了个需求下单买两台设备额外送一个赠品包。传统做法很简单在订单明细表里加一个是否赠品标志位赠品单价写0完事。但如果赠品也需要走库存、也需要财务核算成本、甚至后续还支持单独退掉赠品呢标志位会越加越多逻辑会越来越乱。在REA模型里送赠品就是一次出库事件商品资源和赠品资源都发生流出只是各自对应的收款事件不同商品资源对应一笔现金流入赠品资源对应一个金额为0的赠品核销事件。事件本身不需要一个特殊状态来标记因为资源流关系和关联的收款事件已经完整表达了业务含义。后来业务又要求加赠品券积分兑换赠品赠品转售传统模型已经需要加四五个字段而在REA模型里只需要新增事件类型核心表结构一行不用改。3. 实战用REA搭建一套订单结算系统的核心表前面把理念讲清楚了接下来落到实操。我当时负责重构的模块是一个订单结算系统要求能支撑订单、发货、收款、退款、赠品、积分抵扣六类业务。我用REA把核心表设计成了六张没有一张是订单表但所有订单相关查询都能跑得清清楚楚。3.1 五张核心表的设计与字段解释先看资源表。资源表只存资源主数据和当前余量字段包括资源ID、资源名称、资源类型、单位、当前数量。注意当前数量虽然有点状态的味道但它可以从事件流水实时聚合出来保留它只是为了查询效率不是作为事实来源。事件表是核心中的核心每条记录代表一个已发生的业务事实。字段包括事件ID、事件类型、发生时间、业务单据号可选、摘要。我建议事件类型用带命名空间的字符串比如sales.order_placedinventory.shippedpayment.received避免不同模块的事件类型撞车。资源流关系表把事件和资源关联起来字段包括关系ID、事件ID、资源ID、流入/流出类型、数量、单价、金额。这是REA模型里真正算账的地方。需要注意一切金额和数量都只存在于这张流关系表里事件表本身不存金额这样一个事件涉及多种资源时才不会出现冗余。代理表和参与关系表分别管理代理主数据和事件参与信息。参与关系表字段包括参与ID、事件ID、代理ID、角色类型、关联的内部代理ID。角色类型可以是客户、供应商、销售员、仓库管理员等关联的内部代理ID用于把外部代理关联到负责的内部员工方便权限隔离和业绩统计。事件对关系表用来表达事件之间的成对关联字段包括关系ID、给出事件ID、接收事件ID、关系类型、对账状态。我做的结算系统里发货事件和收款事件就是通过这张表关联的。它不存业务金额只存事件之间的配对关系所以无论业务怎么搭配都不用改表。3.2 关键SQL怎么查有了这几张表应收未收金额这类查询就非常直观。我先从事件对关系表里找到所有与发货事件配对的收款事件再比较双方金额。伪SQL大概是这样的-- 查找某客户所有订单的应收金额对应发货事件流出的金额合计 SELECT SUM(efr.amount) FROM event_flow efr JOIN event e ON efr.event_id e.event_id JOIN event_participation ep ON e.event_id ep.event_id WHERE e.event_type IN (sales.order_placed, inventory.shipped) AND efr.flow_type outflow AND ep.agent_id :customer_id AND ep.role_type customer; -- 查找某客户所有已收款金额 SELECT SUM(efr.amount) FROM event_flow efr JOIN event e ON efr.event_id e.event_id JOIN event_participation ep ON e.event_id ep.event_id WHERE e.event_type payment.received AND efr.flow_type inflow AND ep.agent_id :customer_id AND ep.role_type customer;实际系统里我会再套一层日期、订单号等过滤条件但核心逻辑到这里就够了。你可能会问传统订单模型里这些查询用一句带join的SQL也能写有什么区别区别在于传统模型的查询条件是固定的字段结构一旦业务事件类型变化比如出现退款抵应收查询就要加新逻辑而REA模型只需要在事件类型里加一个payment.refund_offset查询条件按事件类型过滤结果自然覆盖。3.3 新业务怎么扩展新业务接入REA模型的标准流程我总结成三步第一确认涉及哪些资源是否需要在资源表增加主数据第二确认发生了什么事件是否需要增加事件类型第三确认这个事件和已有事件的配对关系。三步走完需要改的只有数据不需要改表结构。比如后来业务方要求支持预充值余额支付我先在资源表加了账户余额资源再新增一个payment.balance_deduct事件类型资源流关系表里记录账户余额流出。这个事件与已有的销售事件形成事件对关系。整个过程核心表没有动一行只增加了几个事件实例和资源实例。相比之下如果在传统订单表上实现至少要加支付方式字段、余额流水表和一堆对账逻辑改动量不在一个量级。4. 从REA到事件溯源与领域驱动设计的落地思考REA模型不光是一张数据库设计图它和现在流行的DDD、事件溯源思想其实是一脉相承的。很多团队做DDD做了半年聚合根画了一堆一落到数据库还是老一套。REA恰好提供了一个天然的分层和元素映射能让领域模型、事件表和代码实现真正对上号。4.1 DDD中如何定位REA三要素在DDD里资源通常对应实体或值对象比如商品、账户余额代理对应领域模型中的角色或参与者事件则直接对应领域事件尤其是那些业务方真的在乎、需要保留完整历史的事实。用这个映射去写代码你会有一种模型终于和业务说同一种语言的感觉。实战中我一般把事件作为聚合的操作入口用仓储模式持久化事件流。比如确认订单这个用例代码里做的事情就是校验参与代理是否有权限、校验资源数量是否充足、然后写入一个订单事件和一条资源流出记录。领域服务只关心业务规则不关心订单状态怎么流转因为状态本来就不是需要存储的实体而是事件回放的结果。4.2 事件溯源为什么天生契合REA事件溯源的核心想法是不保存当前状态只保存导致状态变更的事件序列需要当前状态时把事件重放一遍。这和REA不谋而合。REA里的经济事件本身就是一条不可变事实资源当前数量、客户当前欠款都可以通过事件流重放得到。用事件溯源实现REA时收益最大的模块是对账和审计。因为任何金额变动都能追溯到具体的事件、参与者和关联事件对财务审计时不再需要翻日志表碰运气。代价是需要引入事件存储和快照机制重放性能可以通过定期生成快照来优化。如果你们团队技术栈比较成熟我建议在结算、账务这类强审计模块优先试点事件溯源REA模型会给你省下大量麻烦。4.3 查询模型与报表怎么设计很多人听到事件溯源就担心查询怎么办我的做法是采用CQRS写入侧用事件表记录事实查询侧维护一套专门为报表优化的投影表比如客户维度应收统计表商品维度库存流水表。投影表可以随着事件的写入异步刷新也可以定时批量重建。由于REA中事件对关系非常清晰投影表的更新逻辑不会像传统状态机同步那样容易漏掉分支。报表这块我还建议预留一张科目映射表把REA的事件和资源流关系映射到财务科目。这样业务系统里的事件是通用的财务核算时可以根据需要重新解释同一批事件。比如同样是销售出库管理会计口径和财务会计口径对成本的拆分方式不同只需要调整映射规则不需要改动业务事件。这一点在我实际项目里效果显著财务口径调整了三次核心业务代码一行没动。5. 我在落地REA时踩过的坑与经验理念说得很顺实操肯定不是一帆风顺。我把自己重构过程中踩过的坑和调整方式整理出来给打算动手的同行做参考。5.1 坑一把所有业务都装进event_type结果事件满天飞第一次建模时我几乎把所有动作都定义成事件连用户登录用户浏览这种非经济行为也塞了进去。结果事件表爆炸查询性能下降业务人员也看不懂这些事件到底跟钱有什么关系。后来我做了收敛只把与经济资源流转直接相关的事实建模为REA经济事件其余行为日志另建普通日志表。判断标准很简单事件发生后资源数量、资源金额或代理权益是否发生变化都变了才算REA事件。5.2 坑二内部代理和外部代理没有区分权限就乱了刚开始我只建了一张代理表客户和员工都往里面塞角色字段随便填。结果做权限控制时发现一个客户被不小心分配了修改商品价格的权限排查了很久。后来我严格区分内部代理和外部代理用代理类型字段约束所有权限判断先看代理类型再看角色字段同时把内部代理与外部代理的归属关系单独建表。层级清晰之后权限和业绩统计都顺了。5.3 给新人的三个落地建议第一个建议是从一个边界清晰的子模块开始试点别一上来就重构全部核心链路。我当时选的是结算模块因为它涉及事件类型少、对账需求明确、风险相对可控。第二个建议是预留事件类型和事件对关系的字典维护界面让业务人员可以自助配置新事件而不是每次都找你加枚举。第三个建议是查询报表的投影表一定要和事件表分开部署、分开优化避免事务性写入和重度分析查询互相影响。如果你能把这三条落实落地REA的阻力至少能减少一半。最后再说一个我个人的体会。REA模型真正迷人的地方不是它让你少写多少张表而是它逼着你从记录单据转向记录事实。单据会过期状态会变化事实不会。当你把一个业务系统成功迁移到REA骨架之上你会发现后续所有新需求的讨论方式都变了大家不再问这个字段存哪个表而是问这个业务事件在改变什么资源、由谁参与、和哪个事件配对。这种思维转变才是REA带给我最大的收益。