REA模型实战指南:用事件溯源重构订单与财务系统

📅 发布时间:2026/10/21 5:00:13
REA模型实战指南:用事件溯源重构订单与财务系统
第一次在需求文档里看到项目标题只有三个字母“rea”的时候我确实愣了一下。这种缩写是最容易产生歧义的不带点上下文你分不清它是某个开源工具的代号还是某个业务模型的简称。直到它出现在一次业务系统重构的设计评审里配合着“资源-事件-参与者”四个字一起出现方向才终于明确了这是REA模型Resources-Events-Agents一套最早用于会计信息系统建模、最近几年在事件驱动架构圈子里又被重新捡起来的设计思路。这篇东西不是做成知识科普而是我最近在一套内部系统后面统一叫模拟项目X里把REA从理论搬到生产环境之后踩坑和救火攒出来的记录。核心会讲REA到底解决什么问题、事件表和资源表怎么设计、存量数据怎么迁、查询性能怎么救以及团队怎么从“看到订单表就只想到订单状态”的状态思维里走出来。如果你正在做订单中心、财务系统、积分资产、供应链库存这类偏“账本”性质的业务或者正打算落事件溯源这篇文章应该能省你不少功夫。其实第一次接触REA我也本能觉得“这不就是一套数据库设计规范吗”。但真正做下来才发现它改的不是表结构而是你对待业务数据的姿势。下面从项目背景开始说起。1. 项目概述三个字母背后是一整套事件驱动建模的思路1.1 REA是什么它把一个业务事实拆成资源、事件、参与者三件套REA这个词组最早在会计信息系统的研究里被提出上世纪九十年代初就有了比不少做后端的同学年龄都大。它当时要解决的问题很朴素传统的复式记账模型是围绕“借”和“贷”两列展开的满足会计准则没问题但一旦想从数据库里回答“这批货到底是哪个客户在什么时间通过哪个销售员卖出去的”这种问题就会发现科目表里根本存不下这些业务语义。REA把任何一笔业务无论多复杂都拆成三个基本成分资源Resource有价值、能被转移或消耗的对象比如库存商品、现金、应收账款、积分。事件Event事实发生的动作比如下单、出库、收款、退货。参与者Agent动作的归属者分为内部参与者员工、部门和外部参与者客户、供应商。我常常跟人举一个生活化的例子你去菜市场买菜摊贩称了两斤西红柿资源你扫码付了十块钱事件卖家是你和摊贩参与者。这套模型不做任何借贷判断它只记录“什么资源发生了增减由谁触发谁参与”。这是它跟传统账务模型最大的分水岭一个先定义规则一个先保存事实。传统账务模型和REA的差异我用一个简单对比来说明对比维度传统复式记账模型REA模型核心对象科目、借贷分录资源、事件、参与者关注点是否符合会计准则是否记录完整业务事实历史可追溯性依赖日志和备份事件本身即历史扩展业务语义需要加字段或加表增加新事件类型即可这种区别决定了它在现代业务系统里能有比“会计专用”更广的用武之地。1.2 为什么老掉牙的理论现在又成了香饽饽这次在模拟项目X里选REA不是拍脑袋而是被三个现实问题逼出来的。第一个是事件溯源。新需求要求保留每一笔业务的完整历史不能只留当前状态。传统订单表里order_status一改上一个状态就没了审计只能靠操作日志去猜。而REA天然围绕事件建模事件本身就是要记的那张主表历史不会丢。第二个是微服务拆分。以前订单、库存、应收三个模块各自为政订单状态和库存流水经常对不上账。REA给了大家一个统一语言“资源增减”和“事件发生”可以在消息层面直接流转各服务不需要维护同一套订单状态机只需要订阅事件、投影自己的读模型。第三个是合规审计。业务上要求留存完整的凭证链而REA的事件-资源关联天然就是一条凭证链。出库单、收货单、发票、收款回单全部变成相互关联的事件记录审计的时候不需要再靠翻聊天记录和Excel凑数。当然它也不是万能的。如果业务逻辑简单到“一张表加个状态字段就够”硬上REA就是过度设计。它更适合业务轨迹长、审计要求高、状态机复杂、跨系统需要对齐的领域。这也是我想先放在最前面说的选型之前先判断环境别因为新奇就用。2. 核心概念拆解先把资源、事件、参与者这三个主角认全2.1 资源不是数据库里的所有表而是真正有价值流向的对象资源是REA里最容易理解错的一个词。很多人一听“资源”就觉得“是不是要把项目里的主要实体都归过来”这是误区。REA里的资源必须满足一个条件它在业务流程里会作为价值载体被转移、消耗或者生产。所以客户、员工、供应商这些人不是资源他们是参与者订单、发票这种单据也不是资源它是事件的结果或者载体。真正算资源的在订单场景里是库存商品和现金在积分场景里是积分余额在租赁场景里是场地使用权。判断标准很直接问一句“它在不在进、出、存三个动词的宾语位置上”。在的话是资源不在那不是。设计资源表时我建议至少区分两样东西资源类型resource_type和资源实例resource_id。类型对应一把商品的SKU实例对应一个库存批次或者一件资产。模拟项目X里我们把SKU、库存批次、现金账户、积分池都放进了资源表用resource_type区隔而不是建一堆结构相似但名称不同的表。这样事件关联资源的路径就变得统一事件 → 资源实例ID剩下靠类型来判定语义。资源这个维度还有一个容易忽略的点属性快照。库存商品在出库那一刻的重量、质量等级、所在仓库可能和现在不一样。REA要求你在事件里保存的是“业务发生时的资源状态”而不是“现在的资源状态”这个细节在追溯历史时候价值极大后面讲到事件payload会说。2.2 事件流水账里的每一行才是真正改不了的“事实”如果REA里只能记住一个概念我会让你记“事件”。事件是发生在某个时间点上的业务动作它代表一个不可逆的事实。在建模时我们经常把事件分成“承诺Commitment”和“行动Execution”两个层次。订单算承诺发货算行动采购合同算承诺到货入库算行动。这个区分非常有价值承诺可以由人取消或修改但行动一旦发生对应的资源增减就不能凭空抹掉。很多系统出事就出在把“承诺”和“行动”混在一个状态字段里管理流程一复杂就乱套。落实到表设计上每个事件必须有事件唯一的业务编号、事件类型、发生时间、经办人内部参与者、对手方外部参与者以及当时附带的上下文数据。事件里建议存“当时的业务快照”比如出库时的仓库读数、收款时的金额和渠道后面追溯的时候不需要去别的系统翻记录。事件还有两个铁律一旦落库不允许修改不允许删除。要纠错就再写一个“更正事件”要取消就写“取消事件”。这个规矩一开始就要立死否则后面所有审计和回放都会变成踩泥坑。事件本身的颗粒度也值得推敲。很多人拿到一个需求喜欢把一长串动作塞进一个“提交”里比如把“下单支付发货”全写进一个completed事件。这么做后患无穷查询时确实省事但一旦中间某个环节出错你就再也没办法知道“支付到底有没有发生”了。我现在的原则是一个业务动作一个事件宁可拆细不要合并。2.3 参与者权责清晰是审计追溯的前提参与者分成内部和外部两类这个划分不只是为了分类更是为了权限和追责。内部参与者比如销售员、仓管员、财务审核人他们跟后台账号体系相关外部参与者比如客户、供应商、物流公司他们跟客户主数据、供应商主数据相关。REA要求每个事件至少关联一个内部参与者和一个外部参与者。这一点非常管用任何一笔资源变化都能回答“谁弄的”“发生在谁头上”。在模拟项目X里我们直接把operator_id内部经办人和counterparty_id外部参与者设计成了事件表的必填字段。可能有同学觉得“不是所有业务都有外部参与者吧”比如内部调拨外部参与者确实没有这时候我会在模型层面允许它为NULL但业务规则会要求调拨事件必须关联两个内部参与者转出方和接收方。灵活性和约束力要分开靠业务角色去兜底。参与者这里还有一个容易被忽视的细节参与者角色是会变化的。同一个客户在售前是“线索”在售卖是“买方”在售后是“发起退单者”。REA模型不建议你把角色信息挂在参与者表里而是挂在事件上因为角色是“某个事件里的角色”不是参与者的固有属性。顺着这个思路风控模块要识别“一个客户三个月内换了几个身份角色”就很容易事件表里一查便知。2.4 三类关联关系让模型从“三张孤立的表”变成“一张事实网”资源、事件、参与者只是三块积木让它们能拼起来的是REA定义的关联关系。我用表格概括一下关系类型连接的双方作用典型例子控制关系事件 → 资源事件导致资源增减发货事件减少库存商品对应关系事件 → 事件两个动作业务上一一对应收款事件对应销售事件来源关系事件 → 参与者确定事件的权责归属客户参与下单事件控制关系是核心。每个事件至少要让一个资源增加流入并让另一个资源减少流出。销售出库的同时库存商品减少应收账款或者现金增加这就是典型的“给-取”配对。我常把这个关系理解为任何一笔完整交易都必须有一物换一物的逻辑闭环。如果一笔事件只有资源的减少没有资源的增加或者是反过来那就要警惕业务链路是不是漏了环节。对应关系用来解决“业务链路多跳但数据割裂”的问题。比如“销售出库”事件和“收款”事件在传统系统里分别躺在库存模块和财务模块很难说清哪个收款项对应哪张单。REA里的event_link表把两个事件ID挂起来这条链就能跨模块追溯。跨系统协作时对应关系还能帮你做自动对账只要事件关联不漏应收余额和收款金额必然相等对不上就是上游事件缺失或者关联错位。来源关系强调的是程序不能替业务猜测权责归属。仓库发货内部参与者是仓管员一笔退款内部参与者是客服源头写对了后面做风控的“行为归属”分析才有准确数据。3. 从理论到表结构按REA重做订单与财务模块3.1 传统订单模型有哪些暗病动手之前先说反面教材不然你体会不到REA表的优势。常见的订单模型是三张表起步orders、order_items、payment_records再加一张订单状态流转表。表面上开发效率很高但用久了你就发现三个问题。第一个是状态更新会覆盖历史。order_status从“已支付”变成“已退款”你只知道当前是退款状态不知道它背后经历过什么除非你另外记全量日志。第二个是跨模块对账靠“硬凑”。出库记录在一套系统收款记录在一套财务系统两者之间没有结构化关联月底对不上账只能拉两张Excel用工具肉眼找差异。第三个是字段责任边界不清晰。一个状态字段里既塞“订单已创建”这种承诺也塞“已发货”这种行动还塞“已收货”这种履约结果状态机越来越庞大多个模块的人改同一张表。REA模型不反对有订单概念订单照样可以有但它不再作为唯一主表。订单在REA里更像“一串事件的入口”是承诺事件的聚合视图。这种定位变化根子上解决了“一张表承担太多职责”的问题。3.2 核心表结构事件、资源、参与者、关联表落表设计时我倾向于事件表为唯一的事实主表其他都是它的配料。直接给一套我在模拟项目X里用过的核心DDL供参考CREATE TABLE rea_event ( event_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, event_no VARCHAR(64) NOT NULL, -- 业务事件唯一编号 event_type VARCHAR(64) NOT NULL, -- cargo_issue / payment_received ... occurred_at TIMESTAMPTZ NOT NULL, -- 业务发生时间 operator_id BIGINT NOT NULL, -- 内部参与者 counterparty_id BIGINT, -- 外部参与者可空 payload JSONB, -- 事件快照与扩展属性 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by VARCHAR(64) NOT NULL, CONSTRAINT uq_event_no UNIQUE (event_no) ); CREATE TABLE rea_resource ( resource_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, resource_type VARCHAR(64) NOT NULL, -- SKU / CASH_ACCOUNT / POINTS resource_code VARCHAR(64) NOT NULL, resource_name VARCHAR(128) NOT NULL, status VARCHAR(32) NOT NULL DEFAULT ACTIVE, current_quantity NUMERIC(18,4) NOT NULL DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE rea_agent ( agent_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, agent_type VARCHAR(32) NOT NULL, -- INTERNAL / EXTERNAL agent_name VARCHAR(128) NOT NULL, role_code VARCHAR(64) ); CREATE TABLE event_resource_link ( link_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, event_id BIGINT NOT NULL REFERENCES rea_event(event_id), resource_id BIGINT NOT NULL REFERENCES rea_resource(resource_id), flow_type VARCHAR(16) NOT NULL CHECK (flow_type IN (INCREMENT,DECREMENT)), quantity NUMERIC(18,4) NOT NULL DEFAULT 0, unit_amount NUMERIC(18,4), remain_after NUMERIC(18,4) ); CREATE TABLE event_link ( link_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, event_id BIGINT NOT NULL REFERENCES rea_event(event_id), related_event_id BIGINT NOT NULL REFERENCES rea_event(event_id), link_type VARCHAR(32) NOT NULL );这套DDL有几个关键点解释一下。rea_event里我特意单独建了event_no业务编号而不是直接用自增主键。原因是消息队列重试、接口幂等、多系统联调都要靠业务编号对齐自增主键只能内部用对外没有语义。event_type我用了“cargo_issue”“payment_received”这种动词对象的结构不用“订单完成”这种模糊状态词就是因为事件应该描述发生了什么而不是描述事情做到哪一步。event_resource_link是事件和资源的多对多关联表。一个出库事件可以同时影响多个资源批次一个入库事件也可以影响多个SKU。flow_type里的INCREMENT和DECREMENT定义了资源增减方向。这里我不推荐把“收入”“支出”这种会计语义写进去因为REA的核心是记录事实方向借贷分类是之后推导的提前写反而容易把模型做歪。remain_after是冗余字段本意是查剩余量更快。但它有一个副作用如果事件回放顺序出错或者并发插入冗余值可能和后面事件计算出的余额不一致。所以要存可以但查询必须定期用回放逻辑校验或者干脆不存需要时从快照事件流算。我在生产里是存了的但专门加了一个对账任务每夜扫描校验每个资源最后一个事件的remain_after和资源表current_quantity是否一致。这个校验任务上线以后确实抓到了几次并发写入导致的可疑数据也算是对冗余字段策略的一个提醒。payload字段是JSONB适合存事件快照。比如支付事件要存支付渠道、支付流水号、支付客户端IP出库事件要存批次号、复核人。我不建议把这些零零散散的字段全建列出来因为事件类型太多列爆炸会让表变得臃肿。事件相关的强约束字段比如事件编号、类型、时间、参与者放主列非共性的扩展属性放payload。查询如果确实要按某个扩展属性过滤可以再针对高频属性做JSON索引或抽出额外投影表不要一开始就设计几十个冗余列。3.3 从事件流还原状态回放与投影事件表记录的是历史事实但业务方法大概率还是会问“现在库存还有多少”。这个“现在”是从历史事实推导出来的也就是投影projection。最简单的回放逻辑是取某个资源的所有DECREMENT事件把quantity累减把INCREMENT事件累加加总后就是当前数量。写成伪代码# 伪代码资源当前数量 初始值 所有增量 - 所有减量 def replay_quantity(resource_id): current get_initial_quantity(resource_id) for link in get_increment_links(resource_id): current link.quantity for link in get_decrement_links(resource_id): current - link.quantity return current回放是一条兜底路径平时别拿它做查询。真正扛查询压力的是投影表比如库存快照表、应收余额表。投影可以在事件写入后异步更新事务里先写事件和关联事务提交后再根据事件内容增量更新投影表。这本质上就是CQRS的读模型。查询走投影审计走事件两条路分离。可能有人会担心“那写投影表的时候挂了怎么办”。我现在的做法是投影更新失败不阻断主链路事件已经落库就丢不了数据后台有个重建任务按资源粒度定期扫描事件表重新投影。只要事件表是真实数据源投影表再怎么花式重算都不会破坏正确性。需要注意的是投影表不是简单地把事件表复制一份它是面向查询的优化。比如库存投影表会冗余sku_id、仓库、批次应收投影表会冗余客户、账期。这些都是从事件里推导出来的“当前态”随时可以推倒重建所以在做表结构评审时不必像传统业务表那样小心翼翼加字段放手优化就可以了。4. 实操过程记录在模拟项目X里一步步把REA立起来4.1 第一步从业务动词里挖事件清单落地REA最忌讳一上来就画表结构。我先拉上业务方和几个模块的开发找了一下午做了一件看起来跟技术没关系的事梳理“业务动词”。只要把动词列出来事件清单就出来了。“客户下单”“仓库发货”“客户付款”“财务确认到账”“库存盘点差异入库”“退货商品入库”这些都是动词短语每个未来就是一个event_type。当时还做了一个规定动词要落在一个明确的时点不要用“客户处理订单”这种含糊的动词。时点清晰事件发生的occurred_at才能填事件流才是一条真正的时间轴。动词里还要避开系统实现层面的说法比如“插入支付记录”就不行要用业务语言“客户完成支付”。因为事件表最终是要给业务审计看的不是只给开发看的。4.2 第二步给每个事件配资源和参与者事件清单出来后我给每个事件配了资源和参与者这一步直接决定关联表有没有数据。还是拿对单的场景说事件类型影响资源增减方向内部参与者外部参与者客户下单应收订单承诺INCREMENT销售员客户仓库发货库存商品DECREMENT仓管员客户仓库发货应收款资产INCREMENT仓管员客户客户付款现金账户INCREMENT出纳客户采购到货库存商品INCREMENT收货员供应商采购到货应付义务INCREMENT采购员供应商配的时候最容易遇到“这笔发货对库存是减但对什么资源是增”的问题。如果没有发现对应的增量资源说明这个事件模型的闭环没画完。REA的强制约束这时候就在帮你堵业务漏洞。比如“仓库发货”配完发现只减了库存却没有增加应收那个下午就被业务方拉着一起盘清了“发货后到底应该确认为现金收入还是应收账款”这个原本没人说得清的财务口径。单这一项就避免了一次月底对账事故。4.3 第三步建立事件对应关系事件之间不是孤立的我建了一套event_link把动作串起来。比如“仓库发货”事件和“客户付款”事件通过PAYMENT_FOR_SALE关联“采购到货”和“出纳付款”通过PAYMENT_FOR_PURCHASE关联。为什么要单独建关联表而不是在业务表里加外键因为一个事件可能对应多个事件。客户一次性支付三张历史订单的款项那么支付事件要关联三条销售事件一笔退款同时冲抵订单和运费退款事件要关联两个资源事件。多对多的场景用外键字段做不了只能用关联表。对应关系在迁移阶段还有一个妙用它可以做“链路补偿迁移”。老系统里可能存在半截状态比如订单已经出库但收款记录缺失。通过event_link我能快速找出那些“有发货但无对应收款”的事件把它们列成待处理清单逐条去跟财务核实。这个清单比从状态字段里扫“待收款”要可靠得多因为状态可能是维护错的但事件关系和差额不会骗人。4.4 第四步定义幂等键和事件编号规则事件系统上线后最怕两件事重复写入和乱序到达。重复写入主要是消息重试造成乱序主要是多节点并发生成导致。重复写入靠唯一约束挡我拿event_no做唯一键调用方要生成一个全局唯一的业务事件号。生产环境的规则是“日期业务段随机数”由业务服务生成数据库只做幂等校验。如果插入时遇到唯一冲突直接丢出幂等异常调用方知道这次重试其实已经成功。这里补充一个我在真实事件里踩过的坑只是数据库唯一约束还不够调用方必须在发消息时就把event_no带上去消息里去消费端再拿一次event_no去插事件表。如果消息里没有event_no消费端重新生成一个就会导致同一笔业务出现两个不同的事件编号幂等就形同虚设。所以事件号必须源头生成、全局透传从请求入口一直带到数据库约束层。乱序到达则常用版本号兜底我在每个资源上维护一个version每次事件应用后version加一。系统收到旧版本的事件可以先缓存起来等待直到版本衔接上再应用避免“先到的晚事件把早事件盖掉”这种问题。还有一个更简单的做法在投影更新时用“occurred_at和event_id组合排序”保证晚发生的事件最后应用避免早事件覆盖晚事件。4.5 第五步存量数据迁移与对账验证最痛苦的是老系统已经跑了两三年订单、库存、账款全在传统表里。我的迁移路线分三步。第1步反向补齐事件。从现有发货表生成cargo_issue事件从收款表生成payment_received事件历史字段尽量填全填不了的用payload里存原始记录。第2步回放校验。拿生成的事件流重新投影一遍库存和应收余额跟老系统月末结算的存量值比对。严格模式下要求差额为0允许小额误差时也必须在迁移报告里写明原因。第3步双跑过渡。老系统照常办理业务新事件表同步记录持续两周每天跑对账报表确认两边余额一致后再切换读流量。其中回放校验的SQL思路大概是SELECT r.resource_code, r.current_quantity AS table_qty, COALESCE(SUM(CASE WHEN erl.flow_type INCREMENT THEN erl.quantity ELSE -erl.quantity END), 0) AS replayed_qty FROM rea_resource r LEFT JOIN event_resource_link erl ON erl.resource_id r.resource_id GROUP BY r.resource_id HAVING r.current_quantity COALESCE(SUM(CASE WHEN erl.flow_type INCREMENT THEN erl.quantity ELSE -erl.quantity END), 0);我拿这个SQL把迁移初期的历史遗留问题全抓了出来有些还是老系统多年的隐性故障比如两笔货记了同一个批次导致负库存。这类问题在老系统里一直没人注意事件模型一上来全暴露了。暴露不是坏事正是确定重构价值的时候。5. 常见问题与排查技巧实录5.1 事件已经落库发现录错了怎么办事件表不允许UPDATE和DELETE但人都会犯错业务也会临时变卦。旧事件错了怎么办写一条更正事件。更正事件有两种。一种是“取消事件”原事件还在但新增一条event_type为cancel_xxx的事件把原事件的资源增减反着做一遍。另一种是“修正事件”把原事件作为被修正对象新增一条payload里写清“修正XX事件的错误”把正确的资源变化写进去。这两条路都能保证事件流完整不会出现“历史凭空消失”的信任危机。查询时如果不想看见被取消的事件可以在投影层过滤而不是去物理删数据。这里要特别提醒取消事件和修正事件里的资源方向千万别写反。我见过一次线上事故取消退款事件时把DECREMENT写成了INCREMENT结果客户退款额加上一个负数账目反而乱了。更正事件写完后务必要用回放校验SQL重新算一遍资源余额确认和业务预期一致再算这个更正动作真正完成。5.2 消息重试导致事件重复资源数量翻倍这是最典型的线上事故。我遇到过客户付款回调没收到就得重推结果重推触发两次写入现金账户被加了两次。解决三板斧第一板斧是业务幂等键。event_no等于unique key第二次插入直接报唯一冲突代码捕获后按成功处理不要报错给用户。第二板斧是消费端记录已处理的消息ID。投递给下游的消息带message_id消费成功后记录在去重表里重复消息直接跳过。第三板斧是兜底的核对脚本。每天早上扫一遍“现金账户变动次数 vs 支付回调次数”不一致进告警。虽然前两板斧已经能挡绝大部分问题但余额类资源还是值得多一道防线。幂等这件事我建议从项目第一天就做而不是等出事故再补。因为一旦线上已经存在重复事件脏数据混在事件流里清理成本比想象中高得多。补历史事件时还要考虑原事件的event_no怎么恢复很多情况下日志根本没留只能靠业务台账硬对极其痛苦。5.3 纯事件表查询太慢报表拉不出来事件表动辄上亿行SQL里按event_type筛出来还能扛但要按客户维度做聚合分析就特别慢因为涉及的扫描范围太大。我的解法是做异步投影加物化视图。库存和余额投影表按资源ID做唯一记录每天只增量更新分析报表用宽表从事件表拉数后聚合到客户、商品、地区维度。核心原则是在线查询绝不直接扫事件大表事件表只作为事实归档和回放源。如果投影也需要历史趋势我会加一张日终快照表每天凌晨把每个资源的截至余额存一行查询历史走势直接读快照表速度能快几个量级。注意日终快照表和数据仓库里的事实事实表不同它就是每天的REA资源快照字段量很小但支撑趋势分析绰绰有余。5.4 团队从“状态机思维”转不过弯来落地过程中最费时间的是人不是代码。很多开发第一次接触REA都会问“那订单状态字段砍掉了前端怎么显示已发货”答案不是不显示而是状态由事件流推导。前端要看的不是“订单状态”这一个字段而是后端在投影层把最近一条事件翻译成当前状态比如最近一条事件是cargo_issue前端就显示已发货最近一条是payment_received就显示已付款。状态是投影的产物不是事实。我组织了两次工作坊让团队拿真实订单在纸上画“事件-资源-参与者”关系画完再对照旧表设计讨论哪里绕、哪里被状态机绕晕。当大家体会到“只要事件对状态随便推导”之后转变就会很快。那个曾经最反对的重构开发在亲手改完一个需求后跟我说“难怪以前改状态流转要拉六个人开会原来我们一直在维护一个不该存在的中心化状态字典。”5.5 常见问题速查表症状可能原因解决办法资源余额凭空多了一倍事件重复写入检查幂等键消费去重表对账脚本审计查不到某笔历史操作旧系统没有补事件迁移阶段补齐事件从日志反推事件编号投影余额与事件回放不一致并发乱序或冗余字段过期校验回放逻辑清理乱序缓存重建投影报表聚合太慢直接扫事件大表建日终快照表和宽表查询只读投影某事件要修改金额事件被错误当成可变数据补更正事件不动原事件客户退单后库存变负取消事件资源方向写反检查flow_type方向补正确取消事件5.6 选型与取舍的几点经验最后还有几句踩出来的感受。REA模型适合在“业务链路长、状态多、需要追溯和跨系统对齐”的项目里用它是加法不是减法前期建模和迁移投入比传统表多后期回报在审计、对账和扩展性上。也不要为了REA而REA。如果你的业务就是一张简单的字典表加两个CRUD页面那事件表反而是负担。判断标准很简单有没有人在月底为对账加过班有没有客户问过“这笔订单当时为什么变成这样”如果有REA的价值就在。另外REA和事件溯源是一对好搭档但不等同。事件溯源是一种技术架构REA是一种业务建模视角。你可以用REA建模存储还是关系表也可以用事件溯源但建模很混乱。最好两者结合即“REA造业务语义事件溯源造技术能力”。我在模拟项目X里还有一个快速见效的做法先挑一条核心链路比如下单-发货-收款做改造试点上线跑一个月用对账自动化和审计查询速度说话再去说服其他模块。没有人会因为听了概念就改变架构但看到了实际数据差异说服力会大很多。写到这里我对“rea”这个标题想说的话基本说透了。引用我经常跟同事强调的一句话先把“今天是几号、谁动了哪件货、账上多了还是少了”这类事实存下来组成真相链后面的查询、报表、风控和审计都是投影随便你怎么重组都行。反过来如果一开始只存结果不存原因日后任何一点业务变化都会变成刺向数据库的一把刀。先动起来吧从第一个事件表开始。一次事件流建模顶得上三个月查账救火。