项目标题抛过来的时候只有三个字母“rea”,我一开始也愣了一下,是某个开源库?还是随手起的代号?把背景补全才发现,这里指的是REA——Resource、Event、Agent的缩写,一套从业务事实出发做领域建模的经典思路。REA模型最早诞生于企业信息系统设计领域,专门用来梳理那些“看起来一团乱麻”的业务流转:有什么资源、发生什么事件、由谁参与,三类东西一归类,整套业务逻辑就能串成一条完整的链。
这些年我做过不少电商、进销存、SaaS类的业务系统,踩过最多的坑就是“模块各自为政”:订单系统说已支付,财务说没到账;库存系统说已出库,仓库说货还在架上。数据对不上、责任说不清,最后全是靠加班对账硬扛。用REA这套建模方式去重新梳理业务后,很多问题在源头就没了。这篇文章我会把REA的核心概念拆开讲透,用一个实际的小型订单库存系统做完整建模演示,结尾整理几个我实操中遇到的真实问题和排查经验。适合做业务系统的后端工程师、架构师,以及想弄明白“领域模型到底怎么画”的产品经理。
1. 项目概述:被三个字母浓缩的建模思路
1.1 项目起源与核心定义
REA模型最早在20世纪80年代被提出,最初是为了重新设计会计信息系统。传统的借贷记账法讲究“有借必有贷,借贷必相等”,这套体系本身没问题,但它把业务事实压缩成了“借”和“贷”两类符号,导致业务人员很难直接从账上看出真实业务情况。REA的出发点很朴素:与其记录抽象的借贷符号,不如记录业务本身发生了什么。
具体来说,REA把一切经营活动拆成三类原语:
- Resource(资源):业务中被交换、被消耗、被生产的有价值的东西。
- Event(事件):业务中实际发生的活动,比如采购入库、销售出库、付款收款。
- Agent(参与者):参与事件的人或组织,包括内部员工和外部客户、供应商。
这个模型不绑定任何具体技术,也不要求你用某种数据库,它是一种思考业务的方式。当你接手一个混乱的业务系统时,先别急着画ER图,先把业务翻译成“资源、事件、参与者”三张清单,很多设计问题会自己浮出水面。
1.2 解决什么问题
传统业务系统设计里,我们习惯按“单据流”组织数据:订单表、入库单表、出库单表、发票表。每张表是一个独立的单据,单据之间通过状态字段衔接。听起来没什么问题,但实际跑起来经常出现三种矛盾。
第一种是状态不一致。订单表维护一个“已支付”状态,支付服务也维护一个“支付单”状态,两边如果不同步,用户可能看到支付成功但订单仍显示待付款。第二种是数据口径不统一。库存表里写了一个可用库存数,但没人能说清这个数是“账面库存”还是“可承诺量”,业务一追问就露馅。第三种是追溯困难。发现某个库存数字不对,想查是哪个环节造成的,结果单据之间的关联字段是断的,查起来要跨五六个系统翻日志。
REA从根上绕开了这些坑。它要求你只记录“事实”,也就是事件。所有状态都由事件推导,所有数字都能从事件明细追溯。比如“当前库存”不是一个随便改的表字段,而是所有入库事件和出库事件累加的结果;“客户欠款”不是一个被写死的数字,而是销售出库事件、收款事件共同作用的结论。这种“事件驱动”的建模方式,在财务审计和系统排障场景下优势极其明显——你能从任意一条汇总数据一路追查到每一笔原始单据。
1.3 适合谁来学
如果你主要做管理后台、ERP、电商交易、供应链系统这类业务,REA非常值得花时间研究。尤其是后端工程师,当产品经理拿着几十个页面原型来找你,每个页面都涉及订单、库存、资金,但你不知道“领域模型”该从何下手时,REA能给你一条清晰的路径。
产品经理和业务分析师同样适用。REA提供了一套和业务人员沟通的语言。跟业务方聊的时候,你会发现他们其实不太关心你数据库怎么建,但当你把“事件清单”列出来问“还有哪件事没记录”时,对方会非常兴奋,因为这就是在还原他们的真实工作场景。这也让跨角色沟通变得顺畅很多。
我个人的实践感受是:REA不是银弹,它不解决性能问题,也不直接生成高并发方案,但它能让团队在“业务到底是什么”这件事上达成一致。这个一致,是后续所有架构决策的基础。
1.4 项目定位与外延
如果把REA当做一个“项目”来看,它的交付物不是软件,而是三份文档:资源清单、事件清单、参与者清单,外加一套从事件推导状态的计算规则。这套规则可以落地到任何技术栈里——关系型数据库、事件流平台、甚至普通表格都可以承载。
这也意味着REA本身并不庞大,它的核心概念只有三个,但它延伸出的管理思想非常深。你会慢慢发现,在REA视角下,很多业务系统里看似复杂的模块——库存、应收应付、毛利核算、预算控制——其实都是在回答同一个问题:哪个事件,改变了哪个资源,谁参与了。
2. 核心元素拆解:Resource、Event、Agent
2.1 Resource,资源的判断标准与易混淆点
资源是业务里“有价值的东西”,听起来很简单,但落地时经常判断错。比如“订单”是资源吗?订单只是个单据载体,承载的是“商品”“服务”“钱”这些真正有价值的东西。又比如“客户”是资源吗?客户更准确地说是参与者,真正能作为资源的是“客户资产带来的未来收益”,这个太虚,一般不建模。判断一个东西是不是资源,我用三个标准:能不能量化、有没有归属、会不会被事件改变。
- 能不能量化:库存数量3.5吨、现金余额20000元、积分数1000分,都可以量化。
- 有没有归属:现金属于公司,库存属于某个仓库,应收账款属于某一个客户。
- 会不会被事件改变:入库事件会让库存变多,收款事件会让现金变多,这些是可变的。
同时满足这三条,才考虑在REA模型里建模成资源。否则大概率是单据、参与者的属性,或者是一个应该由事件推导出来的状态。
有个典型误区是混淆“资源”和“单据”。很多同学做库存系统时,把入库单单独建了一张表,然后库存数量也建了一张表。两张表都存数量,互相之间对不上。在REA里,入库单不是一个资源,它是一连串“入库事件”的载体。真正值得建模的资源是“物料”和“库存量”,入库单上的数量最终要落到资源上。这带来的直接好处是:系统中不会出现两套互相独立的数字。
2.2 Event,事件为什么是这个模型的核心
事件是REA模型的主角。它记录的是业务中“发生了什么”,并且应具备三个特征:发生在某个时间点、不可随意修改、必须能关联到资源或参与者。特别注意“不可随意修改”,这是事件化建模的底线。现实中业务人员经常说“我把刚才那笔入库单改一下”,但在REA体系里,正确做法不是修改原事件,而是补一笔冲销事件,再做一笔新增事件。
为什么要这么严格?因为业务系统里所有账目的可靠性都依赖“事件的顺序和内容不能被篡改”。如果允许修改历史事件,库存汇总、财务对账、审计追溯全部失去意义。你只改了一个字段,却可能让过去三个月所有对账单全部作废。
事件可以按类型分为两类:交换类事件和经济类事件。交换类事件描述“资源从谁手里到了谁手里”,比如销售出库、采购入库。经济类事件描述“企业内部价值变动”,比如盘点调整、报废报损。两类事件落到表结构里可以统一设计,用event_type字段区分,但建模时建议分开讨论,因为业务含义不同。
从表设计角度看,事件表是整个模型最核心的事实表,不应再被“单据编号、状态”之类的字段污染。每个事件记录应该包括:事件发生时间、事件类型、涉及资源、数量、相关参与者和来源单据号。这些字段足够支撑后续所有汇总和追溯。
2.3 Agent,参与者如何区分内外
Agent代表参与事件的人或组织。参与者的最基本划分是内部与外部:内部参与者比如采购员、仓管员、销售员,他们的责任范围在公司内部;外部参与者比如供应商、客户、物流公司,代表公司以外的主体。
为什么要区分内外?因为内部参与者决定“责任归属”,外部参与者决定“往来关系”。举例来说,一张销售出库单关联的仓库管理员(内部)负责发货这个动作是否正确执行;关联的客户(外部)决定了这笔交易的对账对象和应收归属。二者缺一不可。
一个Agent在系统中也可能有多种身份。比如一家公司既是供应商又是客户,在往来的账上你希望它是同一个主体,方便抵消;但在事件记录上,它分别以“供应商”和“客户”两个角色出现。工程上的常见做法是维护一张agent表,里面区分agent_type,同时保留全局唯一的agent_id,这样后续做对账和信用管理时都能收敛到同一个主体上。这块一开始就要想清楚,不然将来做“同一客户既是买家又是卖家”的业务时,会为去重问题头疼很久。
2.4 三者如何组成闭环:从资源到事件再到参与者
三段内容连起来理解:一条事件,涉及某类资源的变化,同时关联责任方和往来方。比如“采购入库”这个事件,改变了“库存”资源(数量增加),关联了仓管员(内部)和供应商(外部)。这个三元组就是REA的基本语义,也是后续所有设计的细胞。
实际业务里,一个经济过程通常由两个事件配对出现,形成“交换闭环”。比如“销售出库”事件是商品资源流出,紧接着“收款”事件是资金资源流入,两个事件共同完成一笔销售换回资金的价值交换。如果只记录出库不记录收款,系统的资产表和利润表都是不完整的。
用“闭环”的视角审视需求时还有一个好处:你能快速发现需求里缺了哪一半。比如产品经理只说了“客户下单后要扣减库存”,你追问一句“那钱呢?收款事件建模在哪?”,往往能把需求补充完整。这个过程不是抬杠,而是在构建完整的业务事实链。
3. 实操案例:用REA建模一个订单+库存系统
3.1 案例需求与建模准备
这里我拿一个虚拟的小型电商订单库存系统来走一遍完整流程。这个系统不算复杂,但包含了采购、销售、退货、收付款等常见业务动作。需求只有一句话:要能随时回答库房里还有多少可销售商品,客户欠了多少应收款,供应商那边还有多少未结货款。
拿到需求别急着建表。第一步先列事件清单。我会把业务方的日常操作动作全部罗列出来,每个动作就是一个潜在的事件:
- 采购下单
- 采购入库
- 采购付款
- 销售下单
- 销售出库
- 销售收款
- 销售退货
- 退款给客户
清单列完,逐条检查:这个动作是否改变了某类资源?是否有关键参与方?如果两个条件都满足,基本可以确认是事件。比如“采购下单”这件事,在没发生货、没付款之前,只是表达了“意图”,它是否改变资源?严格来说没有。那它要不要建模?我的建议是保留,因为在多步骤采购流程中,订单状态仍有查询价值,但它不宜作为库存变化的依据。相反,“采购入库”直接增加了可用库存,这是核心事件。
3.2 识别核心资源清单
再看资源。从需求出发,涉及价值的对象主要是这几个:
- 商品:可销售的实物货品,是核心资源。
- 库存:表现为某仓库内可用的数量,与商品配套出现。
- 现金:公司账户里的钱。
- 应收账款:客户应付未付的钱。
- 应付账款:公司欠供应商的钱。
这里最容易踩的坑是“订单金额”是不是资源?不是。订单金额是事件发生时的一个属性描述,而不是资源。真正的资源是这笔订单背后将要产生的“应收账款”或“现金流”。所以不要把订单金额直接建模成资源表,应该从“付款事件”和“出库事件”的关联关系中推导出债权债务变化。
3.3 梳理参与者及角色
参与者清单相对直接:
- 采购员(内部):负责采购下单和入库操作。
- 仓管员(内部):负责实物收货、发货。
- 销售员(内部):负责销售下单。
- 供应商(外部):供货与对账的主体。
- 客户(外部):购买与付款的主体。
值得强调的是,客户既参与了销售出库事件,也参与了销售收款事件。在两张事件记录里,agent_id都指向同一个客户主体,但职责不同。建模时建议事件表既记录内部参与者(经办agent_id),同时记录外部参与者(往来agent_id),两个字段都必要。有人在设计时偷懒只保留一个参与者字段,后面做往来对账时会非常痛苦,因为搞不清楚到底是谁、在哪个环节上参与了这笔业务。
3.4 从事件推导数据库设计方案
概念模型清晰后,落表其实很快。下面是我的推荐设计,按REA语义精简成四张核心表。
先建资源表(resource):
CREATE TABLE resource ( resource_id BIGINT PRIMARY KEY, resource_type VARCHAR(32) NOT NULL, name VARCHAR(128) NOT NULL, unit VARCHAR(16) NOT NULL, quantity_on_hand DECIMAL(18,4) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 );quantity_on_hand这里先放着一个数字,它是冗余快照,真正的可信数据要通过事件汇总得到。我后面还会强调,任何直接UPDATE这个字段的逻辑都必须经过事件校验,否则模型会被绕过,数据可信度又回到原点。
事件表(event)是关键,记录每一次业务事实:
CREATE TABLE event ( event_id BIGINT PRIMARY KEY, event_type VARCHAR(32) NOT NULL, occurred_at DATETIME NOT NULL, resource_id BIGINT NOT NULL, quantity DECIMAL(18,4) NOT NULL, quantity_unit VARCHAR(16) NOT NULL, internal_agent BIGINT NOT NULL, external_agent BIGINT NULL, source_doc_no VARCHAR(64) NULL, UNIQUE KEY uk_source_event (source_doc_no, event_type) );quantity统一为正数,靠event_type决定它是增加还是扣减。这样处理比“正数入库、负数出库”的表达更清晰,以后写汇总SQL时用CASE WHEN区分方向即可。external_agent允许为空,是因为企业内部调整事件(比如盘点损益)并不涉及外部往来单位。
资源与事件是一对多关系,同时还需要一张事件关联表,表达事件之间的因果关系:
CREATE TABLE event_link ( from_event_id BIGINT, to_event_id BIGINT, link_type VARCHAR(32), PRIMARY KEY (from_event_id, to_event_id) );“销售出库事件”和“收款事件”通过event_link建立关联,形成一个完整的交换闭环。想做毛利分析时,只需找到同一客户、同一订单的关联事件组合,计算成本和收入即可。这套模型天然支持审计:任何一张汇总报表都能追溯到原始事件。
3.5 实时库存的SQL推导与落库操作
有了事件表,查询库存就是一条汇总语句的事:
SELECT resource_id, SUM(CASE WHEN event_type IN ('purchase_in', 'sales_return', 'stock_check_in') THEN quantity WHEN event_type IN ('sales_out', 'purchase_return', 'stock_check_out') THEN -quantity ELSE 0 END) AS current_stock FROM event WHERE resource_id = 12345 GROUP BY resource_id;这个查询计算结果就是可信的“事件推导库存”。系统运行早期数据量不大,这条SQL完全跑得动;数据量大了之后,可以每天晚上离线汇总成库存快照表,白天读快照,在线校验时再单独计算明细。但无论加不加缓存,事件表永远是最底层的事实依据。
往系统里写入一笔出库操作时,代码逻辑应该是:先校验该商品当前事件推导出的余额是否足够,再写入出库事件,最后更新resource表的冗余快照。如果业务要求高并发,可以把“校验+写入”放在事务里,或使用分布式锁保护同一个资源。重点是:任何状态下库存字段都不能被直接UPDATE,必须“先有事件,后有状态”,否则REA模型对系统数据可信度的保护就形同虚设。
3.6 扩展字段:多仓库、多币种怎么加
实际业务永远比理论模型复杂。多个仓库,每个仓库的库存是独立的,怎么办?方案很简单,给resource表加一个warehouse_id字段,同一种商品在A仓和B仓分别视为两条资源记录。“资源”此时变成了“商品+仓库”的组合概念,逻辑仍然自洽。
多币种场景下,现金和应收应付类资源需要加currency_code字段。销售出库事件中关联一个人民币计价资源,收款事件也关联对应币种的现金资源。如果涉及汇率折算,不要在事件表里既写原始币种又写折算后金额,会造成事实混淆。我的建议是事件里记录原始币种金额,汇率作为独立参考数据,折算过程放到报表层完成。这样底层数据始终是干净的业务事实,汇率调整时不污染历史记录。
4. 常见问题与排查技巧实录
4.1 分不清这个对象是资源还是事件
建模讨论中最频繁的争论就是“某个东西到底算资源还是事件”。我总结出一句口诀:状态是资源,动作是事件。“库存”是状态,所以是资源;“入库”是动作,所以是事件。“余额”是状态,所以是资源;“收款”是动作,所以是事件。
再举个例子,“订单状态”这个东西,常见设计是在订单表里放一个status字段,从待支付改到已支付。REA视角下,正确做法是记录一笔“支付成功事件”,所有统计从事件判断。订单的status字段退化为一个索引缓存。你会发现用事件来表述转型动作后,业务规则的表达力更强:订单不可能被改回待支付状态,因为历史事件不会反过来删掉。
4.2 负库存的出现与处理
负库存大概是库存系统里最常见的脏数据来源。直接原因基本都是事件顺序错乱:出库事件先于入库事件发生。举个例子,商品还在物流途中没入仓,销售订单就先打了出库单,系统一看库存不够,就可能放开限制允许负库存。
从工程角度,我推荐三层防线。第一层,出库之前必须校验事件推导的累计余额足够,不够则阻塞;第二层,允许人工授权强制过账的开关,但强制过账必须记录操作人,同时打上“潜在负库存”标记;第三层,定期跑对账任务,把所有潜在负库存记录拉出来,人工确认冲销。
有一次我遇到线上库存为负,查了一圈发现是上游供应商的“采购入库事件”被回滚了,但销售出库事件没有同步回滚,两笔事实一删一留,库存余额直接变负。这类事故用REA模型可以快速定位:找到对应资源的所有事件,按时间顺序排列,一眼就能看到因果断裂点。这也再次验证了“事件不可变”的工程价值。
4.3 REA模型与财务记账怎么共存
很多同学问:如果公司已经有一套财务系统,用REA建模会不会“抢了财务的活”?不会。REA模型完全可以做底层业务事实库,传统借贷记账法则作为财务口径的映射层存在。
我举一个例子。REA中一笔“销售出库事件”包含商品成本和销售收入两项信息,映射到借贷记账就是:借“主营业务成本”,贷“库存商品”;同时借“应收账款”,贷“主营业务收入”。这个过程可以写成一套规则引擎,每当事件落库时生成对应的会计凭证。财务系统只消费事件数据,不再需要后端业务系统硬编码“转账逻辑”到每个业务动作里。
这样做的好处是,业务系统里任何一笔操作都有据可查,财务数据和业务数据不会出现“两边对不上”的问题。我曾经参与过的一个项目,早期是把会计分录散落在各种业务接口调用里,后来统一收敛到事件表后面的规则引擎中,月末结账时间从一周缩短到一天。
4.4 模型落地时和现有代码结构怎么融合
改造一个存量系统比从零建系统困难得多。很多团队在落地REA时,都想一步到位把所有模块重构掉,结果往往因为周期太长而中途废弃。我的建议是渐进式改造,先挑选1到2条核心链路做事件化。
比如先从库存链路开始:把采购入库、销售出库两件事接入事件表,其他模块继续维持现状。原库存表继续作为读缓存保留,但所有写操作都改为“先事件,后更新库存”。等这个链路稳定运行,再把订单支付、供应商结算等模块逐步迁移进来。
存量数据的迁移也是一个关键动作。已有的历史单据需要补录成事件。补录的时候不要直接按当前时间生成事件,要尽量还原历史发生时间。否则冲销和追溯的语义会发生错乱。
我实际踩过的坑是,消息队列重复消费导致同一笔事件被写入两次,库存凭空翻倍。解决方法是给事件表加唯一约束,用source_doc_no + event_type作为幂等键。这是一个不起眼但极其关键的设计。任何通过异步方式写入事件表的系统,做唯一约束都是必须的动作。相关的表结构设计我在3.4节已经写上了,建议在初始化表结构时就直接加上。
4.5 排障实例:凭空多出来的库存
排障能力是这套模型真正的价值所在。我遇到过一个真实场景:某个仓库的账面库存比实物多出47件。按照REA模型,我做了三步排查:
第一步,找出这个资源ID下的全部事件,按时间排列。第二步,定位到问题发生的时间窗口,发现有一笔“采购入库事件”重复出现了两次,来源单据号完全一致。第三步,通过数据库唯一索引检查,发现接入消息队列之前该表没有做幂等控制,因为业务服务重推了一条消息导致事件重复写入。
整个定位过程不到半小时。如果换成传统“库存表直接减/加”的设计,问题排查可能要翻遍所有接口日志,还未必能锁定根因。这就是REA模型在“可追溯性”上的硬实力。
经过这几轮实践,我个人的体会是:REA不要求你把系统推倒重建,它要求你换一种方式看待业务事实——把不可变事件当作源头,把可变状态当作推导结果。刚开始做时会觉得多了一张表、多了一层逻辑,运行一段时间后你会发现,问题定位速度、对账成本、需求变更的适应能力,都比传统建模方式好出一个量级。
最后分享一个实用习惯:每次接到新需求,先不管界面长什么样,也不管API怎么设计,先问业务方三个问题——这件事发生了吗?它改变了什么资源?谁为它负责?能清晰回答三个问题,再动手写代码。这个习惯帮我少走了太多弯路。