1. 从“rea”这个标题说起:一上来就猜错方向
看到“rea”这个标题,我盯着看了好几秒。第一反应是英文单词 read 少了个 d,又像是某个前端框架的残影。但在业务系统这块待久了,我条件反射把它补成了大写:REA,也就是 Resource、Event、Agent,资源、事件、代理人。这不是一个最近才火的新框架,而是业务建模领域里一个非常经典的思想框架。这篇文章里,项目代号就用小写的 rea,我想借这三个字母,把一套能直接用在后端设计、业务建模、甚至数据库表结构设计中的方法论完整讲透。
如果你正在做管理系统、交易系统、库存系统,或者被复杂业务流程的表结构反复折腾,那这篇内容值得看下去。它不会给你一个现成脚手架,但会给你一套“遇到业务先别急着建表”的思考顺序。这套顺序练熟了,很多以前要反复改表、补字段的问题,能在设计阶段就避开。接下来我会从概念讲起,用一个模拟的库存管理模块带你完整走一遍建模过程,最后把实操里踩过的坑和排查思路一起整理出来。
2. REA本体模型的核心逻辑:资源、事件、代理人
很多第一次接触 REA 的人,容易把它理解成“把表分成资源表、事件表、代理人表”的模板。这个理解方向没错,但视野太窄了。REA 最初来源于会计信息系统领域,解决的问题是:传统复式记账只关注“借和贷”,却丢失了业务语义。比如传统账本能告诉你账上少了 100 块钱,但回答不了“这笔钱是因为哪次销售出去的”“那次销售对应的履约事件是什么”。REA 把现实世界里的经济活动拆成三类核心要素:资源、事件、代理人,用它们把业务因果链完整保留下来。
我们先说为什么需要它。平时做系统,最常见的设计思路是围绕单据建表:订单表、入库单表、出库单表、库存表。每个表一个状态字段,每次业务变化就 update 一行,再记一条流水。这种做法好处是直接,坏处是时间一长,你很难回答“为什么这个库存变成了这个数”。因为你只保存了结果,没有保存导致结果的过程。REA 的逻辑刚好反过来:它要求系统把“发生了什么”当作第一公民,把“当前状态”当作可推导出来的结果。这样,每一次数量变化都能回溯到具体事件,每一个事件都能关联到具体的人和资源。
2.1 资源、事件、代理人分别代表什么
先说资源。REA 里的资源不是泛指数据库里的一张表,而是指那些有稀缺性、能被控制、并且参与经济活动的对象。现金、库存商品、服务工时、积分余额,这些都是资源。注意,一张业务表里可能藏着多个资源。比如一张订单表里,既有“商品资源”的减少,又有“应收资金资源”的增加。如果你只把订单当成一张表,很容易忽略资源维度的变化。
再说事件。事件是资源增加或减少的那个“瞬间”,是一个已经发生的事实。采购到货、销售出库、盘点确认、报损出库,这些都是事件。事件不等于操作日志,“用户点击了按钮”“管理员审核通过”这些如果没引起资源增减或义务变化,就不应该和经济事件混在一起。
最后是代理人。代理人指参与事件的主体,可以是个人、部门、公司,甚至可以是外部系统。比如供应商、客户、仓管员、仓库本身。为什么要把仓库也当代理人?因为“仓库”在库存领域里负责承载和转移资源,把它作为代理人,调拨、盘点这类涉及两个仓储方的事件才能被清晰地描述。
2.2 三条核心关系,把概念串成业务闭环
光有资源、事件、代理人三个概念还不够,得把它们关联起来,REA 才有建模能力。它主要定义了这么几条关系:
第一条是资源到事件的关系。一个事件必然影响至少一个资源,要么增加,要么减少。而且资源变化通常成对出现,比如销售出库事件里,商品资源减少,对应的现金或应收资源增加。
第二条是事件到代理人的关系。每个经济事件至少关联一个代理人,回答“谁参与了这件事”。有了这一条,系统才能追溯责任。比如入库事件要关联供应商和仓管员,出库事件要关联客户和拣货员。
第三条是事件到事件的关系。经济事件之间需要成对出现,形成经济交换或经济转化。传统账本里“有借必有贷”,REA 里则是“有这个事件,必然有对应的那个事件”。一个采购到货事件,对应一个付款事件;一个销售出库事件,对应一个收款事件。这类关联让对账变得自然。
在上述基础上,REA 还经常提到另一种关系叫控制关系,也就是“承诺”和“执行”。客户下单是一个承诺事件,发货和付款是执行事件。承诺事件和后续执行事件之间的关联,可以拿来支撑“已下单未发货”“已发货未付款”这类业务预警。这个稍后会在案例里展开。
用生活化的话说,REA 就像一个卖菜过程:资源是菜和钱,事件是“你把菜递出去”和“对方把钱付过来”,代理人是你和顾客。传统记账只记录“菜少了,钱多了”,REA 还会追问:这两个事件是不是同一笔交易承诺的?谁负责的?如果菜少了钱没到,问题出在哪个环节?这种追问能力,恰好是复杂业务系统最缺的。
3. 用rea改造一个库存管理模块:从零开始的建模实操
概念讲多了容易飘,下面我们落到具体场景里走一遍。某公司有个模拟项目 X:一个小型电商平台的库存中心。需求并不复杂,多仓库、SKU 库存、采购入库、销售出库、仓间调拨、定期盘点、安全库存预警。如果按传统方式设计,大概率会先画一张库存表:仓库 ID、SKU ID、数量、占用数量、更新时间。然后每次入库出库都 update 同一行,再顺手写一张流水表用于审计。
这种方案的毛病会在后期暴露出来。调拨时要先减后加,中间一旦失败,两边数据对不上;盘点差异不知道该怎么入账;业务方问“这个库存为什么少了两件”,你得去翻流水日志,靠时间猜原因。用 REA 来做,步骤完全不同。我们不从表结构出发,先从业务事实出发,识别清楚资源、事件和代理人,再回推数据模型。
3.1 先识别代理人、事件、资源
第一步,列代理人。这个模块里涉及的主体有供应商、客户、仓库、仓管员。注意,仓库本身也要作为代理人,因为“调拨出库”和“调拨入库”这两个事件发生在不同仓库之间,没有仓库这个主体,事件关系说不清楚。
第二步,列事件。库存领域核心事件包括:采购到货、销售出库、调拨出库、调拨入库、盘点确认、报损。这些事件都有一个共同特点:它们造成了某种资源的增加或减少,属于经济事件。
第三步,列资源。在这个模块里,资源的核心是“各仓库中的可售库存”。为了处理调拨和在途场景,可以进一步细分:源仓库中的商品库存、目标仓库中的商品库存,以及“在途库存”这个资源。还有资金、应收应付这些资源属于资金模块,这里先不扩展。
把这三个维度放在一张表里,领域元素就清晰了:
| 环节 | 涉及资源 | 核心事件 | 关联代理人 |
|---|---|---|---|
| 采购到货 | 在途库存转为仓库库存 | 采购到货 | 供应商、仓管员 |
| 销售出库 | 仓库库存减少 | 销售出库 | 客户、仓管员 |
| 仓间调拨 | 源仓库减少、目标仓库增加、在途库存过渡 | 调拨出库 / 调拨入库 | 源仓库、目标仓库、仓管员 |
| 定期盘点 | 仓库库存与账面核对 | 盘点确认 | 仓管员 |
| 报损 | 仓库库存减少 | 报损出库 | 仓管员 |
3.2 用事件流取代库存流水表
把要素识别出来后,最关键的一步来了:库存数量变化不应该直接改一张“库存余额表”,而是通过追加事件来记录。每个事件里,资源通过明细行表达变化方向和数量。这个思路和我们熟悉的“流水表”很像,但多了两个约束:一是事件必须关联代理人,二是事件之间必须能组成成对的关联。正是这两条约束,让库存数据具备了完整因果链。
具体落表时,可以这样设计。代理人表保存所有参与主体,资源表保存品类和所在库位,事件表保存“发生了什么”,事件明细表保存“哪些资源变化了多少”。下面是一份可运行参考的 SQL 结构:
CREATE TABLE agent ( agent_id INT PRIMARY KEY, agent_type VARCHAR(32), -- supplier / customer / warehouse / user agent_name VARCHAR(128) ); CREATE TABLE resource ( resource_id INT PRIMARY KEY, resource_type VARCHAR(32), -- inventory / in_transit_inventory warehouse_id INT, -- 所在仓库 sku_id INT ); CREATE TABLE business_event ( event_id INT PRIMARY KEY, event_type VARCHAR(64), -- purchase_arrival / sale_issue / transfer_out / transfer_in occurred_at TIMESTAMP, from_agent_id INT REFERENCES agent(agent_id), to_agent_id INT REFERENCES agent(agent_id), exchange_id INT -- 用于关联成对事件,例如采购到货配对付款 ); CREATE TABLE event_line ( line_id INT PRIMARY KEY, event_id INT REFERENCES business_event(event_id), resource_id INT REFERENCES resource(resource_id), quantity NUMERIC(18,3), direction SMALLINT -- 1 表示资源增加,-1 表示减少 );为什么用两条表结构存事件和明细,而不直接在事件表里写数量和方向?因为一个事件可能同时影响多种资源。比如“产成品入库”事件,可能涉及多个 SKU 的增加;“生产领料”事件,会涉及多种原料的减少。如果每种资源一行明细,查询和聚合都更灵活。
这里要额外解释一下 direction 字段。方向是相对于资源而言的:入库事件让库存资源增加,direction 为 1;销售出库让库存资源减少,direction 为 -1。不要把“入库”“出库”这种字面词硬编码成方向。比如“报损出库”,它让库存减少,direction 就是 -1。只要把事件类型和方向分离,后续统计逻辑就能统一写成一个“按资源累加方向乘数量”的公式。
3.3 库存投影表和增量回放
有了一张张事件表之后,业务上仍然需要一个“当前库存是多少”的查询结果。这并不矛盾。我们可以维护一张库存投影表,专门存当前可用数量,它通过事件明细聚合而来。关键在于,投影表永远是派生物,不是事实源。事实源是事件。
投影表的大致结构是:仓库 ID、SKU ID、当前数量、更新时间。每次新事件产生,可以增量更新这张投影表。增量更新的 order 很简单:
- 新事件写入 business_event;
- 新明细写入 event_line;
- 更新投影表,对每个 resource_id 执行“当前数量 = 当前数量 + direction * quantity”;
- 如果有安全库存阈值,再检查投影表里的最新值,触发预警。
这个回放逻辑还可以用来做“某个时点的库存情况”。比如想查看上周五的库存,只需要把截止时间之前的所有事件重新聚合,或者定期打快照。真实性比单纯看一张 update 出来的库存表高得多。
3.4 事件之间的关系怎么落
前面 SQL 里我加了一个 exchange_id 字段,这是 REA 落地时容易被忽略的环节。销售出库事件和收款事件,采购到货事件和付款事件,需要在数据上建立关联,否则“货到款未付”这类风险就无法系统化识别。
最直接的做法是在 business_event 表上增加 exchange_id,同一次销售闭环里的事件共享一个 ID。更规范一点的做法是单独建一张事件关联表:
CREATE TABLE event_link ( link_id INT PRIMARY KEY, event_id INT REFERENCES business_event(event_id), related_event_id INT REFERENCES business_event(event_id), link_type VARCHAR(32) -- exchange / fulfilment / conversion );这样做的好处是,不需要往事件表里塞太多业务字段。事件表保持“发生了什么”的纯粹性,复杂的业务关系单独表达。
4. 实操中经常踩的坑:REA建模的典型问题与排查
REA 看着简单,真要在项目里落地,会遇到不少细节问题。下面这些问题,我几乎在每一次按 REA 建模的项目里都见过。提前知道,能少走很多弯路。
4.1 把状态字段当成事件
这是最普遍的问题。团队理解了“要把事件记录下来”,然后就开始把一切操作都往事件表里塞。“保存了草稿”“审核通过了”“打印了标签”也变成了一条条事件。这会让事件表迅速膨胀,但真正能用于资源计算和业务关联的事件反而被淹没。
判断标准其实很清晰:这条记录是否导致了资源数量增减,或者改变了某个经济义务?如果只是改了单据状态,那它是“状态变化”,不是 REA 经济事件。比如“审核通过”这个动作,它本身不会直接改变库存或资金,因此不应该作为独立事件进入 event 表。正确做法是,在订单聚合里维护一个状态字段,当审核通过时,由某个领域事件触发状态变更,但别把它和“出库事件”混在一起。
4.2 漏掉事件之间的成对关系
有的团队建了事件表,也记录了库存增减,但每笔业务事件都是孤立的。销售出库记录归销售,收款记录归收款,两边没有共同标识。到了对账的时候,只能靠“客户 ID + 金额”去近似匹配,非常痛苦。
REA 强调的是“有借必有贷”的业务版本。销售出库必然对应应收或收款,采购到货必然对应应付或付款。实操中,我都会要求每笔核心交换事件带上 exchange_id 或事件关联表。一旦带上这个字段,未发货订单、未付款订单的统计都会变得非常简单。
4.3 把 REA 当作数据库设计工具,而不是沟通工具
REA 最大的价值其实不在表结构,而在团队语言。业务方说“库存不对”,你光看库存投影表,只能知道结果不对。但如果你和业务方一起画出事件链:哪个资源、哪个事件、哪个代理人、哪两个事件是一对,问题定位会快很多。
我有一次和同事排查“账面库存比实际多了 3 件”,最后发现是盘点差异处理时,直接把盘点差额作为一条“调整事件”记录,却没有记录盘点前数量,导致回放出来的数据和真实历史对不上。这不是技术问题,而是业务建模时没有把“盘点”这个事件的完整性想清楚。盘点事件应该记录实际清点数量,差额则是一次独立的事件。该有的关系闭不上环,往往就是需求和规则没有彻底理解。
4.4 常见问题速查表
| 问题表现 | 可能原因 | 处理建议 |
|---|---|---|
| 事件表越来越大,查询越来越慢 | 把操作日志、状态变更都当成了经济事件 | 过滤非经济事件,历史记录单独存储 |
| 调拨时库存被重复计算或丢失 | 把调拨拆成两个孤立事件,没有关联 | 调拨作为一个事件、两条明细;或用事件关联表绑定 |
| 盘点差异无法还原 | 只记差异,不记盘点前后数量 | 盘点事件里记录实际数量,差额单独生成事件 |
| 应收应付对不上 | 资金资源没进模型,付款事件未与业务事件关联 | 补齐资金资源,统一 exchange_id |
| 改了历史事件,报表数据全乱 | 投影表没有增量更新机制 | 引入事件回放,按资源维度增量维护投影 |
| 讨论业务时各说各话 | 只拿 SQL 当需求文档 | 先画资源-事件-代理人的闭环图,再聊实现 |
5. REA建模的边界感和落地心得
在项目里把 REA 用顺手之后,你会发现它不只是用来设计表,还能帮你想清楚系统边界。
5.1 REA与事件溯源的关系很近,但不完全等同
事件溯源也是把每次变化作为事件追加保存,但它强调的是“用事件还原状态”,并没有规定哪些事件值得保存。REA 正好补上了这一层:用资源和义务来判断事件价值。两者结合时,可以在领域事件流里把 REA 经济事件作为核心事件,把表单状态变化作为次要事件。这样既保存了完整业务轨迹,又不会让系统变成什么都记的日志怪。
5.2 用REA辅助划分微服务边界
做微服务拆解的时候,可以围绕“资源”判断归属。同一类资源的经济事件,尽量收敛到同一个服务里,避免跨服务直接修改同一个资源。比如库存服务处理所有库存资源的事件,资金服务处理所有资金资源的事件。当出库事件发生时,库存服务修改完资源投影,再发一条“库存已出库”的消息,资金服务收到消息后再生成应收事件。代理人的概念也能帮助你识别外部系统边界:供应商、客户都可能是第三方系统,它们与主系统的交互点天然就是事件,而不是数据表。
5.3 项目代号rea给后来维护者的一点小建议
如果你真的只是随手起了“rea”这个代号,我的建议是:在项目文档第一行把全称写清楚,例如“REA = Resource Event Agent”。缩写这东西,自己用怎么都好,提交给团队之后,任何模糊的表达都会成为理解成本。给 rea 补上全称,就是在给未来同事降低门槛。
最后再分享一个我实际用下来很顺手的小技巧:开始建模时,别急着打开数据库客户端。先拿白板画张三角图,左边代理人,右边资源,中间事件,然后给每个事件标注“谁,让哪个资源增加还是减少”。每画一笔,就问一次“和它成对的那个事件在哪里”。闭不上环,往往就是业务规则潜藏最深的地方。把这些地方提前挖出来,比上线后对着数据报缺陷要舒服得多。