ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

REA业务建模:资源-事件-参与者模型实战解析

REA业务建模:资源-事件-参与者模型实战解析 看到rea这个标题我第一反应不是“读”也不是某个地区缩写而是很多年前那个库存项目——REAResource-Event-Agent。先跟你交个底这篇文章我按“资源-事件-参与者”建模来写。如果你手里正捏着订单、库存、应收应付这类系统或者被借贷对账、数据改来改去搞到头大这篇文章能给你另一套建模思路把业务当成一条事件链把余额当成事件链的投影。前半部分讲清楚REA到底在解决什么问题后半部分直接给出表结构、SQL、踩坑经验你可以照着抄。1. 为什么无头无尾的“rea”会被我解释成业务建模1.1 一次库存对账事故让我重新审视“记账”第一次接触REA是因为一次库存模块重构。上下游系统各自维护了一份“库存量”每天都跑对账任务。看起来原理很简单库存等于入库减出库但实际对账时经常差几千件。查到最后发现仓库有个流程是“先发货后补单”补单时单据时间填的是前几天财务系统按入账时间记账订单系统按业务发生时间记账。两个时间不一致同一笔业务落到不同天里余额自然对不上。传统做法是对账日纠错哪边不平手动调一分录。这种办法治标不治本因为系统里根本没有一个“这盒物料在几点几分被哪个仓库发出去了”的独立事实。当时我们最终得出的结论是与其在余额上反复找平衡不如把每一次资源变动都记录成不可变的事件谁要余额谁自己去重放。这个思路就是REA的雏形。1.2 REA是什么三个单词一条价值链REA等于Resource资源、Event事件、Agent参与者。这套模型把企业经济活动浓缩成三个问题什么东西在变发生了什么动作谁参与了这个动作举一个很普通的例子一杯咖啡从豆子到顾客手里。咖啡豆是资源咖啡豆入库、烘焙、制作、售出、收款都是事件采购员、咖啡师、顾客都是参与者。库存余额不是又一个实体而是“入库事件总量减去出库事件总量”的结果。这个直觉比一堆“借库存贷应付”的科目表直接不少。很多年前我第一次看到这三个词心里也犯嘀咕这不是流水账吗等我真正把传统借贷账目拆开才发现事情没那么简单。1.3 它解决的核心问题把事实与结果分离REA最关键的一点是区分“事实”与“派生结果”。在传统系统里库存余额、应收余额被当成事实在REA里它们只是视图。真实的事实是事件流。谁把余额表删了都不怕事件还在就能重建谁把事件删了那才叫事故。这个思想不是我的发明很早就有学者在企业建模领域讨论过。后来我在技术评审里反复复用它的核心价值一是可解释性任何余额都能从一个事件链推导出来二是可追溯性从发货单能一路追溯到供应商、采购单、付款三是可扩展性新增业务类型时不用改一堆寄存的逻辑只是新增一种事件类型。但很多人听到这里会追问那借贷记账法到底哪里不行下面我就认真聊聊这个。2. 传统借贷账目体系的卡脖子点记录的是结论不是事实2.1 借贷分录的隐含假设借贷记账法本身是个非常聪明的设计它把每笔经济业务拆成“资产来源”和“资产去向”两个方向用“有借必有贷借贷必相等”保证账目平衡。问题不在于这个设计本身而在于系统运转久了之后大家把“科目余额”当成了唯一事实源。在企业系统里总账科目余额本质上是聚合结果。聚合结果可以被重建但很多系统设计把它当成不可替代的事实于是每次修数据都要做冲销、补记、调账。调来调去余额看齐了但业务事实被搅成一锅粥。REA模型并不否定记账功能它只是把借贷背后的经济事件、资源、参与者重新显式建模。打个比方借贷体系相当于直接放映电影截图REA则把拍摄素材、剪辑脚本、演员分工都存了下来。截图可以随时从素材里再生成前提是素材不能被随意覆盖。2.2 多币种、多仓、多租户下的复杂度爆炸当业务只有一台收银机时借贷表也够用。但业务一复杂整个链条就开始卡脖子。拿多仓库举例A仓发货到B仓数量只是从一个仓库挪到另一个借贷里却要先记“A仓库存减少”再记“B仓库存增加”。商品价值不一样时还要补一笔“库存差异调整”。业务逻辑每多一层这种调整分录就多一类。多币种更痛苦。一个订单用美元成交、用人民币对账、中间还要评估汇兑损益借贷系统里的科目组合成倍增加。其实每种币种对应的现金是一个资源汇兑损益不过是一个特殊事件导致的资源价值重估。如果用REA事件表加一个币种维度再定义一次重估事件即可。多租户场景同样如此。传统建模往往把客户编号挂在一个大维表里再用过滤条件把所有查询包一层。REA天然以Agent区分主体授权关系本身就是模型的一部分不需要额外的租户魔改。2.3 用事件对偶替代借贷符号再看深一层REA里有个概念叫对偶关系。我理解它就是在回答“为什么会有借贷相等”在现实经济行为中一个资源的流出往往伴随另一个资源的流入。卖出咖啡是咖啡豆库存流出对应现金流入采购生豆是供应商那边的资源流出对应我们这边现金流出。两个事件必须配对这个配对关系被显式建模成事件与事件之间的链接。一旦把对偶关系建模出来系统就比传统借贷表多了一个能力可以在不依赖会计科目的情况下推导出任意两个资源之间的因果联系。看账本时看到的不是孤立的金额而是资金怎么从A跑到B再到C的路径。这也是我之前觉得“这不就是流水账”不准确的原因它确实是流水账但是一张把流水中每一滴水的来源、去向、责任人都标注清楚的流水账。下一节我们把它的边界真正落到表结构上。3. 资源、事件、参与者三个构件的边界怎么拿捏3.1 Resource不是产品目录而是价值载体Resource在REA里定义得很宽库存商品、现金、应收账款、固定资产、知识产权甚至外包团队的时间盒都可以是资源。判断标准很简单第一它有价值第二它能被企业计划和控制第三它能因为一个事件增加或减少。对实施者来说最常见的误区是把资源表当成产品目录。产品目录里那些没有库存核算价值的元数据属性应该放在单独的产品维表里资源表只管“算得清增减”的账。比如同款咖啡豆有“在库”和“在途”两种状态与其建模成两个资源类型不如建模成同一资源类型下两个独立资源实例或者让事件的时间戳自然表达在途与否。状态永远是视图不要让它轻易渗透到资源主数据里。我在实际项目里养成了一个习惯每新建一个Resource之前先问自己一句未来会不会为它单独创建Event如果不会它可能就是另一个资源上的属性而不是独立资源。这句话帮我砍掉过不少设计冲动。3.2 Event不可再分的经济动作不是操作日志Event是最容易被高估也最容易做错的一层。它要求记录的是“不可再分的经济动作”比如一次收货、一次发货、一次收款、一次转账。判断标准可以拆成几条是否能让某个Resource的数量或价值发生变化是不是在真实世界里已经客观发生拆成更细的子事件后是否还有业务意义举个例子“点击下单”这个动作本身不是经济事件它只是表达购买意图“创建订单”也不是经济事件订单属于契约真正的事件是“仓库发出5箱货”。很多团队设计表时把订单创建、订单审核、订单配送、订单完成全部塞进一张大日志表美其名曰事件驱动。这会让后续做余量计算时非常痛苦因为一半记录根本不该影响任何资源。事件表里最需要注意的是方向与事件类型的分开存储。方向INFLOW/OUTFLOW是计算余额必须的信号类型是业务语义。同一种类型在不同场景下可以有不同的方向比如“销售发货”对库存是OUTFLOW对销售费用可能是INFLOW。把两者塞进同一个字段后期会带来数不清的隐患。3.3 Agent责任归属与授权边界Agent解决的是三个问题谁发起了事件谁承担了结果谁授权了这件事落地时最常用的做法是把参与者区分为内部参与者与外部参与者。内部参与者就是本系统内的员工、角色外部参与者就是客户、供应商、承运方。权限控制层面Authorization表专门在参与者和事件类型之间建立授权关系。比如仓库主管可以授权发货事件采购员可以发起采购合同。这个设计比单纯塞进现成权限框架里更贴合业务事实因为业务授权不只是“能不能点按钮”而是“能不能对该资源执行该经济事件”。经验之谈Agent表不要企图记录所有组织架构。部门层级、汇报线是组织信息不是经济参与者。把部门硬塞进Agent里短期看只是多存几个字段长期看部门调整时你根本不知道要不要新建一条资源变更事件。4. 一个销售库存业务的Schema从事件反推账本4.1 先建Event表而不是先建余额表REA落地时我推荐从Event表起步。这跟传统系统正好相反传统系统先定库存余额表然后为了追溯才去加流水表结果流水表永远滞后永远对不上。如果先定义事件所有报表都是从它派生出来的投影表加字段本质上是加一种新事件类型改动可控。下面这组DDL非常精简适合销售库存场景起步。实际生产环境还需要补充租户ID、数据版本号等字段这里先保留最核心的部分。CREATE TABLE resource ( resource_id BINARY(16) PRIMARY KEY, resource_name VARCHAR(64) NOT NULL, resource_type ENUM(ITEM, CASH, ACCOUNT, SERVICE) NOT NULL, unit VARCHAR(16) NOT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE ); CREATE TABLE agent ( agent_id BINARY(16) PRIMARY KEY, agent_name VARCHAR(64) NOT NULL, agent_type ENUM(INTERNAL, EXTERNAL) NOT NULL, is_customer BOOLEAN NOT NULL DEFAULT FALSE, is_supplier BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TABLE event ( event_id BINARY(16) PRIMARY KEY, event_no VARCHAR(32) NOT NULL UNIQUE, event_type VARCHAR(32) NOT NULL, direction ENUM(INFLOW, OUTFLOW) NOT NULL, resource_id BINARY(16) NOT NULL REFERENCES resource(resource_id), quantity DECIMAL(18,4) NOT NULL, unit_price DECIMAL(18,4), occurred_at DATETIME NOT NULL, recorded_at DATETIME NOT NULL, internal_agent_id BINARY(16) NOT NULL REFERENCES agent(agent_id), external_agent_id BINARY(16) REFERENCES agent(agent_id), UNIQUE KEY uk_dup_guard (event_no, resource_id) );注意event_no这个唯一键生产环境里对应的是上游单据唯一号。两笔相同的业务如果被重复录入数据库层会直接拒绝这是幂等的第一道防线。第5章我会细聊这条设计为什么重要。4.2 当前库存怎么算一条SQL直接求和需要当前库存时不需要去读任何余额表直接把Event表按Resource和方向求和即可。SELECT r.resource_name, SUM(CASE WHEN e.direction INFLOW THEN e.quantity ELSE -e.quantity END) AS current_stock FROM event e JOIN resource r ON e.resource_id r.resource_id WHERE e.event_type IN (PURCHASE_ARRIVAL, SALES_SHIPMENT, STOCK_ADJUSTMENT) GROUP BY r.resource_id, r.resource_name;这里我把查询限定在影响库存的事件类型里而不是把Event表里所有记录都拉进来。很多新手会把所有事件全加一遍结果把收款、账务调整之类的INFLOW也加进了库存。所以在设计事件类型时应该给每类事件定义“它影响哪类资源、方向是什么”形成一张事件类型注册表后续所有查询都依赖这张表。4.3 应收应付自动派生发票和收款本身就是一进一出REA还有一个非常好用的红利应收账款余额不是单独维护的而是从发票事件和收款事件里推导出来。每张发票是一个INFLOW事件经济资源是“应收账款”每次收款是一个OUTFLOW事件把应收账款减掉。合在一起就是当前客户欠款SELECT a.agent_name AS customer, SUM(CASE WHEN e.event_type INVOICE THEN e.quantity * e.unit_price ELSE 0 END) - SUM(CASE WHEN e.event_type PAYMENT THEN e.quantity * e.unit_price ELSE 0 END) AS outstanding FROM event e JOIN agent a ON e.external_agent_id a.agent_id JOIN resource res ON e.resource_id res.resource_id WHERE res.resource_type ACCOUNT AND e.event_type IN (INVOICE, PAYMENT) GROUP BY a.agent_id, a.agent_name;这种设计下退款就是一笔新的负数量事件而不是去修改原事件。历史库存增量没有被破坏审计链路也清晰得多。5. 报表、审计与事件溯源从REA得到的三个红利5.1 报表不再是“查余额”而是“重放行为”传统报表是查一张快照表REA是重放一段行为流。我们把余额理解成暂停帧我要看昨天的库存不依赖定时快照只需要加一个时间过滤条件重放到昨天那一刻。这带来一个额外能力任何业务问题都能回到原始行为去排查。比如财务问“这批货物价值怎么波动这么大”传统系统只能看到调账后的金额REA里可以把入库、出库、报损、移仓事件全部摊开每一步写的是什么原因一目了然。它天然适合做审计追踪因为事件流本身就是一笔完整的行为账。5.2 不可变事件、幂等键、业务时间戳三个红线必须一起立想享受REA的好处前提是把底层规则立住。我在项目中始终坚持三条红线。第一事件表只做INSERT禁止UPDATE和DELETE。哪怕录错了也只能再补一条调整事件把错误影响抵消掉。听起来繁琐但换来的是一劳永逸的简单任何时刻的事件流都是不可篡改的历史。第二幂等键必须在数据库层兜底。最稳妥的方式是给业务事件号加唯一索引再配合外部消息或批处理任务里的去重逻辑。没有唯一索引再好的应用层去重也可能在并发下漏掉。第三业务时间戳与系统入账时间戳分开存。occurred_at记录真实业务发生时间recorded_at记录系统落库时间。报表按业务时间聚合问题排查按入账时间查询两者互不干扰。跨时区、跨系统的场景下这个双时间戳设计能避免很多扯皮。5.3 和事件溯源、微服务架构的配合REA模型与事件溯源架构天然亲和。微服务拆分时服务之间传递的往往就是业务事件比如库存扣减、订单付款成功、退款发起。REA正好给这些事件定义了标准范式有资源、有参与者、有方向、有时间。但这里我有一个明确的提醒不要把REA事件和消息队列里的消息画等号。REA事件是已经发生的经济事实持久化在主库里MQ消息是传输载体可能丢失、乱序、重复。正确做法是把REA事件作为唯一事实源消息只是它的一个副本或者投影。下游服务收到消息后可以异步更新自己的投影表投影过期了就从REA事件重放一次。6. 用REA建模时最容易翻车的三个坑6.1 把Event表写成操作日志第一次落地REA时我见过最典型的翻车现场就是Event表被当成万能的“操作日志”。用户登录、按钮点击、页面浏览也往里插。结果查询余额时全表扫描越来越慢而且业务查询根本不知道该过滤哪些事件类型。判断标准我再重复一遍只有会引起Resource增加或减少的动作才配称为经济事件。行为日志请单独放到日志系统两套数据的生命周期、存储策略、访问模式完全不同混在一起只会两头难受。6.2 过度建模把流程节点全部事件化另一种相反的极端是把所有东西都建模成Event。订单创建、订单审核、订单配货、订单发货、订单完成全部分解成一串事件。表面上挺RER实际上业务查询需要跨十几个事件类型才能拼出一个订单状态开发效率急剧下降。原因在于订单本身是契约不是经济事件。契约表达的是双方约定事件表达的是资源实际增减。下单时不产生资源增减货物出库才产生。契约表和事件表分开建模一个管承诺一个管事实这样订单状态自然有了归属。6.3 忽视投影层的性能设计不能只建一个Event表就上线。事件表会无限增长业务报表不可能每次都全表重放。合理的做法是在同一事务里写事件和更新投影表或者用异步消费者更新投影表。库存余额、应收余额、销量统计这些高频查询走投影表历史分析走事件表。我曾经为了省事报表直接对Event表做全量聚合数据量到千万级以后查询直接卡死。后来加了一张库存投影表每次写入事件时同时更新投影报表查询从秒级变成毫秒级。所以建议把Event表当作唯一事实源但不要让它成为唯一的查询源。6.4 把REA套用到根本不需要的地方REA也有明确的适用边界。纯文档管理、简单CMS、个人账本这类业务资源流动极少事件类型单一引入一套完整的事件模型只会增加表和逻辑的复杂度。倒不如用普通业务表更直接。什么时候应该用REA我的判断是业务里有多种资源频繁流动存在多个参与者和多步审批授权同时你还特别需要追溯历史行为和生成弹性报表。只有这类系统REA的投资才值得。7. 我的落地最小集十个表起步7.1 表清单一个保守但完整的REA骨架如果你准备在一个真实业务里尝试REA我建议从下面这组表开始。不追求一次覆盖全部理论先保证核心链路跑得通。序号表名作用备注1resource资源主数据库存、现金、应收账户都在这里2agent参与者主数据内部角色与外部客户/供应商3authorization参与者对事件类型的授权控制谁可以发起哪些行为4event经济事件明细唯一的原始事实来源5event_duality事件对偶关系发货事件对应收款事件6contract契约头订单、采购单的承诺信息7contract_item契约行订单行、商品、数量、价格8policy业务规则折扣、信用额度、发货约束9projection_inventory库存投影表高性能当前库存10projection_receivable应收投影表高性能客户欠款这十张表并不复杂但已经覆盖了一个普通销售库存业务从承诺到履行再到结算的完整闭环。7.2 老系统迁移的三步走老系统里没有事件只有一堆余额和账单怎么迁移我实践下来比较稳的顺序是三步。第一步把历史余额做成初始事件。先把当天库存、应收、应付的存量确认下来作为时间起点生成一条“期初事件”。这是整条事件链的地基。第二步按单据往回补近期事件。从最近三个月起把采购入库单、销售发货单、收款单、付款单都反写成Event表。过程中会遇到单据编号缺失、上游系统时间不准等问题尽量在转化期就把脏数据清掉。第三步双写并行并做日级对账。老系统继续跑一段时间新系统同时接收新业务事件每天比对库存与应收余额。只有连续两到三周误差为零才敢切流量。7.3 什么情况下我劝你“别用REA”最后说句掏心窝的话千万别因为这篇文章觉得REA是银弹。如果你的团队只有两三个人业务一个月改不了一次余额表直接一把梭反而更快。我个人的体会是REA最大的价值出现在业务进入高速变化期的时候。那时候你改一个库存规则传统系统要动七八个模块REA系统可能只加一种事件类型。但这是用前期的建模成本换来的——你需要愿意接受“事件先于视图”的设计思想并且有足够的纪律性维护事件不可变、幂等和时间戳这三条底线。最后再分享一个小技巧技术评审上遇到“REA是什么”这种问题别只背Resource、Event、Agent三个单词。把一杯咖啡的完整生命周期讲一遍——从采购豆子、烘焙、制作、售出到收款每个环节谁是资源、谁是事件、谁是参与者链路天然就是一套REA。把故事讲清楚比背概念有用得多。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表