从订单系统重构到REA建模:事件驱动的业务设计实战 1. 从一次糟糕的订单系统说起去年我接手过一个电商系统的重构项目第一周看代码时差点没把屏幕拍碎。订单流程散落在六七个服务里每个服务对“订单”的理解都不一样有的把支付和订单混在一起存有的把优惠券抵扣直接写成字段塞进订单表最离谱的是库存扣减居然在订单创建时同步执行用户下单没付款就把库存锁死了。整个团队天天在“订单该不该有状态”“支付回调到底该归谁管”这些问题上扯皮。后来我们请了个做过财务系统的老前辈来帮忙梳理他看了我们那张画得密密麻麻的ER图只问了一句话“你们先告诉我业务里发生了什么事什么东西变了谁参与了这个事”这一问直接把所有人问懵了。他接着说自己做业务建模从来不看ER图而是用一套叫REA的思路先承认业务本质上是一串事件而不是一张状态表。那次重构给我留下了很深的印象。REA这个词全称是Resource-Event-Agent翻译过来就是“资源-事件-代理”。它最早是会计信息系统领域提出的业务建模框架后来被不少做领域驱动设计的人拿来当作事件溯源、业务建模的基础思想。你可以把它理解成一套比“数据库表设计”更靠近业务本质的视角Resource资源业务里被消耗、被生产、被转移的东西比如钱、商品、积分、库存。Event事件业务中真实发生的事比如“下单”“支付”“发货”它是会留下记录的动作。Agent代理参与事件的人或组织比如“顾客”“仓库管理员”“供应商”。这套模型最反常识的地方在于它把“订单”这种我们习以为常的东西给拆掉了。它不认为“订单”是业务的基本单位认为业务的基本单位是“事件”——订单只是多个事件提交、支付、发货、签收在某个维度上的聚合视图。想通了这一点很多系统设计的复杂度会瞬间降下来。这篇我就结合那次重构和后来在不同项目里反复使用REA建模的经历把核心思路、实操步骤、容易踩的坑一次说清。如果你是做业务系统、数据建模或者DDD落地的人这篇应该能帮你在下次设计时少走不少弯路。2. 为什么业务建模先想“事件”而不是先想“表”2.1 传统ER建模的隐性隐患新手做设计最常见的路径是拉着业务方问“你们主要有哪些单据”然后对着单据画表格。订单表、订单明细表、用户表、库存表外键一挂ER图出来了评审会上大家都觉得没问题。可上了生产之后问题比想象中来得快。就拿我那次接手的系统来说订单表里直接存了“优惠金额”“支付状态”“物流单号”“售后类型”这些字段表面上一次查询全拿到了实际上每有一个新的业务变化就要去动这张表。比如后来要接入分期支付产品说“就加一个分期状态字段嘛”结果发现订单状态机从“待支付/已支付”变成四五个状态的复杂流转所有依赖订单状态的代码全部要跟着改。到后期没有一个人能说得清订单表里几十个字段到底在哪些场景下会被更新。ER建模的核心问题在于“把现状当结构”。它适合描述一个已经稳定下来的业务但当你要设计一个每天都在演化的业务系统时你画出的每一张表都在提前承诺一种不变性——“订单就是这样的”“结算就是这样的”。而现实业务尤其是互联网业务恰恰是每个月都在变的。REA的思路正好相反。它不关心“订单表长什么样”它先问“业务里到底发生了什么”。这是从“状态”到“流”的转变订单不是一种状态集合而是一连串事件的投影。2.2 REA模型是一种“业务语言”不只是“存储结构”我第一次用REA重新梳理那个电商系统时业务方拿过来的需求是“顾客下单后要能查看物流、申请退款、用优惠券”。按照以前的做法我们会去设计订单表、物流表、优惠券表、售后表。用REA框架后我们做的是这样一层拆解资源商品、钱应收款/已收款、优惠券、库存。事件顾客提交订单产生应付款、顾客完成支付实收款发生变化、仓库发货库存减少、物流资源产生、顾客申请退款退款金额产生。每一条业务规则都能清晰地挂到“某类事件改变了某类资源”这个关系上而不是“某个状态位由谁更新”。这带来的直接效果是你和业务方聊需求时他们说的每一句话几乎都能被翻译成REA里的一种关系字段上不再有那么多分歧。REA最核心的价值是它提供了一套“找词”的方法。业务分析中最难的不是写SQL而是把业务里模糊的表达精确成可建模的术语。业务方说“用户查订单”“客服改订单”“财务对账”这中间涉及的是完全不同的资源和事件。REA强迫你在第一阶段就把它们区分开而不是统统塞进一张大表里。3. REA三要素怎么落进真实业务3.1 Resource资源不只是“数据表主键”初学者容易把Resource直接理解成数据库里的一张表。这是最大的误解。REA里的资源指的是“有价值、可计量、业务愿意去追踪的东西”。它有三个判断标准有价值没有人会为一个无价值的东西去设计一套追踪系统。积分、优惠券余额、商品库存都是有价值的。可计量能够用数量、金额、状态等维度量化。有生命周期资源不是静态的它会被增加、减少、消耗、转化。举个例子电商系统里“购物车”算不算资源购物车本身没价值但它里面的商品条目有。如果把购物车整个当资源建模你会发现很难跟其他资源库存、价格建立统一的关系。正确做法是把购物车拆解成“顾客这个代理”与“商品这个资源”之间的一个中间过程而不是又一个资源节点。我踩过的坑是把“会员等级”当成资源建模。会员等级本身不可被事件增减它是用户累计消费达到阈值后计算出来的派生数据不是资源。正确的是把“累计消费金额”这种可计量的值作为资源会员等级只是它的一个投影视图。资源类型示例是否适合建模为Resource实物资源商品、库存、原材料适合有数量、有状态、会增减资金资源现金、应收款、积分余额适合可计量且会流动数字资源优惠券、会员资格、云存储空间视情况若有余额/有效期内变化则适合派生视图会员等级、订单状态、报表指标不适合是其他资源与事件计算后的结果在实际建模时我建议把资源分成“存量型资源”和“流量型资源”。存量型资源关注当前有多少比如库存流量型资源关注累计发生了多少比如总销售额。REA事件天然负责记录流量资源表只保存流量累积后的快照。3.2 Event事件业务事实的唯一来源REA里的Event是整个模型的锚点也是最难建模的部分。它必须是“真实发生的、不可篡改的业务事实”。什么时候算一次支付不是“用户点了支付按钮”而是“支付网关返回了成功回调”。时间点定义错了后面所有基于事件的统计都会失真。定义Event时有几个非常关键的问题要提前想清楚事件的粒度“下单”和“提交订单”是一回事吗在电商业务里“提交订单”事件会产生“未支付订单”的资源“支付成功”事件才会产生“待发货状态”。粒度太粗会把不同资源的变化混在一起太细又会让建模陷入琐碎。事件是“账本记录”而不是“业务操作”操作是实体的方法事件是已经发生的事实。用户“发起退款”只是一个意图退款是否成立要看后续的审核。所以“退款申请”和“退款完成”是两个事件。事件与资源的关系一个事件通常会增加一个资源的流出、同时增加另一个资源的流入比如销售事件会令“商品库存”减少、令“应收款”增加。这种“双入账”视角来自复式记账法REA直接继承了这一思想。我自己的经验是以“真实世界发生了什么”作为判断标准而不是以“我们的系统做了什么”。有一次设计审批流程团队纠结于“审批通过”和“提交审批”是不是两个事件。实际分析后“提交审批”只改变了一个内部工单的状态并未让任何资源在不同代理之间转移而“审批通过”则授予了该员工某种权限资源。因此前者可以不建模为REA事件后者才是。3.3 Agent代理别把人只当作一个IDAgent定义是“参与事件的人或组织”。初看很简单但实践中常会被忽略一层一个Agent在系统里可能扮演多个角色而不同角色关心的资源是不同的。例如“张三”这个用户在“顾客”角色下下了单之后成为“退货申请人”后来又可能变成“恶意退货”被风控关注的主体。同一个代理在不同事件中行使不同的角色权责。REA建模要求我们至少记录“事件-代理-角色”三元关系而不是给每个事件硬绑一个用户ID。另一个常见问题是“外部系统算不算代理”。在一次积分系统对接中第三方积分商城发来“积分扣减成功”的回调我们不知道这个回调算谁的行为。用REA分析后第三方积分商城可以被建模为“外部代理”它参与的事件是“积分扣减”而扣减的对象是“用户积分余额”资源。这样外部系统和内部系统就有了统一的语言。4. 一次完整建模实战从需求到模型这一节我用一个真实的模拟项目来走一遍完整流程。我们当时需要设计一个“企业内部设备领用系统”需求看似简单员工可以申请领用笔记本电脑、显示器、耳机等资产管理员审批后进行发放离职时需要归还。最初团队直接画了三张表员工表、设备表、领用记录表。看着够用可加了“设备维修”“资产报废”“部门调拨”需求后这套结构根本撑不住。于是我们改用REA重新建模。4.1 第一步找出所有资源、事件、代理先把需求里的名词和动词全部列出来再一个一个归类代理员工、资产管理员、财务涉及折旧核算时、供应商采购设备时。资源设备资产有库存记录与归属、固定资产账面价值、维修保养额度。事件领用申请员工提出领用请求申请时锁定设备资源。发放完成管理员发放设备设备归属权从“仓库”转到“员工”。归还登记员工归还设备设备归属权转回“仓库”。维修记录设备送修维修是改变设备“可用状态”的事件但不转移归属权。你会发现“设备维修”既不是领用也不是归还但它确实改变了“设备可用数量”这种资源。如果当初用ER设计必然会把它当作一个“附属流水表”而用REA后它天然和领用、发放形成一套事件链状态统计变得非常顺。4.2 第二步确定事件时序与资源联动每一条业务规则都可以用“某事件发生后某资源因某代理而变化”来描述。“员工申请领用”事件产生一条“待处理申请”记录同时将设备资源的状态置为“已锁定”。“管理员审核通过并发放”事件让“可用设备”资源减少、“员工名下设备”资源增加。“员工归还设备”事件让“员工名下设备”减少、“库存可用设备”增加。“送修”事件将“可用设备”减少同时增加“维修中设备”等维修完成后反向操作。这套描述最舒服的地方在于每个事件对应的资源变化是确定的不会出现“更新一下设备状态那么复杂吗”这种纠缠不清的问题。后续需求再加“设备丢失”也容易扩展新增“报失”事件让“员工名下设备”减少、“丢失设备”资源增加完全不影响已有事件。4.3 第三步落表时的事件表与快照表分离模型确定后存储结构其实非常自然地就出来了。事件表是业务事实的追加式日志快照表是为了查询效率刻意冗余出来的投影。我们实际建的表结构大概是事件表event_log - event_id - event_type (apply/approve/issue/return/repair/lost) - resource_ref指向对应的资源类型与ID - agent_ref指向员工或管理员 - occurred_at - payloadJSON扩展字段保存事件当时的上下文快照如备注、当时的部门等 资源表device_asset - asset_id - category - status空闲/锁定/在用/维修中/报废 - current_holder_id冗余自事件表便于查询这里有个容易忽略的点事件表要保存“事件发生时的完整上下文”。比如“领用申请”事件发生时员工所属部门是A三个月后调去了B但审计时应该看到的是申请当时的部门A。所以我们在payload里冗余了一份业务上下文而非仅仅外键关联。4.4 第四步用事务与幂等保证数据一致性把事件作为唯一事实来源后最怕出现“事件记录了资源快照没更新”或“重复推送事件导致资源被扣两次”。我们前后的做法是这样同一事件的幂等性给每个事件一个全局唯一的event_id业务上可用“申请单号事件类型操作序号”生成入库时做唯一约束。事件与快照更新放在同一个本地事务里资源表的变更依据事件表里的原始数据进行重算而不是依赖于“前端传来的当前状态”。举例发放设备时应该用“设备当前状态”去校验是否可发放一旦校验通过扣减操作必须与该事件在同一条数据库事务内完成。异步重试不重新扣资源下游服务消费事件时用一个消费进度表记录已处理的事件ID重复消息直接忽略。5. 三个最容易翻车的建模细节5.1 把“状态”当成“事件”模型会变成大泥球最常见的错误是把“订单状态已支付”“内容状态已审核”直接当作一个事件。状态是一系列事件累积后的结果投影它不是事实本身。正确的做法是记录“支付成功”这个事件如果要“已支付”状态用事件流算出来或者维护一个可异步刷新的快照字段。我见过最夸张的案例是团队把状态机里的每个状态转移都建模成事件。系统里Event表一天几十万条状态变更流水但真正的业务价值信息被淹没在一堆无意义的内部切换里。判断一个状态变化是不是应该建模为事件还是看他是否改变了某个资源的账本或者说是否在代理间转移了资源的所有权。5.2 忽略资源“存量”与“流量”的区别统计必翻车在设备系统中“资产总量”是流量值——历史上所有采购流入的设备总量“当前可用设备”是存量值——减去所有流出的结果。如果只建模流量的某几个事件而漏掉了“报废”事件那么存量统计会永远虚高。经验是每一类资源都至少要有一个“增加型事件”和一个“减少型事件”构成完整循环。找不到对应的减少型事件时要警惕模型缺了一块。比如积分资源增加型事件是“签到/消费奖励”减少型事件是“积分消耗”或“积分过期”。有人建模时只写了“积分增加”和“积分扣减”却漏了“积分过期”结果每年年底清积分时对不上账。这类问题在REA建模阶段如果能画出“资源生命周期循环图”当场就能发现。5.3 事件与代理之间缺少角色权限与审计双双失灵在审计场景里光记录“张三操作了”不够还得知道他是以什么身份、什么权限操作的。我们有一个系统因为Agent节点上没挂角色后来安全部门要求提供“哪些客服以高权限修改过订单金额”的审计记录时临时从操作日志里抓关键字拼接痛苦到怀疑人生。所以设计事件表时最好显式预留两个字段agent_role和agent_org_unit。即使当期不需要审计系统上线时你会感谢当时的自己。6. 常见问题速查表建模时反复出现的坎症状根因处理方式业务方说“改个状态就行”你总觉得会出事把状态演化看成单点更新跳出状态位追问“是什么事件导致了这个变化”资源和事件之间关系混乱说不清谁先谁后没有做“资源增减闭环”检查画出每个资源的流入/流出事件链报表统计数据总是差一点某个资源增减事件漏建模了重新盘点资源生命周期找缺失的减少型/增加型事件测试环境里事件被反复重放后数据错乱了事件缺少幂等约束给事件表加唯一业务ID消费侧用进度表去重复盘上月数据时想不起当时某管理员为什么这么批事件表没保存“当时上下文”payload字段中冗余快照按需补充描述信息接口对接方回调事件顺序乱了跨系统事件顺序天然不可靠不依赖顺序每个事件通过resource_ref幂等ID自行校验这条速查表里最值钱的就是“事件表里的上下文快照”那一行。很多系统事件表只记了操作与时间后来做审计、做数据回放、做“为什么当时是这么个结果”的复盘时全凭猜。往payload里塞一份“事件发生时的业务现场信息”后期价值巨大而且一点也不难实现。7. 关于工具与协作流程的个人体会REA建模这件事最大的阻力往往不是技术而是团队已有的思维惯性。做过传统CRUD开发的同事会非常难以接受“怎么连订单表都不要了”这种思路。我的做法是分三步推进第一步先不和团队讲REA理论只带着大家一起把真实业务中的“事件清单”列出来。这个过程几乎不需要解释业务方会觉得你在认真听他们讲话。第二步把事件清单里涉及的“资源增减”和“代理参与人”画出来。常见工具是白板或者在线绘图重点是让所有人看到“事件”作为核心节点的威力。第三步然后才开始聊落表结构。这时候你会发现之前吵不清楚的边界问题居然都因为事件粒度统一而自然消解了。关于事件粒度是否细化业内有不同的实践倾向。我的建议是以“是否需要独立审计、是否会引起资源增减、是否会被外部系统回调”为三个判定条件。至少满足其中一个才单独建模成一个事件。否则就合并到其他事件里作为属性。这样既能保证事件模型的表达力又不会把模型搞得支离破碎。我实际试过最成功的一次是带着五个刚毕业的同事用半天时间把整个财务报销流程用REA推演了一遍。起初他们连“资源”都觉得抽象后来用“报销单是申请事件、审批是审核事件、打款是转账事件、报销额度是资源”的类比讲了一遍当场就通了。从那之后他们做新需求时自己会先问一句“这里发生了什么事件”而不是直接打开数据库看表结构。REA不是银弹遇到没有清晰资源边界的内容型系统、或者纯粹写操作没有业务事件的场景强行套REA会增加复杂度。但只要你做的是有交易、有库存、有资金流转、有审批闭环的业务系统这套“先找事件再定资源与代理”的建模思路绝对值得放进你的工具箱。