标题 "rea" 拆分重构之后,我把它定向理解为“资源-事件-代理”这一套经典的业务建模思想。这套模型在国外企业建模、财务系统设计、DDD领域建模里用得很多,但在国内技术社区的讨论热度一直不算高。实际操作中,用REA思维去重新梳理业务系统,尤其是订单、库存、结算这类涉及多方、跨流程的系统,会明显感觉到逻辑变得干净、可追踪、好扩展。这篇文章就按REA的核心理念、建模实操、常见误区和踩坑经验来展开,附带一套可以直接用来设计业务模块的建模方法和检查清单。
1. 为什么我突然开始重看REA模型
过去几年,我一直在做各类企业级业务系统的领域建模,包括订单履约、库存管理、财务对账、会员积分这类典型模块。用得最多的是事件风暴和DDD聚合建模,后来在一个结算系统重构项目中反复踩坑,才真正把REA翻出来认真研究了一遍。
先回答一个基础问题:REA是什么。它由三个核心概念组成——Resource(资源)、Event(事件)、Agent(代理),分别对应业务世界里的“东西”“发生的事情”和“参与的人或部门”。这套框架最早用于会计信息系统设计,是会计学教授William McCarthy在上世纪80年代提出的,核心目的是用一套贴近业务本质的模型,来描述企业的资源流转和价值变化,取代传统复式记账那种以“借贷科目”为骨架的建模思维。
放在技术语境下理解,REA的价值不是让你不用数据库表、不用领域对象,而是给你一套筛选领域逻辑的“筛子”:哪些实体值得建表,哪些状态变化值得记录,谁参与了变化,变化造成了什么资源的增减。这套筛选逻辑,恰好就是事件驱动架构和DDD战术建模中特别容易潦草处理的环节。
那这次选题“rea”,我就打算沿着从概念到实操的路径,把REA在业务系统建模中的应用彻底讲透。适合谁来读:正在做订单、财务、库存类系统设计的业务架构师、后端开发,也包括刚接触DDD但总是纠结聚合边界怎么划的读者。读完你会得到一套可操作的建模清单,不需要有会计背景,我也会把会计和业务之间那层术语差异拆开看。
2. 拆开看REA的每一个核心原子
2.1 资源:不只是数据表里的字段
Resource在REA里指的是“经济资源”,是系统里有价值、可被交换或被消耗的事物。注意这里有别于技术领域常说的“实体”——资源不只是一条记录,它代表的是某一时刻有价值意义的对象:库里的一件商品、账户里的一笔余额、卡包里的一张优惠券,都属于资源维度。
在建模的时候,容易犯的第一个错误是“见什么表都当资源”。比如订单表、物流单表这类记录型数据,本质上不是资源,而是“事件”的载体。它们记录的是已经发生的交易动作,不是被交易的东西本身。如果你把订单表也建模成一个资源,那后续状态流转、对账追溯,都会被各种流程字段拖垮。
实操上识别资源的判断标准很简单:这个对象会不会被“消耗”“减少”或“增加”?会不会出现在系统内外部的价值交换链路上?如果答案是肯定的,它才具备资源资格。反过来,一个纯粹记录状态的标签,比如“订单状态”“处理节点”,就不算资源,只是事件的结果快照。
2.2 事件:整个模型的发动机
Event是REA模型中最核心的部分——它描述“发生了什么”。事件必须有明确的时点、明确的动作类型,以及明确的参与者。下单、支付、扣库存、发货、签收、退款,这些都是事件。
要用一句话记住这个定位:资源是被事件驱动的,事件是被代理执行的,而整个业务流就是一连串有序事件的串联。这个视角和事件溯源(Event Sourcing)理念十分契合,也和传统ERP系统里只保存当前状态的做法不同——REA会把“过程”作为一等公民保留下来。
建模事件时,要区分增值事件(Increment Event)和减值事件(Decrement Event)。增值事件指资源的流入方向,例如入库、收款、优惠券发放;减值事件对应资源流出,例如出库、退款、积分消耗。几乎所有业务动作都能归入这两类,中间的判断是所有链路是否闭环的检查开关。
2.3 代理:认识“谁”做了这件事
Agent是参与事件的实体,可以是个人、部门或外部系统。代理与事件之间关系有两种——执行关系和参与关系。在订单履约中,“用户”是发起支付事件的执行代理;“仓库系统”是执行发货事件的执行代理。
把代理剥离成独立模块,最大的好处是权限模型、审计日志、异常复核都可以复用同一套身份体系。很多系统在设计时会把操作人直接塞进业务表里当字段,短期看是省事,但一旦涉及多个系统、多类角色,就会出现权限绑死在流程上、无法扩展的问题。REA把代理抽象出来,等于让模型天然具备“谁在何时做了什么”的审计能力。
2.4 三者的连接关系,才是它最值钱的部分
单独看三个概念,并不算惊艳。REA真正的价值在于定义了三者之间的经济联系,例如资源-事件、事件-代理、以及事件-事件之间的双向或链式控制关系。
比如一笔“销售出库”事件,它必然连接一个“库存资源”的减少和一个“销售订单”事件的触发。这个连接不是外键只是逻辑关联,而是在建模层面强制你回答:这个事件到底动了哪些资源?由谁执行的?它是哪个事件触发的后置动作?一旦要考虑这些问题,很多通常被忽略的边角逻辑就暴露出来了。
3. 从零开始:用REA重造一个小型订单系统
3.1 建模前的边界圈定与角色定义
这次实操,我把场景限定在一个极简的电商本地生活订单履约模块,参与角色只包括用户、商家、平台结算三个代理。业务流包含:用户下单、商家接单、用户支付、平台抽成、商家结算、用户取消订单、退款。
第一步不碰表和类,先做一张事件清单,把该业务环节里所有“发生了什么”的候选事件全列出来:
- 用户创建订单
- 用户完成支付
- 商家确认接单
- 商家完成履约
- 平台完成分账
- 用户发起退款
- 系统执行退款
- 用户取消未支付订单
这八个事件就是系统的“事件原语”。后续所有状态、表结构、接口设计,都服务于“记录好这些事件”这个目标,而不是反过来让事件去适配表结构。
3.2 第二步:识别每类资源的进出路径
事件清单确定后,第二步是把资源路径画清楚。
用户创建订单 -> 生成一个“待支付订单快照资源”(实际上它是订单资源的一个状态版本);支付事件 -> 增加“平台收款账户余额资源”,同时产生“用户支付流水资源”;商家接单 -> 没有直接资源变更,但触达了履约事件;履约完成 -> 减少“平台应付商家账资源”与“用户已购服务资源”之间需要建立结算关联;退款事件 -> 减少“平台收款账户余额”,增加“用户账户余额”。
这里注意一个细节:不要把“订单状态”直接建模为资源变动。订单状态是事件产生的派生结果,你完全可以随时通过回放事件计算出当前状态。为追求写读性能而单独维护状态字段没问题,但业务语义上必须清楚它不是源头。
3.3 第三步:定义代理关系和权限范围
三类代理的建模相对固定:用户、商家、平台。各自的事件执行边界需要严格区分。用户能触发创建订单、支付、取消订单这类上游事件;商家能触发接单、履约这种执行类事件;平台只触发分账、结算类动作。
在设计API接口时,我们同样沿用这条边界:每个代理对应一组可执行操作集合,操作与事件一一对应。这样权限配置会变得异常简洁,审计也舒服。
3.4 关键落库设计:事件表长什么样
落到数据库实现时,我最终采用了一套“事件主表 + 资源流水表 + 代理关联表”的三角结构。
事件主表结构如下:
- event_id:全局唯一事件编号
- event_type:事件类型(枚举)
- occurrence_time:业务发生时间
- agent_id:执行代理ID
- resource_id:关联资源ID(可能为空,比如用户取消未支付订单时没有资源变动)
- parent_event_id:触发当前事件的父事件ID(可为空)
- payload_json:业务上下文快照
这套结构把事件、资源、代理全部串在一个逻辑链路里。每产生一个新事件,生成一条事件记录,同时根据事件类型追加资源流水。后续要做对账、统计、风控,都只需要围绕这套事件流来做,不需要在十几个业务表里来回join。
3.5 从模型反推接口协议
接口方面,我强烈建议所有变更类操作都设计成“提交事件”的语义,而不是“更新状态”的语义。
例如:POST /api/v1/events/payment,而不是POST /api/v1/order/status/paid。前者表达的是“发生了一个支付事件”,后者表达的是“修改数据状态”。两者前端调用可能完全一样,但接口语义会直接影响后端设计者的建模自觉。项目重构之后,我们团队内部定了一条规矩:任何修改操作先想清楚它在业务上到底是哪个事件,再去写接口逻辑。这条规矩本质上就是REA思维在工程层面的落地。
4. REA与常见建模方法的核心差异
4.1 传统数据库表驱动的建模方式
传统方式下,设计订单模块基本是建订单表、订单明细表、支付流水表、库存表,用外键把它们关联起来。开发效率高,但随着业务分支增多(售后、改价、多渠道、多次支付合并),问题开始显露:表结构需要不断加字段,状态机和业务规则纠缠在一起,排查一个人为操作引发的数据不一致问题时,只能在十几张表手工核对时间线。
REA对此的解决思路是“保留原始事件数据,不强求单表状态一致性”。传统表驱动方式把业务状态视为核心数据,REA则把业务状态视为事件演算的产物。只要事件流完整,任何时点的快照都可以被重建。
4.2 DDD与REA结合,是实践中最顺滑的组合
很多人问DDD的聚合和REA建模到底是什么关系。我的理解是,DDD提供的是“如何划分边界并管理复杂性的方法论”,REA提供的是“边界内部如何识别核心要素的视角”。两者搭配时,聚合根往往就是某个主事件或一组事件链,实体对应资源和代理的形态。
比如订单聚合并不适合把“订单状态”当聚合根,可以改成“订单创建事件”为聚合起点,把支付履约退款等事件组织在同一段事件链中,订单信息变成事件链上的投影。这能极大减少聚合内状态不一致的问题。
4.3 事件溯源、CQRS和REA的天然契合
REA要求完整保存事件流,和事件溯源架构天然同频。事件溯源把业务状态存储为事件序列,REA则告诉我们应该存储哪类事件。两者结合,系统在复盘用户纠纷时,可以把当时发生的事件帧逐帧还原。CQRS模型下,读模型完全可以用事件流构建出针对不同角色定制的数据视图。
但这里必须提醒一点:不是所有业务都适合完整事件溯源。比如库存操作频繁、吞吐量大的场景,每笔事件都落到事件库,存储成本和序列化成本会直线上升。稳妥做法是把REA事件模型用于交易结算类业务(低频但高价值),而对于库存流水这种高频数据,可以只保留汇总结果和最近一段时间的明细事件。
5. 实操中最容易踩的五个坑(附排查方法)
5.1 把快照当事件,导致事件流断裂
我见过太多次系统里记录了“当前欠款金额”,但没有记录“这笔欠款最初是因为什么事件产生的”。一旦审计追问来源,就只能靠人工翻日志。排查方法很简单:对任意一条资源流水,问一句“这条流水是由哪个事件写进来的?”如果回答不了,事件链就是断裂的。
5.2 缺少父事件关联,无法追根溯源
如果一个子事件没有父事件ID,就只能靠时间戳去猜测链路。尤其在退款、改签这类多级流程场景,没有父事件关联会让问题定位难上数倍。设计事件表时,必须预留parent_event_id字段,并在写入逻辑中强制要求非根事件填写。
5.3 滥用“资源”概念
最常见的错误是把所有业务对象都塞进Resource,导致资源、事件、代理边界混乱。判断标准回到前面的定义:如果一个对象不会被增加、减少或消耗,并代表某种可以被交易或流通的价值,那它就不是该业务域的资源。
5.4 忽略时间字段的口径一致性
REA模型中时间属性是核心字段,但不同系统对“业务时间”“系统时间”“支付渠道时间”的定义差异极大。做对账时如果混用,会直接导致核对失败。建议强制一条规则:事件表始终记录三个时间维度——业务发生时间、系统入库时间、外部来源时间,分别建字段,别偷懒复用。
5.5 代理身份复用导致审计粗颗粒
如果把“平台”当成一个代理来记录,分不清到底是平台客服操作,还是平台自动任务触发,后期定责就无从谈起。代理建模的粒度应至少到“操作入口”,比如平台客服A和平台自动任务,属于同机构但不同代理子身份,在建模时必须区分开。
6. 排查问题时的实操清单
这套模型搭好之后,排查问题本身也能形成一个固定的操作流。
出现资金对不上、库存不平、积分多发这类异常时,我的排查顺序是这样:
- 先锁定时间窗口,查找窗口内全部事件记录。
- 按资源维度筛选涉及该资源的所有事件。
- 按代理维度检查是否存在可疑角色操作。
- 按父事件链路,从根事件逐级往下展开。
- 最后比对资源流水总量是否等于各事件明细的代数和。
这套方法论从根源上把定位成本降低了。之前用传统表结构排查对账问题要跨模块拉一堆Excel,手工比对,现在直接对着事件表写几条SQL,几分钟就能定位。
基于我个人的实操体会,REA建模在交易、财务、供应链这类需要强审计和高追溯性的业务领域,价值尤其明显。如果你的系统还在天天处理各类数据不一致纠纷,不妨抽半天时间,把自己模块里的事件完整列一遍,把资源流动画清楚。这个动作本身,就已经能帮你找到不少隐患。
最后再分享一个小技巧:项目初期建模时,不要先想着ER图,先在白板上只写“事件动词”和“资源名词”,用“谁执行什么动作使什么资源发生了变化”这个句式覆盖所有业务规则。等所有规则都能用这个句式说清楚,再去设计表结构,一切就变得非常自然。这条路我走通了,也希望给正在做业务建模的你提供一个新的思考支点。