基于事件驱动架构重构遗留 PHP 系统的实践记录

阿乐
阿乐 管理员 年卡会员
发布于 2026-09-04 15:51 ·1 浏览 ·0 回复

好的,这就按标题写一篇偏实践向的技术分享文章。


项目背景:那个“动哪儿哪儿炸”的老系统

我接手的是一个运行了九年的老 PHP 系统。它承载着核心的订单和库存业务,代码规模庞大,一个方法动辄几百行,Controller 里直接写 SQL、调用第三方 API、处理文件上传,各种副作用像毛线团一样纠缠在一起。

最痛的是每次业务迭代,改动一个底层函数,都要排查全链路的影响面。团队里有个不成文的规矩:改代码前先开“风险评估会”,重点不是怎么做,而是“别把别的模块搞坏”。

这样的系统,继续往上堆功能,迟早会变成一座无法逾越的巴别塔。我们决定在不大规模重写的前提下,引入事件驱动架构(Event-Driven Architecture, EDA),试图把这团乱麻一点点捋顺。

什么是我们想要的“事件驱动”

我们定义的事件,不是简单的 `onClick`,而是业务事实(Business Fact)。比如“订单已支付”“库存已锁定”,这是一种已经发生且不可更改的事实。事件驱动架构的核心,不只在“发消息”,而是把**业务逻辑的副作用(发短信、写日志、推送、其他模块的依赖)从请求的主流程中解耦出去**。

这让主链路只关心核心业务规则,而非核心的衍生动作,则交给异步的事件处理器去关注。

第一步:让系统“会说话”——构建统一的事件基础设施

这是整个重构的起点。在 PHP 生态里,没有 Java 那种重量级的框架约束,这既是自由也是混乱的源头。我们没有采用某个重量级 PHP 框架的事件系统,而是做了两件事:

首先,定义统一的事件消息结构。我们基于组合模式,用统一的事件信封(Envelope)包裹事件数据,包含唯一的 `event_id`、发生时间、事件类型实体(Payload)和链路追踪 ID。这很重要,因为它是审计和排查问题的凭据。

其次,分层处理事件通道。对于 进程内同步 的事件(例如“订单创建成功需要初始化购物车”),我们使用轻量级的消息分发器,快速且不产生异步问题。对于 跨服务异步 事件(例如“订单完成触发 ERP 出库”),我们引入 RabbitMQ 作为消息代理,保证可靠投递,不至于丢失消息。

第二步:在老旧代码边界上寻找突破口

我们不可能把所有的模型瞬间改造成纯事件溯源(Event Sourcing)。那太理想化,而且风险极高。我们的策略是找切入点

我们从业务痛点最重的“订单创建”链路入手。

改造前,`createOrder()` 这个函数里大概做了十件事:插入订单表、扣减库存、给客服发企业微信通知、更新用户积分、调用物流接口生成预下单... 全部是 `try-catch` 包裹的同步操作。

改造后,主流程被精简为三个步骤:
1. 校验商品和价格;
2. 以数据库事务保存订单主记录;
3. 发布 `OrderCreated` 事件。

紧接着,在我们的事件总线上,挂了几个监听器(Listener/Consumer):
- `InventoryDeductHandler` 监听事件,去执行库存扣减。
- `UserPointHandler` 监听事件,执行积分累加。
- `NotifyHandler` 监听事件,构建通知文案发送工单系统。

这里有一个容易踩坑的细节:失去本地事务的保护后,如何保证数据最终一致性? 我们采用了“本地消息表”模式,将发布事件这个动作,与业务数据放在同一个数据库事务中。只有在主表写入成功,事件表插入成功,事务才能提交。后台有一个守护进程专门扫描事件表,将未确认投递的消息补发到 MQ。

第三步:拆解“上帝类”,重构数据库访问逻辑

遗留 PHP 系统最糟糕的不仅是控制器臃肿,还有数据库查询语句散落在视图中。引入 EDA 后,我们强制规定:事件的处理器内部,不允许直接操作不属于该事件聚合根的表

这个强约束逼着我们重新设计了数据库访问层。原来一个订单服务里可以顺手查用户表,现在它的处理器只能操作订单相关的仓储。如果需要用户信息来校验,只能通过已有的事件快照(Payload)传递,或者显式调用用户服务提供的查询接口。这虽然繁琐,但让模块间的依赖边界重新变得清晰。

踩过的坑与反思

重构并非一帆风顺,我们踩了几个比较典型的坑:

1. 事件的粒度问题。
早期我们喜欢定义非常细碎的事件,比如“订单状态属性 A 变更了”“属性 B 变更了”。结果导致监听器逻辑复杂,且难以追溯。后来我们调整了事件粒度——按业务动作定义,不是按字段变更定义。比如从“状态变为已转单”调整为 “OrderShipped”,这让事件语义更贴合真实业务。

2. 消息顺序问题。
股票/库存这类场景对顺序极敏感。如果库存回补和锁定两个消息顺序错乱,可能导致超卖。我们最初用 RabbitMQ 的简单队列,发现乱序问题后,通过引入分区键(Routing Key),将所有相同订单 ID 的消息路由到同一个队列,保证了单个订单维度的有序性。

3. 循环依赖造成的递归爆炸。
一次我们把“用户注销”和“订单归档”事件互相监听,导致死循环。这个教训让我们引入了事件溯源追踪标记,在监听器入口增加事件传播深度限制,防止 A 事件触发 B,B 又反向触发 A 的死锁。

效果与总结

目前我们的系统已经完成了 3 个核心业务模块的 EDA 化改造。最直观的变化是,新增一个衍生业务功能(比如“订单完成后自动给用户发一张优惠券”),代码开发量从原来的修改 3 个以上类、介入主流程 SQL,变成了新增一个独立的事件处理器,甚至不需要重启原有服务进程。

这次实践并没有摧毁旧系统,而是像在围城之外建立新的秩序。对于遗留 PHP 系统,最忌讳的就是好高骛远的大版本重构。用小步快跑的方式画出边界,用事件流把混乱的业务流变成单向的河流,这或许才是让老系统重获新生的务实路径。

他们都看过 1 人浏览过
断了的弦

全部回复 0

还没有回复,来抢沙发~