我第一次见到REA这个缩写是在和一个做财务系统的朋友聊天时。他提到核心表全部按“资源-事件-参与者”来建模我当时第一反应是这不就是把ER图画细一点吗直到后来自己被一个销售对账需求逼到墙角被一堆改来改去的状态字段折腾得焦头烂额才回过味来——REA不是一种画图风格而是一种描述业务事实的纪律。简单说REA是Resource资源、Event事件、Agent参与者三个词的缩写它把每一次有经济后果的业务动作都拆成一笔“可回放”的事件而不是一条条会被覆盖的记录。这篇文章想聊清楚的就是这套建模思想到底是什么、怎么落地、以及我在实践里踩过的坑。适合正在做订单、库存、账务相关系统的开发者和数据建模的同学。如果你只想应付一个简单表单系统REA确实有点重但只要你见过“一张订单表往里面堆了十几个状态位”的场面就值得花十分钟把下面的内容看完。1. 为什么用REA——先想清楚业务怎么“记账”再谈表结构1.1 库存表暴露出的问题余额是“结果”不是“事实”大多数业务系统在起步时Model都是这么设计的一张商品表带一个stock_qty字段一张订单表带一个status字段一张用户表带一个balance字段。这套设计在业务简单时没什么问题可一旦出现退款、取消、补单、对账、审计麻烦就来了。举个最常见的场景商品A的库存字段写着58但你需要回答“这个58是怎么来的”。你只能翻订单流水看哪些单子加了、哪些单子减了。如果中间有人直接改过库存字段或者某个订单状态被回滚这个58就已经失真了。问题的根源在于你把一个应该被推导出来的“结果”当成了“事实”来存储。余额、库存、状态这些都是某个时刻的状态快照它们会变而且一变就丢掉了历史。而真正不会变的是那些曾经发生过的事件某人在某天买了三件商品某人在某次退货中退回一件。1.2 REA的三元组从“表结构”回归“经济事实”REA最初是会计领域提出的建模思想核心就三个词资源Resource有价值的东西比如商品、现金、积分。事件Event让资源发生增减的业务动作比如销售、采购、付款。参与者Agent参与事件的个人或组织比如顾客、供应商、销售人员。用一句话概括**谁在什么时候因为什么事件让什么资源发生了怎样的变化。**这三个词组合起来能描述绝大多数业务场景。比如“书店卖出三本书”资源是书和现金事件是销售和收款参与者是顾客与书店。这个描述里没有“库存数量”这种会被覆盖的数字有的只是可追溯的事件。你可以随时问系统“当时发生了什么”而不是“现在还剩多少”。REA最让我佩服的一点是它跳过了中间状态。传统建模关心“订单现在处于什么状态”REA只记录“发生过什么事件”。状态是事件发生后自然推导出来的东西而不是需要小心翼翼维护的易变字段。1.3 REA与事件溯源的区别很多人会把REA和事件溯源Event Sourcing混为一谈这里说一下我的理解。事件溯源是一种存储与实现手段核心是“不更新事实表只追加事件日志”REA则更像一种业务分析框架它告诉你哪些东西该被称为“资源”、哪些动作才值得记成“事件”。两者天然契合但不是一回事。我实际项目中用过两种结合底层按事件流存储对外暴露按REA语义组织的聚合接口。如果团队规模小也可以先只用REA做数据库表结构设计服务层不搞复杂的事件总线照样能解决很多对账问题。REA给你的是业务视角的清晰度事件溯源给你的是存储层面的不可变性。2. 拿销售场景手把手建模从业务事件到ER图2.1 场景定义与实体清单先说一个我练手用的虚构场景就叫“模拟项目X”吧一个跨平台的销售系统。业务流程本身不复杂顾客下单购买商品系统从仓库发货顾客付款平台收到货款。看起来就是一个普通电商闭环但要支持售后、退款、对账传统表结构已经开始吃力。按照REA的思路第一步不是画ER图而是把业务里“有经济后果的动作”全部列出来。我当时的动作清单是这样的顾客下单承诺购买系统发货商品离开仓库顾客付款现金进入平台账户商品采购入库商品进入仓库支付供应商货款现金流出注意像“用户浏览商品”“加入购物车”这类动作不算经济事件它们不改变任何有价值的资源所以不需要建模成Event。2.2 三类核心实体怎么识别实体识别有个很实用的判断标准**问自己这个数据如果被删除会不会让历史账目对不上**如果会它大概率是事件或资源如果不会它可能是辅助信息。在上述场景里资源有两类商品类资源库存商品和资金类资源应收款、现金。参与者至少三个顾客、销售平台、供应商。事件则有销售、发货、收款、采购、付款共五个。还有一种判断方式资源是名词事件是动词。名词如“商品”“现金”“积分”是资源动词如“销售”“付款”“采购”是事件。参与者则是动作的执行主体。这套识别规则几乎可以直接套用。2.3 关系如何建立资源流、转换流、职责流识别出实体之后最关键的步骤是建立关系。REA建模里关系主要分三类第一类是“事件-资源”关系也叫资源流。销售事件消耗了商品资源同时创造了应收款资源付款事件消耗了应收款资源同时增加了现金资源。用通俗话说事件像管道资源像管道里流动的水。第二类是“事件-事件”关系也叫转换流。销售和付款之间是配对关系前者代表“应给”后者代表“实际给”采购和付款之间同样是配对关系。这种配对可以让你发现某个销售事件发生了但对应的付款事件迟迟没到——这就是未结应收。第三类是“事件-参与者”关系也叫职责流。每个事件至少要关联两个参与者执行方和接收方。比如销售事件执行方是平台接收方是顾客。某个事件如果不关联参与者说明业务描述不完整。这三类关系理清之后ER图基本就出来了。我一直认为REA最值钱的地方就在这一步它强迫你想清楚“这笔业务动作到底交换了什么”而不是一上来直接按页面原型建表。2.4 通过流程连接看懂整条业务链实体和关系都有了怎么把它们串成一条完整的业务链关键在资源。继续用书店的例子采购事件把现金资源转换为库存商品资源销售事件把库存商品资源转换为应收款资源收款事件把应收款资源转换为现金资源。这里现金资源同时出现在两个事件的两端它就是连接采购、销售、付款这几个事件的“铰链”。我在实际建模时会专门检查每一类资源是否至少出现在一进一出两个事件里。如果某类资源只有进的没有出的或者只有出的没有进的那很可能漏掉了某个关键业务动作。比如只建模了“发货”却没有“收货”库存商品资源的链条就断了。这种从“资源流动”角度审查业务的方式比单纯画ER图更容易暴露业务盲区。它让你站在账本的高度看系统而不只是站在接口的角度看功能。3. 从模型到表建表、编码与查询落地方案3.1 事件表、资源表、参与者表怎么设计模型阶段再怎么清晰最终也要落到数据库表上。下面这组SQL是从我实际项目里简化出来的数据库用PostgreSQL但换成MySQL只要改一下自增语法就行。资源表create table resource ( id bigint generated always as identity primary key, code varchar(64) not null unique, name varchar(128) not null, res_type varchar(32) not null, currency varchar(8) default CNY );参与者表create table agent ( id bigint generated always as identity primary key, agent_type varchar(32) not null, name varchar(128) not null );事件表create table event ( id bigint generated always as identity primary key, event_no varchar(64) not null unique, event_type varchar(32) not null, occurred_at timestamptz not null, buyer_id bigint references agent(id), seller_id bigint references agent(id), created_at timestamptz not null default now() );事件资源关联表用来记录每个事件影响了哪些资源、增减数量是多少create table event_resource ( id bigint generated always as identity primary key, event_id bigint not null references event(id), resource_id bigint not null references resource(id), quantity numeric(18, 4) not null, unit_amount numeric(18, 4) not null );这里有几个细节值得注意。一是金额数值统一用numeric不要用float对账场景下浮点误差会让你生不如死。二是event_no加上唯一约束这是幂等的基础后面会展开说。三是occurred_at和created_at分开存前者是业务实际发生时间后者是记录写入时间两者在补单、延迟同步时会出现明显差异。3.2 事件幂等与不可变设计REA模型落地时最容易踩的坑就是把事件表当成“订单状态表”来用——有人下单了insert一条event后面订单取消又把这条event删了或者改了状态。这么一搞历史就被破坏了。我的强制规范是事件表只有insert没有update和delete。业务发生变化时不在原事件上修改而是新增一个反向事件。比如订单取消新增一个负数量的销售事件跟原事件配对账目依然能对平。这样做还有一个额外好处并发场景下天然支持重试。客户端重复提交由于event_no唯一约束的存在后插入的同号事件会直接报错不会造成双倍记账。幂等这块我多说一句。消息队列场景里消费者重复投递事件是常态。如果你的事件表没有唯一约束对账报表会莫名多出一倍数字排查起来非常痛苦。我后来在event_no生成规则里加入了业务线前缀加订单号比如sale_order_20230402_001既方便定位又天然保证全局唯一。3.3 派生状态的SQL实现库存余额可以随时从事件表中聚合出来核心思路是把每个事件对资源的影响方向约定好采购为正、销售为负、退货为正。下面是一个简化的库存视图SQLselect r.id, r.name, sum( case when e.event_type PURCHASE then er.quantity when e.event_type SALE then -er.quantity else 0 end ) as current_stock from resource r left join event_resource er on er.resource_id r.id left join event e on e.id er.event_id where r.res_type INVENTORY group by r.id, r.name;这种查询写起来很规整而且不依赖任何“当前库存”字段。如果发现库存对不上直接从源头事件排查不用去猜哪次update改错了。3.4 工程妥协投影表与物化视图但这里有个现实问题事件粒度很细存量数据大了之后每次库存汇总都实时聚合数据库会撑不住。我项目里最后的方案是增加投影表projection。投影表本质上就是“余额表”比如inventory_balance它的来源是事件表但会通过一个订阅者或定时任务增量更新。当新事件写入时投影表同步累加或累减。也就是说我允许存在一份“结果表”但它永远只是事件的派生品而不是事实来源。修正数据时必须改事件再重放投影。这套架构让我同时拿到了两个好处短期查询性能上去了长期可追溯性也没有丢。代价是写完事件后要维护投影更新逻辑多了一点复杂度。我的取舍标准是如果这个查询会被大量用户高频触发建投影如果只是月底跑一次报表直接实时聚合。4. 这些坑我替你踩过了REA实战问题清单4.1 事件表被当成“订单状态表”来更新很多第一次用REA的团队建模做得很漂亮一到写代码就原形毕露。订单状态一变直接update event set event_type CANCELED。事后对账时系统里根本看不到“曾经有过一次销售然后被取消”的记录只有一条孤零零的取消事件。排查这个问题的唯一抓手是审计日志或者数据库binlog极其痛苦。我后来在代码评审标准里加了一条硬性规则**任何对event表执行update的代码都不许合入。**业务逻辑里要表达“取消”就生成一个新的取消事件两个事件拼起来才是完整的历史。4.2 退货和退款被建模成“修改原事件”还有一次某团队把退货设计成了直接改销售事件的数量原来卖3件退1件就把销售事件的数量改成2。表面看库存对了实际应收款、订单快照、财务入账全部对不上。正确做法是在销售事件之外新建一个退货事件数量为负关联同一个商品资源。这样“卖了3件退了1件净卖2件”是查询层算出来的结果而事实层永远保存着两条原始记录。退款也是一样不要改原付款事件新增一笔负向付款事件来充抵。4.3 事件粒度失控另一个典型错误是把所有用户操作都建模成事件。“用户加入购物车”“用户点击了结算”“用户修改了收货地址”如果这些也算事件事件表会膨胀到无法维护而且九成是没用的事件。我的判断标准是**只有让资源价值发生变化的行为才值得记事件。**加购物车没有改变库存、现金、应收款不值得记结算产生订单并锁定库存改变了库存资源值得记。把这条标准写进建模文档能让团队少吵很多架。4.4 什么时候不该用REA说句公道话REA不是银弹。如果业务只是一个内部工具比如员工名册管理、会议室预订强行上REA只会增加开发量。这类项目根本没有对账和审计需求一张表搞定的东西没必要拆成事件和资源。判断要不要用REA我一般问三个问题第一这个业务是否有经济后果是否涉及金额、库存、积分等敏感资源第二你需不需要回答“过去某时刻系统里是什么状态”第三是否有多方协作导致的纠葛场景比如顾客、平台、供应商之间对账。三个问题只要有两个以上是肯定的REA就值得引入。5. 从一张表到一套账REA改变的不只是数据库设计5.1 编码层面更容易测试与扩展用REA建模之后业务扩展带来的数据结构变动会少很多。新增一个业务动作往往只是往事件表里增加一种event_type配套关系表复用现成的。总不可能每个新需求都新造一张资源表吧做审计时却能轻松按事件维度拉全量流水。这一点在接口设计上特别明显。传统状态机风格的服务每加一个状态就要改一堆校验逻辑和接口文档REA风格的服务对外暴露的核心接口是recordEvent业务动作进来就落事件派生查询通过投影完成。新增业务时服务骨架不用动。5.2 对账、审计、报表变得理所当然我印象最深的是一次月结对账财务要求平台、仓储、支付渠道三方数字一致。如果用传统表结构光是把订单表、退款表、优惠券表、支付流水表join一遍就够喝一壶。REA体系下所有资源增减都沉淀在事件表里按资源和时间窗口做一次聚合账目天然平齐。这背后的原理是REA本身继承自会计的复式记账思想。每个事件至少影响两个资源比如“发货”同时减少库存资源、增加应收款资源一减一增资源总量守恒。有这个约束兜底报表对不上时基本可以断定是漏记或重复记事件而不是逻辑错乱。5.3 从小模块试点开始最后给想尝试REA的同学一个落地建议不要一上来就把老系统全部重写找一个业务边界清晰、对账需求强烈的模块试点比如积分账户或者库存流水。用REA建模这两个模块再把老系统并行跑一段时间对比新旧账目。我当时的做法是先做一个只读的“事件查询后台”把订单、售后、支付流水按事件结构展示给运营和财务看。他们一旦习惯了“所有数字都能追溯”的查询方式就再也回不到只能看当前状态的老系统了。这个内容后续还可以这样扩展给事件表加上责任链、审批流让敏感操作需要授权后才会写入。根据我个人经验REA最大的价值不是帮你设计出一套高明的表结构而是逼着你换一套语言去描述业务。先写清楚“发生了什么”再考虑“怎么存”一半以上的建模争论会自然消失。愿你少走我走过的弯路。