先说个有意思的事我们内部这个项目的代号就叫 rea全称拆开是 Resources-Events-Agents也就是资源-事件-代理模型。最早听到这个名字的人都会愣一下以为是什么缩写拼错了但实际用下来你会发现这套建模思路对业务系统的价值远比名字本身看起来要厚重得多。我这几年做过好几套订单、库存、财务相关的业务系统踩了不少坑也重构过不少烂摊子。这次做一个企业核心资源管理系统的重构干脆把整个核心数据模型推倒重来全部基于 REA 模型来设计。这篇文章就把这段实战经历完整记录下来包括思路、建模方法、落地细节、踩坑记录纯经验分享没有废话想直接用 REA 思路改造业务系统的朋友可以直接参考。1. 为什么用 REA 模型重新设计核心数据架构1.1 传统订单-凭证-余额记账式设计的隐形债务很多业务系统的核心库表长这样订单表、发货单表、收款单表、应付账款表每个表之间靠手写状态字段拼接业务流系统越跑越重报表越写越玄学。我们这次接手的老系统就是这样光一个订单状态就有十几处独立状态字段状态之间的流转逻辑散落在不同服务里要搞清楚某笔订单到底哪个环节出了问题得同时查五六张表。根本问题出在建模的起点大家都在用会计凭证思维建模脑子里想着的是借方、贷方、余额而不是业务世界真实发生了什么。你把业务系统设计成了账本那所有逻辑最终都会围绕账本转而不是围绕业务本质转。REA 模型恰好反着来它要求你先回答三个问题这个业务里有哪些资源被创造或消耗了发生了什么事件导致资源发生变化是谁参与了这些事件把这三个问题理清数据模型自然就出来了而且它能覆盖的业务形态远比订单-凭证这套要广。1.2 REA 三个核心概念和一个关键原则REA 模型由会计信息化领域最早提出核心概念就三个资源Resource所有有价值并能被企业控制的东西包括现金、库存商品、固定资产、服务能力甚至数据使用权。事件Event导致资源增减变化的具体业务动作比如销售出库、采购入库、支付货款、收取服务费。代理Agent参与事件的人或组织包括客户、供应商、内部员工、部门。这三个概念不是孤立存在的它们之间的关系遵循一个关键原则每一个经济事件必然导致至少一项资源流入和一项资源流出。举个例子一次销售事件库存资源流出现金或应收账款资源流入客户和销售人员参与了这个事件。这个原则的价值在于它强制你从这个动作改变了什么的角度思考业务而不是从这个单据应该填什么字段的角度思考。订单、合同、发票都只是事件的载体或产物不是建模中心。这跟我们平时设计的以订单为中心的系统思维上完全是两回事。1.3 这次项目的目标与范围模拟项目 X 的诉求很明确把原来的订单、出入库、账务三套独立子系统整合成一套核心数据底座要求所有数据可追溯能回答资源从哪来到哪去系统内部不存冗余的累计余额所有余额必须是实时可计算的派生值。这个诉求和 REA 模型的特性高度匹配。我们定下的原则是业务数据全部用资源 事件 代理三元组记录旧的余额字段全部废弃只保留事件明细所有账面上的累计值都用视图或查询实时算出来。2. 建模实操把业务拆成清晰的资源、事件与代理2.1 识别资源的判断规则刚开始建模的人最容易卡在这东西算不算资源上。我们自己总结了一套判断规则百试百灵资源必须能被拥有或控制并且能被交换或消耗。举个例子商品是资源因为它能卖出去换钱现金当然是资源因为它能用来买东西仓库是资源吗是因为它能出租或出售也可以被内部占用客户是资源吗不是你没法拥有一个客户客户的属性应该挂在代理上。合同是资源吗合同本身不是但合同赋予的某项权益比如独家分销权是资源。这里有个容易搞混的点应收账款到底算不算资源老会计系统里应收账款是资产科目但在 REA 建模里我更倾向把它理解成销售事件发生后产生的一个派生状态而不是直接建模成资源。真正属于资源的是现金本身以及未来收到现金的权利。如果你把应收账款建成了资源表后续处理退款、冲抵、坏账全都要为它单独写状态机复杂度会直接失控。简化做法是只把最实在的东西建模为资源能派生出来的概念一律不落库。2.2 事件识别的核心判断方法事件是 REA 模型的心脏识别准了整个系统就稳了。判断一个动作算不算经济事件看两条它是否直接导致某项资源数量或状态发生变化。它是否发生在某个具体的时间点而不是一段持续状态。比如下单不是经济事件因为下单本身没有改变任何资源只是表达了购买意图支付货款是经济事件因为它直接让现金减少了审核通过也不是经济事件它只是对事件的一种审批行为真正的事件发生在通过之后实际执行的动作上。这个区分极其重要。我们原来老系统有个审核状态字段所有人都知道单子审核了但审核之后到底发生了什么资源变化代码里完全没有记录。REA 的思路是把审批这类控制行为放在事件的元数据里标记核心数据只关心真正意义上的经济动作。以下是我们这次模拟项目中识别的核心事件清单事件名称涉及资源流入涉及资源流出参与的代理采购收货库存商品无应付义务派生供应商、仓管员销售出库无应收权利派生库存商品客户、销售人员收款现金无客户、财务人员付款无现金供应商、财务人员内部调拨目标仓库库存源仓库库存仓管员、部门你会发现每一条事件天然就是资源的一进一出会计上的借贷关系变成 REA 里资源流的流入和流出逻辑反而更直观了。2.3 代理的建模细节代理建模看着简单实际上坑不少。我们一开始把客户和供应商分别建成两张表结果发现同一个主体可能既是客户又是供应商业务系统里就出现了同一家公司的两个档案数据割裂得一塌糊涂。REA 推荐的思路是代理是一种泛化概念对应一个统一的主体表再通过角色关联区分是客户、供应商还是内部人员。这是我们最终的代理结构agent 表存主体基本信息名称、信用代码、联系方式等。agent_role 表主体与角色多对多关联同一个主体可以同时是客户和供应商。这样设计的好处非常实际关联方查询一次到位不用像以前那样查供应商要看供应商表查客户要看客户表两边对不上还得人工匹配。2.4 事件与资源的连接表设计事件与资源之间是典型的多对多关系。一次销售出库可能涉及多种商品一次采购收货也可能分批入库到不同仓库。所以我们需要连接表来表达哪些资源通过哪个事件发生了多少数量变化。我们管这张表叫event_resource_entry事件资源条目核心字段包括事件ID资源ID变动方向流入或流出变动数量变动单价这张表是整系统的数据核心来源。所有库存汇总、成本核算、财务报表都是这张表的聚合结果而不是从任何一张余额表里读出来的。3. 核心表结构设计与落地方案3.1 六张核心表的 DDL 思路真正建表的时候我们并没有搞特别复杂的物理模型核心就六张表三张主数据表资源、事件、代理加三张关联表事件资源条目、事件代理关联、代理角色关联。下面是核心表的简化和 DDL 示例。资源表CREATE TABLE resource ( id BIGINT PRIMARY KEY, resource_code VARCHAR(64) NOT NULL UNIQUE, resource_name VARCHAR(128) NOT NULL, resource_type VARCHAR(32) NOT NULL, -- 商品、现金、固定资产、服务 unit VARCHAR(20), status VARCHAR(20) DEFAULT ACTIVE );事件表CREATE TABLE business_event ( id BIGINT PRIMARY KEY, event_no VARCHAR(64) NOT NULL UNIQUE, event_type VARCHAR(64) NOT NULL, -- 销售出库、采购收货、收款 occurred_at TIMESTAMP NOT NULL, location_id BIGINT, -- 仓库、门店、部门 remark VARCHAR(512), version INT DEFAULT 0 );事件资源条目表这表是核心中的核心CREATE TABLE event_resource_entry ( id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, resource_id BIGINT NOT NULL, direction VARCHAR(8) NOT NULL, -- IN / OUT quantity DECIMAL(18, 4) NOT NULL, unit_price DECIMAL(18, 4) NOT NULL, amount DECIMAL(18, 4) GENERATED ALWAYS AS (quantity * unit_price) STORED, CONSTRAINT fk_entry_event FOREIGN KEY (event_id) REFERENCES business_event(id), CONSTRAINT fk_entry_resource FOREIGN KEY (resource_id) REFERENCES resource(id) );代理相关表实际建了两张代理主表和事件代理关联表角色区分用一张辅助表不展开细说。注意事件表和事件资源条目表的 ID 用了业务自增主键同时事件编号上建了唯一索引。这个设计是为了支持后续的幂等写入和审计追溯避免同一个业务事件被执行两次。3.2 为什么不存余额字段而是实时计算这是整个重构里争议最大的一条决定。运营团队一开始非常不适应查库存还要跑查询多慢以前一张库存表直接查多快我的坚持理由是任何长期依赖余额字段的系统最终都会被余额击穿。余额最大的问题是它隐藏了过程。如果余额对不上你只能看到结果错了却不知道是哪笔事件导致的。而事件的不可篡改日志天然能回答资源是怎么从 A 变化到 B 的。实际落地上我们没有让前端报表直接实时聚合明细表而是采用了两个折中手段关键高频查询建立了物化视图后台定时刷新比如当前实时库存客户当前欠款。低频分析类查询走明细聚合引擎直接打事件表。这套方案上线后的实际效果是核心事件表单日写入峰值约 300 万条物化视图刷新延迟控制在 30 秒左右业务上完全够用且再也不会有某个余额字段被某处代码偷偷改了的问题。3.3 从老系统到 REA 的数据迁移策略老系统迁移是重头戏也是最容易翻车的环节。我们定的原则是旧系统的历史数据不逆推建模全量保留原库只把迁移节点之后的新数据纳入 REA 体系。具体分三步走期初快照迁移启动时把老系统的所有核心资源余额库存、现金、在途应收应付做一次全面的期初盘点生成一批期初导入事件作为 REA 系统的起点。增量切换老系统停更新新业务全部走 REA 事件流每天比对两边汇总结果连续对账一两周才会彻底下线老库。历史追溯确实需要追查老数据的场景提供跨系统查询入口旧数据只读不允许任何旧系统写入新库。这套不迁移历史只迁移基准的策略极大降低了迁移风险。很多项目都死在了想把历史数据完美搬过去这一步上实际上历史数据待在旧库里只读比强行塞进新模型里安全得多。4. 落地踩坑常见问题与排查技巧实录4.1 多对多关系设计容易踩的隐性坑用连接表表达事件和资源的关联刚开始没问题但业务一复杂就会遇到一个麻烦连接表里到底是一行一个资源还是一行一条事件资源日志比如一次销售出库涉及三个商品正常建模会拆成三行每个商品一行条目。但如果同一个商品在一张单子里既有正常销售又有赠品怎么办这时候再拆两行逻辑没问题但对账的时候容易眼花。我们的经验是event_resource_entry 永远不追求业务上一行对应一物一价一状态只保留最原子的事实一行一变。后续需要按需求聚合成单头视图在应用层做不要在建模层强行折叠。4.2 事件溯源和状态不同的问题REA 用事件表达事实但业务场景中总有人需要问这单现在什么状态。我们一开始只设计事件流结果运营天天来问订单到哪一步了只能临时去翻事件时间线效率太差。最后我们加了一张event_chain事件链状态登记表它记录的是当前业务聚合根的最新事件快照本质上是事件流的一个投影缓存。比如一个订单聚合的事件链状态是已支付它其实是下单事件、收款事件、出库事件顺序发生后的综合结果。这里要特别强调状态是投影不是源头。源头的唯一事实是事件流状态表只为了查询和展示性能而存在可以定期重建如果算坏了直接刷事件流重算就行。这个理念保证了系统的事实层永远是干净、可重放的。4.3 同一个事件涉及多代理时怎么处理实际业务里一个经济事件往往涉及多个代理。比如一笔采购订单有采购员、仓管员、财务人员甚至供应商销售员参与。我们一开始只想到事件关联一个客户或供应商后来发现内部审计需要知道这笔单子谁下的、谁验的、谁付的款仅挂一个代理根本不够。补全方案是event_agent 关联表除了事件 ID、代理 ID还有角色字段比如 CREATE_BY、VERIFY_BY、PAY_BY。这样一次采购事件就能把全程参与者全部挂上去审计追问时一条 SQL 就查出来再也不用翻日志。4.4 高频查询的性能优化实战REA 模型最大的性能争议就是明细过大、聚合太慢。我们项目上线后确实出现过库存查询从秒级变成三秒的问题原因是物化视图刷新策略太乐观每天凌晨全量刷新白天因为增量数据不断累积查询被迫回源到明细表。排查后我们发现物化视图增量刷新的条件设计错了。修整方案非常朴素但实用按事件发生时间做增量刷新而不是按事件 ID。物化视图里只保留当前有效资源量的快照不做跨日累计。高频日查询走快照月报表走离线数仓两者数据来源同源但计算路径分离。调整后物化视图刷新从 30 秒降低到 5 秒左右库存查询稳定在 200 毫秒内算是把 REA 模型的性能短板彻底补上了。5. REA 模型和主流架构思路的融合思考5.1 REA 与事件驱动架构天然互补用过 REA 之后你会强烈地感觉到它跟事件驱动架构EDA和事件溯源Event Sourcing在气质上高度一致。REA 告诉你应该记录什么事件EDA 提供事件的传递机制事件溯源则保证事件流可以作为唯一数据源被重放。我们这次把三者的关系理顺了REA 是建模规范明确定义哪些是资源、哪些是事件。事件溯源是存储模式事件是唯一事实源状态为投影。EDA 是运行机制事件通过消息队列分发到下游做异步处理。这三层搭起来系统架构的稳定性比之前靠数据库状态字段硬撑要强太多了。下游业务只需要订阅事件自己投影自己关心的数据核心库的压力和环境耦合都大幅下降。5.2 什么场景适合用 REA什么场景不适合REA 不是万能钥匙也有的场景用了反而难受。我根据自己的经验整理了适合和不适合的情况适合用 REA 的场景不适合用 REA 的场景进销存、供应链、财务核算纯内容展示系统多实体协作的复杂业务流高频 CRUD 内部办公系统需要严格审计追溯的核心财务非常简单的一次性小工具状态复杂、口径容易冲突的聚合系统没有资源增减概念的管理面板简单说只要系统里的事儿可以抽象成资源变多、资源变少、谁干的这种永恒日常REA 就值回票钱。反过来你要是做个相册管理、文章发布引入 REA 只会被人骂过度设计。6. 项目复盘我个人认为最值得一说的三个体验第一建模阶段的克制感比设计能力重要。我们在推 REA 的时候团队里好几个人总想把合同、审批、流程都建模成资源结果全被我拦下来了。判断一个东西该不该建模成资源别问系统能不能为它建表要问它会不会被交换、被消耗不会的一律丢到代理或事件属性区。第二迁移数据永远别贪多求全保住基准线和源头一致性就够了。我们这次从决定迁移到正式切换用了三周前两周都在做对账和数据校准真到切换那一步反而很平静。最大的风险往往不是新技术不行而是旧数据把新模型撑爆了。第三REA 模型给了技术人一套跨业务领域的通用交流语言。后来我们内部讨论需求的时候不再问这个按钮存哪个表而是直接问这个事件涉及哪些资源、哪些代理参与讨论质量直接提升一个档次果然概念模型的力量大于数据库表结构。如果这篇文章让你对 REA 模型产生了兴趣我建议你先别急着建表找一个最小的业务场景比如你手头的采购流程、出库流程拿纸笔把资源、事件、代理画一遍再把事件流的流入流出箭头标出来。画完两三个场景你自然能感受到这套建模思路和传统字段堆砌的天壤之别。后续我打算把这次项目里不同事件的类型粒度划分方法单独写一篇里面牵扯到的聚合根边界和事件依赖排序问题有很多细节点值得掰开说。先写到这儿各位实际业务哪一步卡住了欢迎来评论区讨论。