我知道写这种只有零星线索的技术分享时最怕什么:要么硬凑一篇水货,要么东拉西扯把读者带沟里去。看到“rea”这个项目名的时候,大多数人会以为是某个开源库名字被截断了,或者干脆是随手起的代号。但对我来说,这三个字母首先跳出来的是一套在业务系统数据建模里非常重要、却又常常被人忽略的思考框架——REA(Resources-Events-Agents,资源-事件-代理)。
这套框架不挑行业,不绑定任何具体技术栈,卖点只有一个:把业务系统里的“账”和“事”捋清楚,让数据模型能真正反映业务是怎么发生的,而不是只告诉你余额平不平、勾稽对不对。这篇文章就围绕REA模型的核心思想、落地步骤、容易踩的坑和我自己的实操经验展开,适合正在设计业务数据库表结构、做企业后台系统规划,或者被一堆借贷分录搞得头大的开发者、产品经理和架构师阅读。
1. 只有三个字母的“rea”:我为什么把它解读成一套建模思想
我第一次看到“rea”这个项目名时,同事说是某个内部工具,文档空白,代码仓库里只有几个空目录,连README都没写。当时本想直接略过,但出于好奇查了一圈资料,结果挖出了一套上世纪八十年代就提出来的业务建模框架:REA。它的全称是Resources-Events-Agents,翻译过来就是“资源-事件-代理”。这套思想最早出现于会计学领域,目的不是教你记账,而是回答一个更根本的问题:一个企业的经济活动到底是什么?如何在信息系统里把它完整、真实地描述出来?
很多人一听到“会计学出身”就劝退了,觉得这跟程序员没多大关系。但这个观念恰恰是错的。传统会计模型解决的是“账怎么记”,核心关注借贷平衡、科目汇总、报表披露。而设计业务系统时,我们需要回答的往往是“业务怎么跑”——订单从哪来,货怎么发,钱怎么收,库存什么时候变动。这两者的关注点不一样,底层的数据结构自然也不一样。
REA的思想本质上是把业务过程拆成三个基本元素:资源、事件和代理。
- 资源(Resource):有价值、可被识别和追踪的东西,比如商品、现金、存货、服务工时、应收账款。
- 事件(Event):对资源造成“流入”或“流出”的业务动作,比如订购物品、收到货物、支付货款、发货给客户。
- 代理(Agent):参与事件的人或组织,比如客户、供应商、销售员、仓库管理员、财务人员。
组合起来就是:谁,在什么时候,通过什么事件,让什么资源发生了变化。这句话听起来像废话,但它能直接指导你设计数据库的表结构、关联关系甚至微服务的领域边界。我后来在很多项目里尝试用这套思路重新梳理旧系统,发现那些纠缠不清的“中间表”“状态表”和“流水表”,其实都可以回归到这三个词上面。
如果你手头的项目名恰好也叫“rea”之类让人摸不着头脑的缩写,不妨先停下来问一句:它背后有没有可能指代某种成熟的领域模型或方法论。很多时候,一个简短到极致的名字,背后反而藏着一套可以复用的思维资产,比代码本身值钱得多。
2. 传统业务数据模型的两个“内伤”:账平了,事却丢了
为什么要放着成熟好用的“凭证+分录+科目”模式不用,非得引入一套新的框架?这是我在实践中面对最多质疑的地方。为了把REA的价值讲清楚,我先说说传统建模方式的两个内在缺陷,它们不是谁的代码写得不好,而是范式本身决定了有些信息存不进去。
2.1 内伤一:只记录“记账结果”,不记录“业务原因”
传统财务系统的经典结构是:凭证(Voucher)→ 分录(Journal Entry)→ 科目(Account)。销售一笔货物,财务做一笔收入确认的凭证,借应收账款、贷主营业务收入;发货时再做一笔结转,借主营业务成本、贷库存商品。这套结构经过上百年的优化,逻辑严谨,审计友好,但如果你是一名软件开发者,想从这套结构里反向推导出“哪位客户在哪个时间点通过哪位销售员订购了哪种商品”,就会非常痛苦。
原因很简单:凭证里没有也不需要保存这些业务语义。凭证只保存了金额、方向、科目,至于这笔收入对应的是哪张订单、哪份合同、哪次发货,往往要通过一张“辅助核算表”或者一堆备注字段去维护。维护得好还行,维护不好就直接崩了。我见过太多系统里,订单表和凭证流水完全脱节,财务那边对账对不上,最后只能靠人肉补凭证,每季度末财务同事都要加班。
这种模型的核心问题是:它只存储了业务活动发生后的“会计视角结果”,而把业务活动发生时的“事件链条”剪碎然后丢掉了。
2.2 内伤二:把“状态”当成了“事实”,时间维度和责任主体丢失
另一个常见问题,是传统数据模型极度依赖“当前状态”。比如库存表存一个当前数量,订单表存一个当前状态(待付款/已付款/已发货/已完成),客户表存一个“累计消费额”。
听起来没问题,但运营想分析“这个季度我们到底发过多少次货、平均每个订单隔多久才发货,客户一般从哪里知道我们并且最终成单”时,这个模型就哑火了。因为“当前状态”是结果,不是事件。一次扣库存可能由下单、取消、退货、盘亏四种原因引起,但你只存了“现在剩多少”,没有存“为什么发生变化,是谁在什么时候操作了这次变化”。
这正是REA模型想要解决的核心问题:把业务数据从“记账结果”还原为“事件流”,并且给每个事件挂上资源变动和参与代理。在我看来,现代业务系统的数据底子如果只用传统借贷模型打底,就像只给汽车装了仪表盘而没装行车记录仪,能显示数字但复原不了过程。
3. REA的三个核心要素与一次订单的全链路拆解
前面说了那么多理论,现在用一个我在模拟项目X里反复使用的例子,把REA三要素拉出来遛一遛。场景很简单:客户A向公司订购了100件商品,销售员B接下订单,仓库C发货,财务D收款。放在传统表结构里,这可能涉及订单主表、订单明细表、出库单、收款单、发票,外加一堆汇总统计表。
换成REA视角,整个链条被重新切成六个组成部分。
业务事件链可以拆成四个事件:
- 订购事件:客户A提交订购意向,触发销售员B和客户A的互动;
- 发货事件:仓库C按订单发出100件商品,资源(库存商品)流出;
- 收款事件:客户A向公司支付货款,资源(资金)流入;
- 开具发票事件:财务D将订购、发货、收款关联起来,形成一条完整证据链。
资源只有两个主角:
- 库存商品:从一家供应商采购后进入公司,随后在发货事件中流出给客户;
- 资金或应收账款:在收款事件中流入公司,最终变成可用的银行存款。
代理也比较清晰:
- 内部代理:销售员B(参与订购)、仓库C(参与发货)、财务D(参与收款、开票);
- 外部代理:客户A(参与订购、收款)。
把上面这些内容放进一张表里,对比就很直观:
| 要素 | 传统模型中的体现 | REA模型的体现 |
|---|---|---|
| 订购 | 订单表,保存客户、商品、价格,状态可能混乱 | 订购事件,记录“谁向谁表达了购买意向”,关联销售员和客户两个代理 |
| 发货 | 出库单 + 库存扣减,只记结果 | 发货事件,记录库存商品资源流出,关联仓库代理与客户代理 |
| 收款 | 收款单 + 银行流水,和订单关系靠手工关联 | 收款事件,记录资金资源流入,关联付款方和收款方代理 |
| 资源 | 商品表、库存表、科目余额表 | 库存商品、资金是两个有独立标识的资源节点 |
| 责任 | 字段里写“负责销售的”,无法结构化 | 代理节点通过事件与资源关联,谁操作了什么一目了然 |
看到这里你应该已经能感受到REA和传统建模的差异:它不是不要“账”,而是把账单降到了“事件”的附属产物;它不是不要“状态”,而是把状态视为“事件序列执行后的推导结果”,随时可以重算。
我在实际设计这个模拟项目时,表结构大概长这样:
CREATE TABLE resource ( resource_id BIGINT PRIMARY KEY, resource_type VARCHAR(32) NOT NULL, -- INVENTORY / CASH / RECEIVABLE resource_name VARCHAR(128) NOT NULL, unit VARCHAR(16) ); CREATE TABLE agent ( agent_id BIGINT PRIMARY KEY, agent_type VARCHAR(32) NOT NULL, -- INTERNAL / EXTERNAL agent_name VARCHAR(128) NOT NULL ); CREATE TABLE event ( event_id BIGINT PRIMARY KEY, event_type VARCHAR(32) NOT NULL, -- ORDER / SHIP / PAY / INVOICE occurred_at TIMESTAMP NOT NULL, description VARCHAR(255) ); CREATE TABLE event_increase ( event_id BIGINT NOT NULL, resource_id BIGINT NOT NULL, quantity NUMERIC(18, 4) NOT NULL, direction VARCHAR(4) NOT NULL, -- IN / OUT PRIMARY KEY (event_id, resource_id) ); CREATE TABLE event_participation ( event_id BIGINT NOT NULL, agent_id BIGINT NOT NULL, role VARCHAR(32) NOT NULL, -- SELLER / WAREHOUSE / PAYER / PAYEE PRIMARY KEY (event_id, agent_id) );这套结构的重点是:资源数量增减和代理责任关系全部通过事件来建立,事件之间还可以通过“转换关系”(比如发货事件消耗掉了一次订购事件的库存商品)串联成链条。后来我需要做“某客户所有历史订单轨迹”的查询时,不需要在几十个表里做JOIN,直接沿着事件流扫一遍就出来了,数据一致性和可解释性比老系统强得不是一星半点。
4. 从旧库到REA模型:我走过的迁移三步走
理论讲完,很多人会问:手上已经有一堆订单表、库存表、凭证表了,总不能推翻重来。我的观点是:REA不需要你炸了老系统,它完全可以作为“中间语义层”在旧库基础上逐步生长。我给自己总结了一套三步走的迁移路线,在模拟项目X里验证过,效果还不错。
4.1 第一步:盘点业务事实,划定“事件边界”
迁移之前先不要碰数据库,而是和业务方一起捋清楚每个业务动作到底是不是一个“事件”。最简单的判断标准:这个动作是否造成了某个资源的流入或流出?是否有一个明确的责任代理?如果两个条件都满足,它就是一个事件;如果只满足部分条件,它可能只是某个事件下的“步骤”。
在模拟项目X里,我最初把“提交报价单”也当作一个事件,后来发现报价单没有产生任何资源变化,也没有实际责任代理,只是订购事件发生前的一个信息准备过程。改成事件的附件属性之后,整条链路简单了很多,也不会误导后续的分析查询。这个判断过程很重要,划错了边界,后面整棵树都是歪的。
4.2 第二步:建立“事件-资源-代理”的ERC矩阵
我会为每个核心事件画一个ER图,但不是画那种复杂的UML图,而是写一个简单的矩阵,列名是资源(流入/流出)和代理(参与方)。
比如“发货”这一行:
- 流出资源:库存商品
- 流入资源:(货权转移到客户,但此时还没变成现金,所以是应收账款减少,也可以建模成资源责任转移)
- 参与代理:仓库管理员、承运方、客户
每写一行的过程,本质上就是在做业务规则的重新核对。你会发现很多旧系统里“默认不用管”的规则,在这个矩阵里必须给一个明确的位置。比如“退货”,它在旧库里可能只是库存表和应收账款的回冲,但在REA矩阵里是一个独立的反向事件,有独立的代理和责任链。这一步做完,业务方反而比过去更清楚自己的流程到底是怎么走的了。
4.3 第三步:逐步写对账脚本,用事件流替代陈旧口径
迁移不需要一个晚上完成。我的做法是先并行跑一段时间——旧系统继续作为操作系统的数据来源,REA事件表则作为分析底座,每周写几个对账脚本,比对两边库存数量、应收余额和期间销售总收入。
对账脚本其实很简单,核心就是:REA模型里每个资源的期末数量 = 初始数量 + 所有流入事件累加 – 所有流出事件累加。只要这个等式始终成立,就说明事件数据没丢、方向没反、数量没错。和传统科目的试算平衡表逻辑一致,但语义丰富得多,因为每一行差异都能直接“钻取”到对应的具体事件和代理。
当连续跑了一个月,两边数据全部对得上之后,我再逐步把业务系统里的关键读写路径切到REA模型上,老表慢慢降级为归档表。整个过程没有一秒钟停机,也没有一次手工补录。
5. REA落地中最容易踩的四个坑:每条我都付过学费
即便方法论再完善,实际落地的时候还是会有各种意想不到的坑。我把自己和身边朋友在几个项目里踩到的问题整理成了四条,每条后面都附上了我的处理经验,希望能帮你少走一些弯路。
5.1 坑一:把“实时库存”当成资源表来建
这是最容易犯的错误。有人一听说REA要记录资源增减,直接在数据库里建了一张“库存余额表”,每次发货就UPDATE一下。表面看是资源事件驱动了,实际上是换汤不换药——余额表一旦并发更新,照样丢更新、照样难以追溯。
真正的做法是:资源余额永远是“视角”,不是“事实”。事实只有一条,就是事件表里的流入流出记录。任何时间点的库存量,都应该通过“期初量 + Σ流入 – Σ流出”来推导。如果你担心每次都全表聚合太慢,可以单独建一张“物化视图”或“汇总快照”作为查询缓存,但写入路径一定要走事件表,不要让业务代码直接去改余额。
5.2 坑二:把“文档状态”和“资源变化”混为一谈
订单状态、物流状态、审核状态这类“流程状态”很容易被新手当成资源来建模。比如“待发货”“已发货”“已签收”,看起来像资源状态,但本质上它们是流程节点,不是有价值且能转移归属的资产。
正确的处理方式是把流程状态设计成“事件时间线”上的一个派生属性:当发货事件被写入,订单流程状态自然变为“已发货”;当签收事件被写入,变成“已签收”。不要在资源表里维护一个“is_shipped”布尔值,否则你会陷入无穷无尽的状态同步和状态机维护。真正的模型应该由事件顺序决定状态,而不是反过来。
5.3 坑三:代理建模做不彻底,外部代理和内部代理混在一张表里
代理并不只有“客户”这类外部角色。仓库管理员、操作员、审批人都是代理,他们在事件里承担的责任和权限完全不同。如果把“销售员”和“客户”都塞进一个agent表,看起来方便,但后续做权限控制、责任倒查、绩效分析时,需要频繁区分代理类型,查询条件会越写越重。
我的经验是:agent表可以共用,但角色要单独拆出来,事件参与关系里必须有明确的role字段。宁可多写一点数据约束,也不要为了省事把角色语义塞进备注里。
5.4 坑四:试图用REA覆盖所有业务,包括配置数据和纯流程数据
REA是为了描述“价值交换活动”而生的,它不适合描述所有后台数据。比如用户登录日志、页面埋点、操作日志,这些事件没有资源流入流出,也没有典型的代理责任关系,强行套用REA会让表结构变得极其奇怪。同理,系统参数配置也只是一些静态信息,不需要资源事件代理三件套。
我在模拟项目X里划了一条明确边界:凡是涉及“商品、钱、资产、权利、义务”变动的业务活动,走REA建模;凡是系统内部日志和配置类的内容,继续用普通的关系表模型。这样既享受了REA在核心业务链路上的可追溯性,又避免了过度设计带来的复杂度。
6. 到底什么场景适合用REA:掂量完再动手
最后聊一个很现实的问题:不是每个项目都该上REA。作为方法论,它有非常强大的反直觉价值,但也有学习成本和建模成本。我自己现在的判断标准有三条:
第一条,业务链路上是否存在多个角色参与和多次资源转移。比如电商、供应链、分销、金融支付、物流管理,都适用;一个小型的内部通知发布系统,完全不适用。
第二条,是否存在强烈的审计追溯和纠纷仲裁需求。如果我需要回答“三个季度前的某笔库存变动是谁在什么背景下发起的”,REA会帮你节省大量排障时间;如果业务方只看看汇总报表,传统模型更快。
第三条,团队是否愿意投入磨合成本。REA不是一天能上手的,它要求建模者对业务本身有深入理解,而不是对着标准SQL模板填空。团队如果习惯拿“老系统怎么建我就怎么建”的思路做事情,强行上REA大概率会半途而废。
如果三条全中,我的建议是认真研究REA,它不是学术玩具,而是可以落地的设计范式;如果只中一两条,也可以只在局部核心模块引入,比如订单履约和结算模块,不必全套铺开。在我做过的多个项目里,最成功的往往不是“全系统REA化”的项目,而是把REA用在最关键价值交换链路上的项目,范围小,见效快,业务和研发都容易接受。
我自己现在接手新系统设计时,已经习惯先把业务事件链条画出来,再考虑要不要上REA。哪怕最终没有全量采用,那一张“事件-资源-代理”矩阵表也足以让我比过去更快地发现旧模型里的设计漏洞。这套思路就像一把好用的尺子,测过了,才知道原来那些“理不清的数据关系”其实可以这样井井有条。