第一次被拉进一个代号叫 rea 的项目时,我下意识以为这只是某个模块的缩写。直到产品经理把一整套财务对账流程图拍在桌上,我才意识到,这个 rea 大概率对应着企业建模领域那个经典术语——REA,Resource-Event-Agent,资源-事件-主体。它不是新概念,早在二十世纪八十年代就由学术界系统提出,但这两年做数据中台、微服务拆分、审计数字化,它被重新翻出来用的频率越来越高。简单说,REA 是一套告诉你“业务到底该怎么记录”的语义模型:不要只记余额,要记事件;不要只关心科目,要关心资源在谁手里、因为什么事件发生了转移。
我就用一杯咖啡把它讲明白。你走进一家店买咖啡,在传统会计眼里发生的是一笔“主营业务收入+现金收款”;在 REA 的结构里,这是三个东西的互动:资源(咖啡豆、咖啡成品、你付的钱)、事件(制作咖啡、销售咖啡、完成支付)、主体(你、店员、供应商)。不管多复杂的电商、供应链还是银行系统,大多数业务流程都可以还原成这三类对象以及它们之间的关系。这篇笔记不打算讲得太学院派,我会从模型概念讲到物理表结构,再讲我在实际项目里踩过的坑和排查思路。
1. rea 到底在说什么:用一杯咖啡把模型讲透
1.1 一件事,怎么拆成三个对象
“下单、出餐、配送、支付、退款”这些词在业务系统里天天见。REA 做的事情其实很简单:把这些事归类,并规定它们和资源、主体之间的关系。
Resource,资源,指企业拥有或能够控制的有价值对象。它可以是实物(咖啡豆、库存商品、仓库里的手机),也可以是抽象权利(应收账款、积分、优惠券、知识产权)。判断一个东西算不算资源的标准是:它是否具备经济价值,并且能够在事件中流入或流出。有价值的服务时间也可以建模成资源,比如律师咨询一小时、保洁服务一次,只要它能被定价、能被交易,REA 就认它是资源。
Event,事件,是资源发生增减变化的那一个时点。咖啡豆从供应商库房进入门店库房,是一次入库事件;顾客付钱购买了咖啡,是一次销售事件;顾客发起退款,是反方向的事件。事件是 REA 的核心,因为它记录了“发生了什么”,而不只是“剩下了什么”。这一点非常关键,后面讲和传统模型的差异时还会反复提到。
Agent,主体,是参与到事件中的人或组织。包括内部主体(店员、采购员、仓管员)和外部主体(顾客、供应商、平台)。主体承载了“谁负责”“谁受益”“谁控制”这层语义。没有主体的资源流是不完整的,因为你没法回答“这批货到底归谁管”这种最基本的业务问题。
这三者之间的关系也很固定:事件必然和资源有流向关系(stockflow,库存流),比如销售事件让咖啡库存减少、让现金资源增加;事件必然和主体有参与关系(participation),比如店员参与制作、顾客参与购买;事件之间还有配对关系(duality,二元性),比如销售与收款配对,采购与付款配对。这一套关系网织起来,REA 模型就像一张乐高积木图,你不需要发明新的积木形状,只需要用现有的积木拼出不同的业务。
1.2 和传统记账、传统 ER 建模的区别
传统会计模型核心是账户和科目,一张资产负债表里躺着很多余额。余额是结果,而 REA 重视的是过程,是事件。其实两者并不矛盾,但传统记账模型存在一个明显问题:一旦业务模式变化,比如从卖商品变成卖服务,从一次性买断变成订阅制,科目体系就要跟着调,报表口径也要全部跟着改。REA 恰好把重点放在事件层面,新增一种业务就新增一类事件,扩展模型而不是推翻模型。
| 对比项 | 传统会计/ER 模型 | REA 模型 |
|---|---|---|
| 核心对象 | 科目、账户、状态实体 | 资源、事件、主体 |
| 信息焦点 | 余额、期末状态 | 发生的事实、时间点 |
| 扩展方式 | 加科目、改表结构 | 加事件类型、加资源类型 |
| 适合场景 | 稳定、口径固定的场景 | 复杂、动态、跨组织协作 |
| 代表性输出 | 资产负债表 | 事件流、资源流 |
这个对比不一定绝对,因为很多系统是两者混着用的,但看懂这个差异,你就理解了 REA 的价值到底在哪里。传统模型回答“账平不平”,REA 回答“业务是怎么发生的、资源是怎么流转的”。当你想对账、做审计、做数据溯源时,REA 的语义优势会被放大到相当明显的程度。
2. 建模骨架:资源、事件、主体、契约要怎么建
2.1 四个基本概念逐个说清
如果只记资源、事件、主体,那还只是 REA 的第一层。真正要落地,你还需要第四个概念:Commitment,契约。
契约描述的是“未来将要发生”的经济活动。顾客下了订单,此时商品还没出库,钱也还没收,但订单本身是业务链条里不可跳过的一环。在 REA 里,订单通常被建模成契约,它规定了未来某个时间点将发生什么事件、涉及什么资源、由哪些主体参与。把契约引入模型之后,计划和执行就被分开:订单是计划,出库和收款是执行,计划和执行通过履约关系连起来。这样做的好处是,系统能表达“承诺了但没有兑现”的状态,对供应链和财务场景特别重要。
事件也要再做一次细分。REA 把事件分成经济交换事件和经济转化事件。经济交换是资源在两个主体之间变更控制权,比如销售、采购、支付;经济转化是资源在单个企业内部的形态变化,比如把面粉加工成面包、把零部件组装成整机。传统建模往往只关心“库存进多少出多少”,REA 则会进一步追问“这个库存是通过交换还是转化来的”,因为它会直接影响成本和责任的归属。
主体通常分成内部主体和外部主体。做多组织协同系统时,主体还有一个容易被忽视的作用:它是组织边界的锚点。同一笔库存,在供应商账上叫“发出商品”,在采购方账上叫“在途物资”,两边用 REA 建模时会把事件和主体对应起来,对账就变成在事件层面的匹配,而不是在报表层面的猜测。
2.2 四条核心关系:流向、配对、参与、控制
REA 的语义主要靠关系承载,不是靠字段类型承载。常见的四条关系,我一个个说。
Stockflow(资源流向)连接事件和资源。简单理解就像水库和水渠:事件是闸门,资源是水。开闸放水,库存减少;下雨入水,库存增加。每条 Stockflow 都要带方向,要么流入要么流出,最好也带数量。没有数量的流向关系是不能落地的,后面建表时我会给一个用数量的示例。
Duality(事件配对)连接两个事件,表达“价值互补”。销售和收款是配对,采购和付款是配对。放在生活里就是“一手交钱一手交货”,但交货这个事件和交钱这个事件往往不同时发生,所以需要 Duality 把它们串起来。Duality 是 REA 最容易被忽略但最重要的关系,因为没有它,你无法判断一个业务闭环是否完整。很多对账难题,本质上就是在检查 Duality 有没有闭合。
Participation(主体参与)连接事件和主体。谁卖了货、谁买了货、谁审批了付款,全部通过 Participation 体现。它可以是一对多,多个主体参与同一个事件;也可以是多次,同一主体在事件的不同阶段扮演不同角色。
Control(资源控制)连接主体和资源,表达“当前资源在谁手里”。在 REA 的严格语义里,Control 更多是在描述一种静态的委托持有关系,而不是动态的流入流出。实际项目里如果你建了资源表而且资源有明确的 owner 字段,那这个 Control 关系其实已经被模型隐式表达了。
2.3 三种流程:交换、转化、闭环
把上述概念组合起来,REA 支持三种典型流程,分别对应业务系统的三大类主流程。
交换流程(Exchange):双边资源换手。销售交换、采购交换、退款退货都属于这一类。特征是至少有两个资源流,一个流出、一个流入,并且有 Duality 配对。
转化流程(Conversion):资源在内部变形态。原材料变成半成品、半成品变成成品。特征是只有一个企业主体参与,但涉及多个资源和事件,比如领料事件、加工事件、入库事件。
闭环流程(Closed Loop):契约驱动执行的全过程。从客户下契约开始,经过实际执行事件,最终到达对账和结算。这个流程最贴合真实业务,因为它把“计划→执行→核对”串成了完整链条。
理解这三种流程,建模时就不会抓瞎。拿到一个业务,先判断它属于交换、转化还是闭环,再开始找资源和事件,效率会高一截。
3. 从概念到代码:电商订单系统的 rea 落地实操
3.1 选定业务场景和边界
理论讲完,总得动手。我用一个典型的电商订单系统来演示,这个场景大家熟悉,不容易被业务细节绕晕。
业务边界是:商品目录、库存、顾客下单、支付、发货、退货。为了不让模型膨胀,我刻意不把营销活动、优惠券分摊、多级分销放进来,那些都可以在同一个框架上加,但一次讲太多容易糊。模型的目的是解决经济语义问题,不是解决所有业务问题。边界画得越清楚,建模越顺利。
这个场景里有三个主要主体:顾客、店铺运营人员(或销售员)、仓库管理员。供应商作为采购环节的主体,在演示里也保留,但不展开它的内部流程。
3.2 六个步骤把模型建出来
我实际建模时会按下面六个步骤走,顺序很重要,跳步容易后面返工。
第一步,识别资源。初始资源有:可售商品、库存实物、现金(或应收账款)、优惠券。优惠券在严格 REA 里可以算一种对未来的购买权利的承诺契约,但为了演示简单,我把它当作资源处理。资源要带数量或金额语义,否则没法核算。
第二步,识别流程。这个系统以交换流程为主:顾客购买商品是销售交换;顾客退货是逆向销售交换;向供应商采购是采购交换。生产转化暂时没有,因为卖的是标品,不涉及复杂加工。
第三步,识别事件。关键事件有:销售出库、支付收款、采购入库、付款给供应商、退货入库、退款给顾客。注意“顾客下单”我放在契约层,不放事件层。它还没有引起任何资源增减,所以不是事件。
第四步,识别主体。顾客、销售员、仓库管理员、供应商结算员这四类主体要落到实体上。有些系统里允许同一个自然人在不同角色之间切换,REA 建议把“人”和“角色”分开建模,主体表里记录角色,人员表记录底层的自然人。
第五步,连接关系。把销售出库事件连到库存商品资源(流出)、连到顾客和销售员(参与);把支付收款事件连到现金资源(流入)、连到顾客(参与);销售出库和支付收款之间建立 Duality。采购、入库、付款同理。
第六步,补充契约。创建销售订单和采购订单作为 Commitment,并把订单与后续的执行事件关联。之后做“订单履约进度”或者“逾期未发货清单”时,直接查契约状态就行,不用在事件流里大海捞针。
3.3 落成物理表:一个可以直接抄的模型
REA 不是只有概念,它完全可以落到关系型数据库里。我的方案是“五张基础表+关系表”的模式,所有业务类型都用通用结构表达,不为了每种业务单独建表。
资源表用来存资源的主数据和当前库存金额。事件表是系统的核心事实表,每发生一次资源增减就插一条记录。事件和资源是多对多关系,因为一次销售可能同时减少多种商品,所以我用关联表 event_resource 来连接。主体表单独建。契约表记录承诺和状态。关联表则承载 Duality 和 Participation。
-- 资源表 CREATE TABLE resource ( resource_id VARCHAR(64) PRIMARY KEY, resource_type VARCHAR(32) NOT NULL, resource_name VARCHAR(128) NOT NULL, quantity DECIMAL(20, 4) NOT NULL DEFAULT 0, unit VARCHAR(16), owner_agent_id VARCHAR(64), status VARCHAR(32) ); -- 事件表 CREATE TABLE event ( event_id VARCHAR(64) PRIMARY KEY, event_type VARCHAR(32) NOT NULL, event_time TIMESTAMP NOT NULL, description VARCHAR(256) ); -- 主体表 CREATE TABLE agent ( agent_id VARCHAR(64) PRIMARY KEY, agent_type VARCHAR(32) NOT NULL, agent_name VARCHAR(128) NOT NULL ); -- 契约表 CREATE TABLE commitment ( commitment_id VARCHAR(64) PRIMARY KEY, commitment_type VARCHAR(32) NOT NULL, agent_id VARCHAR(64) NOT NULL, status VARCHAR(32), commit_time TIMESTAMP NOT NULL ); -- 事件与资源关联表 CREATE TABLE event_resource ( event_id VARCHAR(64) NOT NULL, resource_id VARCHAR(64) NOT NULL, flow_direction VARCHAR(8) NOT NULL, quantity DECIMAL(20, 4) NOT NULL, PRIMARY KEY (event_id, resource_id, flow_direction) );Duality 可以单独建一张表 event_duality,A 事件和 B 事件互为配对,插入时成对写入;Participation 用 event_agent 表表达。实际使用中,我还会在事件表上增加业务流水号、来源单据号、幂等键等字段,用来和老系统做映射。
如果要查询“某段时间内卖了多少钱”,一条 SQL 就能从 event_resource 里聚合流出金额;要查询“哪些订单没有对应收款事件”,直接拿 commitment 去匹配 event 关联的 Duality 即可。你会发现,很多原本要写十几个存储过程的财务对账逻辑,在 REA 模型里变成了单纯的关联查询,这就是语义模型带来的红利。
4. 三种技术路线的落地对比与选型建议
4.1 关系型数据库:稳妥的老牌方案
绝大多数企业应用最后还是会落到关系型数据库里,REA 也不例外。关系型的优势在于事务成熟、查询方便、生态好,团队学习成本低。用我上面那张五表结构就够,不用引入复杂的分布式组件。
关系型落地要注意两点。第一,不要把可变的业务属性硬塞进固定字段,要给资源表和事件表预留 JSON 扩展字段或者附属属性表。第二,所有事件都要带幂等键和来源单据号,否则重试机制会把同一件事记两次。事件表的写入是高频操作,一定要建好时间索引和关联索引,不然查 Duality 的时候会慢到让你怀疑人生。
4.2 图数据库:关系查询的天然主场
REA 本质上是一张语义网,资源、事件、主体是节点,流向、配对、参与是边。用图数据库表达会比关系型更直观,审计场景尤其明显。
举个例子,要查“某位顾客在过去一年里购买过的所有商品、对应的支付事件、涉及的退货退款”,在关系型里要做多次多表 JOIN,而在图数据库里直接沿着边遍历即可。边上面还可以挂数量、金额、时间等属性,语义不会丢失。图建模时,节点类型就是 Resource、Event、Agent、Commitment,关系类型用 StockFlow、Duality、Participation、Fulfillment。这样一套模型可以直接作为知识图谱的骨架。
它的缺点也很清楚:图数据库在分布式事务和高并发写入上不如传统关系型稳,很多企业不敢把它放到核心交易链路。我的建议是核心写库用关系型,分析查询和图谱展示用图库,两边通过事件流同步,各取所长。
4.3 REA 和事件溯源、分布式账本的组合
事件溯源(Event Sourcing)是这几年非常火的架构模式,它的核心思想是把系统状态的所有变化都保存为不可变事件日志,需要查询状态时重放事件。REA 和事件溯源天然兼容:REA 告诉你一件事该不该记为事件,事件溯源告诉你事件该怎么存怎么重播。
两个结合之后,事件流就变成了整个系统的“事实源”。业务状态可以从事件流里推导出来,审计日志也顺带完成了。我之前在一个供应链协作项目里试过,把采购订单、发货单、付款单全部拆成 REA 事件,各参与方各自保存事件流,对账时只要比对事件流里的 Duality 是否配对,双方不需要反复传 Excel 表。
分布式账本、区块链的原理也类似,只不过把“各存各的日志”升级成了“多方共识一份日志”。跨组织对账、供应链金融场景下,REA 提供业务语义,账本保证不可篡改,两者是很好的互补。但这里要泼一盆冷水:单机或普通企业内部系统没必要为 REA 硬上区块链,成本高、吞吐低、运维复杂,关系型加事件表已经能覆盖绝大多数需求。
5. 避坑实录:可能踩烂的四个坑
5.1 把 REA 等同于事件溯源
这是我见过最多的混淆。REA 是语义模型,解决“业务事实应该怎样表达”;事件溯源是工程架构,解决“事件日志如何成为系统真相”。两者能结合,但不是一回事。一个团队如果只上了事件溯源,但没有 REA 的语义约束,事件会越记越乱,最后变成一堆无法归类的流水账;反过来只学 REA 不用事件溯源,那它只是一个数据模型,写操作还是老一套状态更新。
我的经验是:先把 REA 模型画出来,确认哪些是事件、哪些是资源,再决定是否引入事件溯源。不要让架构反过来倒逼语义。
5.2 事件粒度失控
事件粒度是 REA 建模最头疼的问题。切太细,比如“用户点击了按钮”也算事件,系统会变成一场灾难;切太粗,比如把整个订单生命周期当作一个事件,经济语义又丢没了。
我的判断标准是三条:是否引起资源数量或控制权的实质变化,是否对财务核算有直接影响,是否会被多个流程共享引用。三条里命中任何一条,才值得建模成事件。“支付成功”和“发货完成”是事件,“页面浏览”“点击加入购物车”不是。建模型前把这个标准讲给产品经理听,能少吵很多架。
5.3 从科目表迁移到 REA 时主键混乱
老系统里通常已经把流水记成了子表,业务单据是主表,流水挂在单据下面。迁到 REA 后事件变成了主表,原来单据上的多个流水要拆成多个独立事件,这时候主键不能沿用老的流水号,因为同一单据会拆出多条事件。
正确做法是建新的事件 ID,同时保留老单据号和流水号作为业务追溯字段。如果直接拿老流水号当主键,退货、部分退款、拆单发货这些场景会直接撞主键。我在真实项目里踩过这个坑,返工了两天。
5.4 订单状态机可以和 REA 共存,但别揉在一起
REA 适合做事实记录和对账,但不适合做面向用户的复杂状态流转展示。用户端通常希望看到“待支付、已支付、待发货、已发货、完成、已退款”这种状态机;REA 的事件流里没有单一的状态字段,状态是需要从最新事件推出来的。
强行用 REA 事件流替代状态机,会让 C 端接口变得非常别扭。我在项目里采取的做法是:写入链用 REA 事件作为唯一事实来源,面向展示层单独维护一个读模型的状态字段。事件流负责真相,读模型负责效率,两者通过订阅同步。这不是违背 REA,而是合理的架构妥协。
6. 延伸思考:rea 与领域驱动设计、微服务的联动
6.1 DDD 聚合根怎么对齐 REA
领域驱动设计(DDD)现在几乎是中大型业务系统的标配,但很多团队在划聚合根时没有充分参考语义模型。一个订单聚合根如果只是把订单头、订单明细、收件地址揉在一起,那它仍然是纯粹的 CRUD 结构。
REA 帮助我重新审视聚合的边界:订单本质上是“契约+一组待执行事件+一组已执行事件”的组合。契约是根,事件是内部实体,资源变化通过事件发布出去。这样设计之后,订单状态变更会变得异常清晰,因为所有状态变更都对应一个具体事件。聚合之间通过事件通信,比如支付服务发布“支付成功”,库存服务订阅后发布“库存扣减”,订单服务再订阅更新状态,链路清清楚楚。
6.2 用 REA 指导微服务拆分
微服务拆分最怕按页面拆、按团队拆,拆完服务间到处联调。REA 给了另一个视角:服务的边界应该围绕“资源和影响资源的事件”来划分。库存服务管库存资源和入库出库事件;支付服务管资金资源和收款付款事件;订单服务管契约和履约跟踪。
每个服务内部可以是一个独立的 REA 子模型,服务之间通过事件传递资源变更事实。这样整个平台的业务语义是一致的,谁也不会再问“这个字段是余额还是发生额”。跨服务链路追踪也简单了,只要事件 ID 贯穿全程,数据血缘一查就出来。
6.3 数据权限和审计日志的额外收获
很多团队做数据权限做得非常痛苦,要给每个按钮配权限、给每行数据配归属。REA 模型天然记录了“谁在什么时间对什么资源做了什么事件”,你根本不需要额外设计一张审计日志表,事件表本身就是审计日志。
做数据权限时,你只要知道当前登录人对应的 Agent 类型,以及他参与的 Event 范围,就可以控制他可见的资源范围。这个思路在财务、医疗、政务类的合规系统里非常实用。事件表那几行代码,直接省掉了一套独立的权限审计模块设计。
我个人的体会是,REA 并不是银弹,但它是少有的“语义和实现能对上”的业务建模框架。项目期初多花几天把资源、事件、主体梳理出来,后面写查询和做对账会节省大量时间。做企业应用的朋友,尤其是做订单、库存、财务这几块的,值得拿一个小模块先试试,用过一次再回传统模式,你会明显感觉到差别。