REA模型实战:用Resource-Event-Agent重塑业务领域建模
项目标题抛过来的时候只有三个字母“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、Agent2.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语义精简成四张核心表。先建资源表resourceCREATE 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怎么设计先问业务方三个问题——这件事发生了吗它改变了什么资源谁为它负责能清晰回答三个问题再动手写代码。这个习惯帮我少走了太多弯路。