REA建模:资源-事件-代理模型如何支撑业务事实与事件溯源架构

发布时间:2026/10/20 18:33:06
REA建模:资源-事件-代理模型如何支撑业务事实与事件溯源架构 最近在复盘某套内部业务系统的数据模型时把“rea”这个项目代号翻了出来。它不是某个工具的内核名而是我手头一组实践笔记的简写Resource-Event-Agent资源-事件-代理模型。如果你做后端、做数据架构或者正在设计一套需要严格追溯业务的交易系统REA是你绕不开的一套建模思维。它解决的核心问题是让数据结构完整保留业务事实而不是只留结果、丢了过程。这篇内容适合正在做订单、库存、财务、审计类系统的同学参考哪怕之前完全没听过这个名字只要按后面的步骤走一遍也能画出属于自己的REA模型。1. REA到底是什么为什么值得把它当作建模底层框架1.1 一个词概括REA的设计哲学REA模型最早来自会计信息系统的语义建模方向上世纪八十年代由一位做会计信息系统研究的学者提出。它的核心思想非常朴素任何一笔业务都可以被拆成三类基本构件——资源、事件、代理。资源是有价值的对象事件是改变资源状态的业务活动代理是参与事件的个人或组织。这三者的英文首字母连起来恰好是REA。传统会计里的复式记账法把业务事实压缩成“借”和“贷”两列信息被锁死在科目结构里。这种模式的问题在于如果业务规则变了比如从单一币种变成多币种或者从一次性买断变成分阶段履约原来的表结构很难自然扩展。REA则完全不同它不问“这笔账应该记到哪个科目”只问“发生了什么事实”。事实保留得越完整后面做报表、做审计、做重组就越从容。我用一个生活化的类比来解释传统建模像拍照片记录的是某个瞬间的余额状态REA像拍视频记录的是从下单到支付、发货、签收的整个连续过程。照片看的是结果视频可以回放和回溯。对于交易系统来说能回放意味着可以回答“为什么是这个状态”而不是只知道“现在是这个状态”。1.2 REA与传统建模方式的差异以及适用场景很多同学习惯直接画ER图从表出发反推业务。这没有什么不对但如果业务复杂到一定程度直接从表设计入手会陷入“先有鸡还是先有蛋”的纠结该建订单表还是订单明细表状态字段要列几个枚举值退款是单独一张表还是加字段这些问题如果只靠ER思维往往拍脑袋决定等业务变化时又要不断加字段、加状态、加分支逻辑。REA的切入点是反过来的。它先问业务上有哪些资源在流动哪些事件在改变资源哪些代理在触发事件。以订单系统为例订单不是核心核心是“商品资源被预订”“库存被扣减”“资金被转移”这一连串事件。订单头更像是这些事件的组织者而不是建模起点。我把REA和传统方式做了一个对比方便你看清差异对比维度传统借贷记账/ER建模REA建模出发点科目表或数据表业务事实与语义保留信息借贷余额、汇总状态完整事件流与资源变化扩展能力加字段、加枚举状态加事件、加资源类型可追溯性难追溯中间过程天然保留过程痕迹与领域驱动设计契合度一般高尤其适合事件溯源哪些场景最适合引入REA我认为是交易链路长、角色多、存在多步骤履约的系统。比如电商订单从下单到支付到发货再到签收中间还有退款、拒收、换货供应链里的采购、入库、出库、盘点财务域里的预算、付款、报销、核销。这些场景的共同特点是业务事实之间有明确的因果和时间顺序用REA建模能避免后期大量的状态机补丁。如果只是做一个简单的内部工具比如一个几十行数据的管理后台直接ER建模就够了强行套REA反而增加理解成本。这也是我反复强调的一点REA是给复杂业务准备的框架不是所有项目都需要的银弹。2. 三类核心构件与建模规则先把地基打牢2.1 资源、事件、代理到底怎么区分很多初学者听完“资源-事件-代理”三个词觉得懂了真正动手时却卡住商品算不算资源订单算不算事件退款算不算反方向的事件这里我给出一个实用的判断标准。资源有一个关键特征它稀缺、可交换、能计价而且状态会因为事件而发生增减。现金是资源商品是资源积分是资源甚至一项服务的额度也可以建模成资源。但纯信息类的东西比如“收货地址”不是资源它不会因为业务事件被消耗或产生。订单本身在REA里通常也不是资源而是一组承诺的载体这一点我后面单独讲。事件的特征是发生在某个时间点、由一个代理发起、导致至少一个资源发生流入或流出。订货、收款、发货、收货都是典型事件。事件是REA模型的锚点它必须有时间、有参与者、有资源吞吐量。要注意事件记录的是“发生了什么”不是“应该发生什么”。实际发生和计划发生要分开建模这两类在REA里分别叫执行事件和承诺。代理则相对好判断任何能独立承担责任并做出行为的主体都是代理。客户、供应商、员工、仓库管理员、系统机器人都是代理。代理是事件的发起方和承担方也是后续责任追溯的对象。判断一个实体是不是代理就看它是否具备“决策和行为能力”而不是看它是否带了一个人的名字。我整理了一张区分表建模时可以对照着判断名称判断特征典型例子常见误判资源稀缺、可交换、能计价、状态被事件改变商品、现金、库存额度把订单当成资源事件有时间点、有代理、有资源增减销售、收款、发货、退货把状态字段当成事件代理有决策和行为能力的主体客户、员工、供应商把部门当成代理承诺对未来事件的契约约束订单、合同、采购协议把承诺当成事件部门到底算不算代理要看业务上部门是否能独立对外承担责任。大多数情况下真正签约、付款、担责的是具体的组织而非部门所以建议把部门建模成代理的属性而不是单独的代理。2.2 事件之间的时序依赖与关键约束规则REA模型最容易被忽视的是事件之间存在因果关系和时间顺序。比如先有订购事件后有发货事件先有发货事件后有收款事件。这个顺序不是数据库里的外键约束能自然表达的而是业务规则的语义约束。建模时我把事件分成两类交换事件和转换事件。交换事件表示资源在两个代理之间发生所有权转移比如销售收款、采购付款。转换事件表示资源在内部被消费或生产比如原材料被加工成成品、库存被盘点调整。做交易系统时至少要区分这两种否则无法回答“这个资源是被外部换走了还是内部消耗了”。REA还有一条核心规则每个事件必须至少涉及一个资源、一个代理。这条规则听起来简单却能在建模时筛掉很多“假事件”。比如“点击按钮”不是一个事件因为它在REA语义下没有资源增减而“提交订单”在某种业务定义下可能包含资源预留那它就要连接到具体的库存资源上否则就只是系统日志不是经济事件。我自己的建模习惯是每画一个事件就强制问三个问题这个事件改变了什么资源谁发起的与这个事件存在时序依赖的另一个事件是什么如果三个问题里有两个答不上来这个“事件”大概率应该重新定义。这个检查动作在项目初期多花十分钟后面能省下返回值修复的好几天。3. 用一个订单履约案例完整走一遍REA建模实操3.1 从需求中抽取资源、事件、代理理论知识说得再多不如完整走一遍实操。我以某电商企业的“在线零售订单履约系统”为例展开讲建模全过程。需求背景是这样的客户在网上下单购买商品系统扣减库存客户完成支付仓库发货物流配送客户签收。业务方明确提出三个硬性要求第一每一笔订单都要能追溯到完整的业务过程第二支持部分退款和部分退货第三财务报表要能按权责发生制取数而不是按收付实现制。拿到需求后我先不急着建表而是在白板上画业务事件流。事件流不是从“创建订单”开始的而是从“客户选购商品”这个动作开始。逐层拆解后得到这样一串关键事件客户提交订购请求、系统确认库存预留、客户完成支付、仓库执行出库、物流完成配送、客户签收确认。这些事件按时间顺序排列构成了业务的完整骨架。接下来抽取资源。在这个案例里明显的资源有可售商品库存、客户预付资金、应收款项。可能被忽略的资源是“配送服务”它同样有价值、可以被交易在物流环节建模成资源会更精确。我把商品、资金、配送服务都列入资源清单。然后抽取代理。参与事件的主体包括客户、平台运营人员、仓库管理员、配送司机。其中配送司机在签收环节需要作为代理记录下来因为后续如果有“货损责任判定”需要知道是谁负责配送的。最后补上承诺层。订购请求落地后系统生成订单订单本质是客户与平台之间的一组承诺平台承诺发货客户承诺付款。REA模型里承诺不是简单的关联表它串联起未来的若干事件。我把订单头建模为承诺实体把订单行建模为承诺明细连接后续的支付事件和发货事件。3.2 转成数据库实体与关系给出可直接落地的表结构模型画好之后要落成数据库结构。我在设计表的时候遵循了几个关键原则资源表只维护资源的静态属性和当前存量快照事件表只追加记录不做更新事件与资源之间通过关联表建立多对多关系。这直接为后续的事件溯源打下底子。下面是我在这个项目中实际使用的一组核心表结构做了脱敏简化但结构完整可参考CREATE TABLE resource ( resource_id BIGINT PRIMARY KEY, resource_code VARCHAR(64) NOT NULL UNIQUE, resource_type VARCHAR(32) NOT NULL, resource_name VARCHAR(128), current_qty DECIMAL(18, 4) NOT NULL DEFAULT 0, unit VARCHAR(16) ); CREATE TABLE agent ( agent_id BIGINT PRIMARY KEY, agent_type VARCHAR(32) NOT NULL, agent_name VARCHAR(128), contact_info VARCHAR(255) ); CREATE TABLE commitment ( commitment_id BIGINT PRIMARY KEY, commitment_code VARCHAR(64) NOT NULL UNIQUE, commitment_type VARCHAR(32) NOT NULL, status VARCHAR(32), created_at TIMESTAMP ); CREATE TABLE economic_event ( event_id BIGINT PRIMARY KEY, event_type VARCHAR(32) NOT NULL, event_time TIMESTAMP NOT NULL, commitment_id BIGINT REFERENCES commitment(commitment_id), agent_id BIGINT REFERENCES agent(agent_id), direction VARCHAR(8) NOT NULL, quantity DECIMAL(18, 4) NOT NULL, amount DECIMAL(18, 2), FOREIGN KEY (agent_id) REFERENCES agent(agent_id) ); CREATE TABLE event_resource ( event_id BIGINT NOT NULL REFERENCES economic_event(event_id), resource_id BIGINT NOT NULL REFERENCES resource(resource_id), quantity_change DECIMAL(18, 4) NOT NULL, PRIMARY KEY (event_id, resource_id) );表结构的核心是economic_event和event_resource两张表。economic_event记录了事件本身包含事件类型、发生时间、由哪个承诺驱动、由哪个代理发起、资源流入还是流出。event_resource则描述事件具体影响哪些资源、影响多少数量。commitment表用于将预订、请求与后续执行事件关联起来。这套结构和普通订单表最大的区别是订单表只保留当前状态而事件表累积了所有状态变化。可以说普通的订单表是“快照视图”事件表才是“事实底稿”。所有的业务查询既可以从快照读也可以从事件回放得到。3.3 扩展场景部分退款与退货怎么建模业务需求里的部分退款和部分退货是许多建模方式容易翻车的点。如果用“订单状态字段 退款金额字段”这种思路刚开始看着简单一旦出现多次部分退款字段就无法表达清晰的因果链路。REA建模对退款退货的处理非常自然退款是反向的资金事件退货是反向的库存事件它们和原始销售事件通过承诺关联。我建了一张退款事件表来承接这些逆向事件同时保留原始宣传事件的信息保证追溯链路完整CREATE TABLE reverse_event ( reverse_event_id BIGINT PRIMARY KEY, original_event_id BIGINT NOT NULL REFERENCES economic_event(event_id), event_type VARCHAR(32) NOT NULL, event_time TIMESTAMP NOT NULL, agent_id BIGINT REFERENCES agent(agent_id), quantity DECIMAL(18, 4) NOT NULL, amount DECIMAL(18, 2) NOT NULL, reason_code VARCHAR(64) );每一次部分退款都必须在reverse_event里插入一条记录并关联到原始支付事件。这样从原始事件到逆向事件的路径永远可以完整回溯。更重要的是在做CCFR或审计对账时我可以把原始事件金额减去所有逆向事件金额得到真实净额而不是依赖某个被反复修改的状态字段。官方报表那边也有收获。财务要按权责发生制出报表REA模型天生就能支撑因为承诺和事件分开记录在承诺发生的那一刻系统就知道未来会有什么资源流入流出在事件发生时再更新实际数据。这种“先承诺后执行”的结构让财务取数不再依赖人工调账。4. 项目落地中常见问题与排查技巧实录4.1 高频出现的设计误区REA模型理论上清晰实操中容易踩坑的地方其实很集中。第一个高频误区是“把系统操作当成业务事件”。登录、查询、点击、接口调用这些是系统操作不是REA语义下的经济事件。如果把这些都建模成事件事件表会涌入大量噪音数据真正的资源变化反而被淹没。我的判断标准是这个操作是否直接改变了某个资源的状态。没有资源变化的操作最多记入操作日志不进事件表。第二个误区是“资源表与库存表混为一谈”。REA里的资源是抽象概念商品是一个资源类型某个仓库里的实际库存是资源的实例快照。很多同学直接把库存表当作资源表结果每次库存变动都要去更新资源表不仅性能差还丢失了历史过程。正确做法是库存快照可以单独看但事实底稿应该以事件记录为准。第三个误区是“试图用REA替代所有数据库设计”。REA负责的是业务语义层和事件溯源层不是最终展示层的查询结构。如果业务查询需要对订单做复杂聚合完全可以建读模型、宽表、物化视图。把REA模型直接用于所有查询路径会让简单需求变得很笨重。建模这件事要区分“源系统的事实底座”和“查询系统的展示模型”。4.2 一份面向复盘的检查清单把这些教训整理成一份检查清单每次建模完成后过一遍能省掉不少返工时间检查项具体问法自查结果事件真实性该事件是否改变资源状态是否有发生时间和代理严格筛选代理完整性每个事件是否关联到唯一的发起代理逐一核对承诺与事件分离承诺数据是否与执行事件数据分开存储杜绝混用时序依赖事件之间的先后关系能否推演链路完整逆向事件处理退款、退货、取消是否有独立逆向记录不覆盖原事件资源口径统一资源ID和资源类型是否全局唯一清理历史脏数据除了清单我再分享两个排查时的小工具思路。第一个是“按承诺回放事件”取一个订单的承诺ID把它关联的所有事件按时间排序输出完整的事件列表。如果某个订单的事件链条中断说明建模或写入有缺漏。第二个是“资源平衡校验”针对某个资源ID把所有流入事件数量相加减去所有流出事件数量再与资源表快照对比。对不上说明有事件漏记或重复记。这两个检查我几乎每个迭代都在做。当时我第一次把REA报表模型交给财务对齐时发现累计销售额对不上排查了一下午。后来用“资源平衡校验”一查发现某个仓库的退货事件在写入时漏了资源方向标记导致退货被当成正向出库库存快照和事件流直接不一致。这种问题用传统状态字段模型非常难查但REA的平衡校验能让它在几分钟内定位。5. 项目之外的一些体会与后续扩展直接把REA模板套进所有新系统不是一个好主意。我个人的习惯是“业务复杂度驱动建模”而不是“建模驱动业务复杂度”。简单系统用简单模型复杂交易链路才动用REA完整框架。项目里真正该坚持的是“事件不可覆盖、事实不可删除”这条底线这比完整落地REA更重要。这个内容后续还可以向两个方向扩展。一是和事件溯源框架结合将economic_event直接作为事件溯源的事件源让微服务之间的数据一致性靠事件流来保障减少分布式事务的依赖。二是把承诺层做得更厚将合同、订单、预约都归入承诺模型统一管理企业级履约协议把销售、采购、库存全部串成一张完整业务网络。这两条路我都还在继续验证过程中收获最大的是对“事实与状态分离”这句话的理解。建模看起来是在设计表实际上是在设计一套关于业务真相的记录方式这个认知比任何一张表的字段定义都重要。