资讯动态

从REA建模到事件驱动架构:业务系统设计的资源、事件与代理人实践

发布时间:2026/10/20 13:14:33 来源:尧图企业网站定制
手头拿到一个立项名只写了“rea”三个字母没有需求文档、没有原型图、没有验收标准。按理说这种项目可以直接打回让业务方补材料但真正做过业务系统重构的人都明白很多问题不是需求没写全而是压根没想清楚。我判断“rea”大概率对应业界那套 REA 建模思路——核心是Resources资源、Events事件、Agents代理人三个词的首字母。这篇文章把我在实际项目里怎么从这个模糊标题出发一路拆到数据库表和接口设计的过程完整讲一遍包括踩过的坑和最后沉淀下来的检查清单适合刚接触业务建模的后端开发、架构师以及长期被“需求变来变去”折磨的产品经理参考。1. 项目首析标题只有 rea 三个字母怎么把需求做成可执行的方案1.1 三个字母的公开解读与私人心得先聊一个很多人会问的问题rea 到底是不是这三个词其实业界对 REA 的解读非常统一它最早出现在会计信息系统领域最初目的解决的是账务数据和业务事实脱节的问题。后来不少 ERP、供应链系统的设计者开始用它做企业级流程建模因为它提供了一个极度稳定的业务骨架任何一笔业务都能拆成“谁在什么时间做了什么动作、影响了哪些资源”这个骨架天然适合计算机落地。但如果只把 REA 当成三个名词的缩写它就变成了一句正确的废话。我个人的理解是REA 真正值钱的地方在于“强制你从事件出发而不是从状态出发”。普通设计习惯是以单据为核心POS 单、入库单、库存表、应收表一堆东西摆在那里系统要处理的是“把单据 A 转换成单据 B”的复杂流转。而 REA 要求你先放下单据问一句业务里到底发生过什么这个动作叫什么谁参与动到了哪笔资源这句话初看轻飘飘实际上是两种完全不同的开发世界观。以单据为核心意味着将来每一次需求变更都可能加字段、加状态、加接口以事件为核心意味着业务变动大概率是“新增一种事件类型”核心表和核心逻辑可以长期稳定。回到 rea 这个项目我当天就做了一个关键决策不急着出原型先带着 REA 视角重新梳理业务流程把所有参与方、业务动作、资源对象列成一张大表再回来谈技术方案。事实证明这一步省了后面至少两周的返工。1.2 为什么事件视角比数据视角更适合业务系统用生活化的类比来解释传统的表结构像“记流水账”每个业务对象保存自己的最新状态比如库存表里直接写“当前数量500”。而事件视角像“装监控摄像头”系统里只记录精确到明细的事实比如“张三在 9:45 卖了一台设备给李四单价 1000”。库存表里那 500 是怎么来的靠回放这些事件算出来或者定期汇总事件结果生成快照。这两者有什么区别假设某天财务说库存数字对了要查是哪笔记录把库存搞错了。传统结构里你只能看到当前数量至于哪些单据发生过、先后顺序如何很难还原。事件结构里直接把所有库存节点上的交易事件拉出来排序哪个环节算错一目了然。这个特性在涉及多人、多部门、多系统交互的场景下特别值钱因为它给了系统一种“可追溯真实”的能力。另一个好处是系统之间的集成变简单了。很多老系统联调困难是因为 A 系统把结果直接写死给 B 系统B 系统只能被动接受结果一旦数据中间有偏差两边根本说不清。REA 则鼓励把事件本身作为上游产出物大家共享同一份事实流下游系统各自用自己的规则去解读。总线、消息队列这类基础设施本质上干的就是这件事。所以 rea 项目里我坚定地选择“事件先落地、状态按需算”的架构取向这也是后面所有设计的原动力。2. 核心原理拆解资源、事件、代理人三角怎么用在架构设计上2.1 资源一切要管的“东西”资源在 REA 里是个很宽泛的概念可以是实物资产库存商品、生产设备也可以是非实物的现金、应收账款、某段时长的服务能力核心特征是“可以被某个业务动作消耗或产出”。比如“支付现金”这个动作消耗的是现金资源“收到订单”这个动作承诺了未来交付本质上是产生了一份“应收账款”资源。很多团队设计资源表时习惯把“库存现有量”作为字段存在资源配置上这种做法在 REA 里属于结果而非资源本身。资源对象本身只表达“有什么东西存在”它的数量与状态应该由事件推导出来。例如你要在系统里表示“一批苹果”不要建一张表专门记苹果库存数量而是建立“苹果存货”这个资源库存数量通过把所有“入库事件”数量求和再减去“出库事件”数量得出。这个思路一开始会觉得绕但真遇到并发、审计、库存月结时就显出威力了因为汇总计算的过程本身就能被复核。在我做的模拟项目 X 里资源还被细分为“可分割资源”和“不可分割资源”。苹果是可分割的1 吨苹果可以被拆成 100 份出库一台设备是不可分割的要么整体交付要么不交付。这一区分直接决定事件表里的数量字段是整数还是小数、有没有批次号甚至影响后续盘点的粒度。2.2 事件让状态产生变化的唯一起点事件是 REA 模型里的中心组件也是我对 rea 项目最看重的部分。什么样的事情够格称为一个事件我给的判断标准很简单必须发生在某个时间点、由某个代理人执行、且直接导致至少一份资源的增减或承诺变化。不满足这三个条件的信息优先考虑挂在资源或代理人上面不要硬塞进事件表。举个例子“用户提交了售后申请”是一个事件“用户抱怨了一句物流慢”就不是。因为前者至少改变了售后工单资源的状态后者只是一个文本信息不影响任何资源。落库的时候我们建了一张 event 总表字段包括事件类型、发生时间、提交人、涉及资源 ID、数量、方向增加或减少、关联的上一事件 ID。这张表承担了非常重的职责但好处是所有业务流程的“事实”都沉淀在一个地方排查问题的时候直接对着事件流看很少有信息黑洞。事件与事件之间往往有先后依赖。比如“采购下单”和“收货入库”是两件事中间隔着一个时间差按 REA 设计应该分成两个事件。很多新手会顺应直觉把“下单后直接入库”写成一个事件节省环节。我强烈不建议这么做因为一旦出现部分到货、分批验收、货损少件这类混乱场景拆开除单据之外没有第二个能准确对账的出路。2.3 代理人谁参与、谁负责、谁获益代理人可以是个人也可以是组织包括客户、供应商、内部员工、仓库管理员、承运方。REA 特别强调一个业务事件至少涉及两个集成的代理人一方发起另一方接收。这不是官僚主义而是为了保证每条事实都有明确的责任边界。比如“收货”这个事件发起方是供应商驾驶员接收方是我方仓库保管员。如果事后发现货物数量不对可以直接定位当时双方各自确认了什么如果只有一条“已收货”的日志没有明确谁参与最后追责时又得从操作日志里面猜。代理人字段和权限体系高度相关它不仅是用来展示“操作人是谁”更是业务规则的核心输入某个角色能否发起某类事件是否能绕过某条校验链。在一个跨组织协作的场景中代理人的重要性会进一步放大。订单系统、仓储系统、财务系统各有一个“用户”概念如果设计时只把用户 ID 当作普通外键插入事件表跨系统识别同一个人会非常痛苦。更合理的做法是引入“代理人员主数据”的概念无论哪个系统调用先映射到统一的内部员工或往来单位标识再关联权限及组织层级信息。这个设计为后续做多租户、多分公司扩展省下大量集成成本。3. 实战落地把采购入库到销售结账的全链路揉进 REA 模型3.1 先圈流程边界先定义关键事件再反推资源与角色拿到 rea 项目后我没有直接开写代码而是用一个下午把所有主要业务场景的动画片拉出来圈定三个“边界问题”系统到底管哪一段哪些事情必须留痕哪些事情只作为输入参数以模拟项目 X 的零售购销流程为例我先列出核心事件链供应商报价供应商 A 提出可按某价格供货。采购下单采购员向供应商发出采购订单。收货入库仓管收到实物录入实收数量。上架陈列商品进入可售状态。顾客下单顾客承诺购买某些商品。收款确认顾客支付货款。出库交付商品交给顾客。这个阶段不需要考虑数据库表重点是让所有业务方对“哪些是事件”达成一致。当时业务方一直想把“库存”也列为一个事件被我拉回来了。“库存发生变化”是结果不是事件它一定由更具体的事件驱动如果你把结果也伪装成事件后面所有追溯链条都会断掉。边界圈定以后再反推资源。资源清单通常比事件清单少很多因为资源相对稳定。这个项目里资源主要有商品存货、库存现金、应收账款、应付账款、顾客承诺、供应商承诺。你能看到资源不一定都是实打实的东西“承诺类资源”特别容易被忽略但它在信用销售、订货合同场景里非常重要。没有承诺资源就没有办法表达“顾客已经订了货但还没付款提货”这个中间状态。3.2 字段表设计的替代方案用事件记录而非可变状态传统表设计碰到“确认收货”这类流程入门做法是给 orders 表加一个 status 字段0 表示待发货1 表示已发货2 表示已签收。REA 的做法完全不同订单创建、发货、签收各是一条事件事件类型不同、发生时间不同、操作人不同订单当前处于什么状态通过查询事件流最新记录得到。下面我用一段简化的 schema 示意一下事件表的形态CREATE TABLE business_event ( id BIGINT PRIMARY KEY, event_type VARCHAR(50) NOT NULL, -- 例如 purchase_order, stock_in, customer_payment event_time TIMESTAMP NOT NULL, agent_from BIGINT NOT NULL, -- 发起方代理人 agent_to BIGINT NOT NULL, -- 接收方代理人 resource_id BIGINT NOT NULL, -- 涉及资源 quantity DECIMAL(18,4) NOT NULL, -- 数量 unit VARCHAR(20), direction SMALLINT NOT NULL, -- 1 为资源增加-1 为资源减少 linked_event_id BIGINT, -- 关联上一环节事件例如“收款”关联“顾客下单” biz_no VARCHAR(64), -- 业务单号供前端查询与打印使用 source_system VARCHAR(32), -- 来源系统标记 UNIQUE KEY uk_events (event_type, biz_no, resource_id) );这张表设计的时候有几个细节值得说明。第一linked_event_id 不是强外键业务上不一定能保证每个事件都有严格的一对一上游所以只做记录不做级联约束。第二biz_no 的存在是为了兼容老系统的“单号”概念很多报表和分析依然依赖业务单号保留它是为了平滑迁移。第三direction 字段允许我们通过统一公式“当前库存SUM(增加事件数量)-SUM(减少事件数量)”算总额避免为不同类型的业务各建一套汇总逻辑。除此之外还需要一个 resource_snapshot 表做性能缓存。它存的是每个资源在某时刻的数量快照比如每日库存快照。快照的意义在于减少全量事件求和带来的性能压力但快照一定是异步生成且可重新计算的绝不能作为唯一数据源。简单说“事件表是真相快照表是缓存”。3.3 一个可运行的查询示例和接口边界说明纯 schema 不够直观我实际跑过的查询大概是这样。要算“某个商品当前的可用库存”只需要一条 SQL 聚集事件SELECT resource_id, SUM(CASE WHEN direction 1 THEN quantity ELSE -quantity END) AS current_stock FROM business_event WHERE resource_id 10086 GROUP BY resource_id;如果要算“这个月供应商 A 累计供货金额”就要先找到采购到货事件中 agent_from 为供应商 A 的所有记录再关联到当时的约定单价。单价我建议单独放在一个价格配置表由对应事件记录 price_version 做关联不要直接冗余到事件表里。如果不做版本关联供应商改价后回看历史流水会出现金额“被追溯性”的错误。这个问题我在项目里踩过一整个来回后来统一加上 price_version 字段才算安心。接口边界上我倾向把所有事件统一收敛到一个写接口类似 eventing service 入口所有外部系统不允许直接改库存表。控制器只负责接收“谁来、对哪个资源、做什么类型的动作”服务层负责补齐事件上下文如当前时间、来源系统、做业务校验、落库事件最后触发下游读模型更新。这套链路看起来多了一层转发但把事件写入权限收口是后续做好审计、重放、幂等处理的基础。如果让每个业务模块直接 insert 自己的事件表不同模块对同一资源的状态判断很容易出现分叉。4. 常见错误与排查实录一次真实系统跑偏后的对照复盘4.1 事件与状态混用导致账目对不上项目做到中期测试反馈库存账和实物盘亏了很多。打开数据一看一大半原因是代码里非常自然地写了类似“库存表 num num - 1”的语句只更新了库存表没有写入任何事件。库存数字变了但查不到哪笔操作引起的。这就是典型的“事件与状态混用”——没有建立“状态必须来自事件回放”的纪律导致事实链断裂。纠正方法并不复杂先让所有增量写在事件表再允许库存表基于事件异步刷新同一次请求里如果既有增删事件又有库存更新必须放在一个事务里保证一致性。最初会觉得性能上有额外开销但实际上库存表的刷新可以通过批量汇总完成并不会导致系统变慢。更关键的是后来再遇到对账差异我可以基于事件流逐项排错不再靠猜。4.2 重复事件没有幂等设计导致金额翻倍有一次联调测试发现应收账款的总额比实际业务多了一倍。检查后发现是支付回调被消息中间件重复投递支付处理器没有判断同一笔支付事件是否已经处理过重复写入了一条相同的收款事件。这个坑在普通表模型里同样存在但在事件模型里特别致命因为事件写完后不可篡改重复一条就直接污染整个汇总结果。解决方案是在事件表上加业务唯一键这个唯一键不应该是数据库自增 ID而应是业务流水号。比如支付事件用“支付渠道 支付渠道单号”做唯一索引重复插入时数据库直接抛约束错误代码层再捕获这个异常用查询已有事件返回原结果替代再次处理。这就是幂等设计也是事件驱动系统的基本功。哪怕你的项目不是高并发场景我也建议从第一天就做好这个约束因为事后补数据的成本远高于事前加索引的成本。4.3 过度抽象把模型做成了中看不中用的“万能库”还有一种失败方式是把所有东西都抽象成事件后业务规则不知道该在哪一层执行。比如“新客户下单必须经过信用审核”、“库存不足不能发货”这类业务规则如果建模时只盯着“一切皆事件”就可能出现事件无脑落库校验规则全丢到前端后端完全裸奔。REA 模型对应的事件不是无序集合事件之间是有流程约束的。实现上需要为每种事件类型定义一个“前置条件校验器”在这个事件被写入之前检查资源和代理人的状态是否满足要求。这个做法理解起来很简单事件表负责记录事实校验器负责保证事实符合业务预期。它既保留了事件记录的灵活性也避免了系统变成一本没有任何规则约束的流水账。每新增一种事件类型第一件事是写出它允许发生的前置条件和不允许发生的情形这条经验真的能避免项目后期大量的“脏事件”。4.4 实例排查实录一张表支持多业务时代理人和资源的维度要统一那次排查让我印象很深。上线一版系统后运营发现同一个仓库却出了两份“实时库存”仓储系统和销售系统显示数据不一样。两个系统各自实现了一套库存计算逻辑仓储系统以“入库单”为准销售系统以“销售出库单”为准两个单据在时间口径和资源粒度上根本没对齐。仓储按“批次”记录销售按“SKU”记录中间没做映射汇总结果自然不一致。最后我们坐下来把所有业务事件统一到同一张事件消费视图按 SKU 维度重新梳理资源编码规则并为批号、仓位增加维度字段。同时约定所有系统读取同一份库存快照事件流不允许各自从自己的业务表算“库存”。改完之后两边数据终于对齐也让大家意识到 REA 模型的真正价值不是省两张表而是把全链路的事实口径统一起来。那件事以后我在所有项目里都会强调“资源字典”是第一优先级先统一资源和代理人的编码再谈事件流转。5. 给团队落地 REA 的实用工具与验收清单5.1 建模会议应该问的五个关键问题现在团队内部推 REA基本不会一开始就去讨论表结构。我每次引导建模会议时会用一组固定问题既能拉齐认知也能快速暴露出业务矛盾问题目的这件事发生的最原始动作是什么确认事件而不是把“状态改变”误当事件这个动作发生前必须满足什么条件找出前置校验逻辑沉淀业务规则有哪些人或组织参与了这个动作识别代理人划定责任边界这个动作增减了哪项资源数量怎么算建立资源清单和数量口径这个动作在现实世界能追溯的单据或凭据是什么映射老系统单号保证可迁移追溯会议时间一般控制在两小时以内。超过这个时间还没把事件链理清楚通常意味着业务方自己也没想清楚硬建模只会给后续挖坑。5.2 建模到开发落地的验收清单开发阶段我一般会带一份验收清单写完代码逐项打勾每个业务动作都有唯一事件类型编码且事件表和事件类型表正确关联。所有事件都有明确的发起方和接收方不允许出现空代理人的事件。所有资源增减都通过事件流转代码里搜不到“直接 update 资源表数量字段”的语句。关键业务流水号有数据库唯一约束确保消息重复时不会产生重复事件。事件表设计时间字段与业务时间字段分离避免把服务器时间当成业务时间导致排错混乱。快照表可以全量重建且与事件表对账一致重建过程不需要停服务。事件检索接口支持按时间、资源、代理人、事件类型过滤这是后续排查问题的基础设施。这些检查项看起来简单但漏掉任何一条以后出问题都会让你多熬很多个夜晚。我自己排产时把这些项写进迭代的完成定义里而不是留给测试阶段才发现。5.3 实操心得与扩展思路从一个只有“rea”三个字母的项目开始走到现在这一整套稳定的事件驱动业务骨架我最核心的体会是模糊标题反而是好事因为它逼着你先把业务本质想清楚。很多人觉得 REA 是会计概念离开发很远其实它更像一种“业务事实优先”的设计习惯。你不需要把整个系统推翻重来才能用上它选一条最容易出乱的链路比如库存、对账、订单状态先按事件流重写一遍体感马上不一样。这套思路后续还可以继续扩展比如把事件流接入链路追踪体系给每个事件生成全链路 ID或者基于事件数据做业务监控看板统计某类事件发生频次、耗时分布预警异常增长。我后面又在另一个项目里把相同的事件模型扩展到工单系统连“客服接手-响应-解决-回访”这种非实物业务也能顺畅建模可见它的适用范围比想象中要大得多。实际动手的时候我建议你不要追求一步到位的大改造挑一个最让团队头疼的对账场景做一个小实验对比改造前后的排查时间与数据准确性。尝到甜头之后推进阻力就会小很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑