3.3 微服务架构与 PHP:多站协同 本节摘要:微服务把一座大站拆成多座按业务划分的中小站:订票、支付、通知各自独立部署、独立扩容、独立选型。拆分不是免费的——网络会失败、事务会跨库、运维成本翻倍。本节讲单站到顶的信号、按业务能力划界的方法、服务间通信与数据一致性的最小方案,最后给出"要不要拆"的三个决策维度。先说结论:模块化单体是默认起点,微服务是特定条件下的升级而非潮流跟风。 单站到顶的信号 单体(所有业务代码一个项目)没有原罪,它在多数项目的整个生命周期里都是正确答案。真正到顶的信号有三类。其一,部署互锁:通知模块改一行文案,整个站要全量回归、深夜发版——发布风险与频率开始互相拖累。
本节摘要:微服务把一座大站拆成多座按业务划分的中小站:订票、支付、通知各自独立部署、独立扩容、独立选型。拆分不是免费的——网络会失败、事务会跨库、运维成本翻倍。本节讲单站到顶的信号、按业务能力划界的方法、服务间通信与数据一致性的最小方案,最后给出"要不要拆"的三个决策维度。先说结论:模块化单体是默认起点,微服务是特定条件下的升级而非潮流跟风。
单体(所有业务代码一个项目)没有原罪,它在多数项目的整个生命周期里都是正确答案。真正到顶的信号有三类。其一,部署互锁:通知模块改一行文案,整个站要全量回归、深夜发版——发布风险与频率开始互相拖累。其二,扩容错位:报表导出吃内存、下单接口吃连接数,明明只有报表告急,却只能整站复制扩容,资源花在刀背上。其三,团队互堵:多个小组改同一个代码库,合并冲突与"我改的代码弄坏了你的功能"成为日常。
三条信号的本质是同一条:业务模块之间的耦合超过了组织与基础设施的承受力。注意信号与"代码感觉乱"的区别——后者先靠 2.1 的编制纪律与 2.10 的模式治理,治理不好就拆微服务,等于把三处混乱升级为三处独立混乱加网络成本。
划界的原则是"按业务能力,不按技术分层"。错误示范是拆出"接口服务、逻辑服务、数据服务"——那是把分层抄进服务拓扑,每次下单要跨三个服务跑一圈。正确做法按业务闭环切:订票站(班次查询、下单锁座)、支付站(收付款、对账)、通知站(短信、邮件、推送)。每个服务内部依然是第三章的完整框架应用(自己的控制器、服务、数据库),对外只暴露接口。拓扑如下:

每个站内部就是 3.2 节的完整框架应用,对外的接口遵守 2.3 节的 JSON 规范。注意"自己的库"这条铁律:两个服务共读一张表,等于拆了形式没拆耦合——数据边界与业务边界必须一致。
服务间通信分同步与异步两条道。同步(HTTP 接口)适合"必须现在知道结果"的调用,代价是调用方要面对超时与失败;异步(消息队列)适合"事情发生了,相关方各自跟进"的场景,发送方不等接收方。订票站调支付站的示例:
<?php // 订票站侧:调支付站的预下单接口(同步道) $client = new CurlHttpClient(); $response = $client->request('POST', 'http://pay-service.internal/prepay', [ 'json' => ['order_id' => 1001, 'amount' => 240.0], 'timeout' => 3, // 超时必须显式定 ]); if ($response->getStatusCode() !== 200) { // 网络会失败:降级路径在此——订单挂起,稍后重试或提示旅客 queueRetry(['order_id' => 1001]); }
超时、重试、降级三件套是同步调用的固定配置,缺一件都是隐患。跨服务的数据一致性则要换思路:没有跨库事务,就设计成"最终一致"。典型链路:支付站完成扣款后向队列广播"订单已付"事件;订票站消费事件把订单置为已支付;通知站消费同一事件发短信。任何一步失败,靠消息重投与补偿任务兜底——每一步都可能出现短暂的不一致(款扣了、订单还挂着),业务上要接受这个窗口期并给出旅客可见的状态("处理中")。
给一张诚实的决策表,而不是口号:
| 维度 | 适合留在单体 | 拆微服务的底气 |
|---|---|---|
| 团队规模 | 几人小队同库开发顺畅 | 按业务分组、能独立负责各站 |
| 流量形态 | 整体流量均匀 | 各模块负载差异悬殊需独立扩容 |
| 运维能力 | 无容器编排与监控体系 | 已有部署流水线、链路追踪与值班机制 |
三列里有一列到达右栏条件,且单站信号确凿,才认真考虑拆分。还有一个务实的中间态值得推荐:模块化单体——代码库仍是一个项目,但按业务划出模块边界(各自的控制器、服务、模型目录,禁止跨模块直查表),未来拆分时每个模块平移成独立服务即可。它把"拆"的成本提前变成了"守边界"的纪律,是多数团队性价比最高的一步。
抽象原则不如推演实例。拿"旅客完成支付后收到短信"这条链路,分别在单体与站网两种形态下走一遍,代价与收益都会具体起来。单体形态:支付动作完成后,同进程内顺序调用通知模块——一个数据库事务边界可以罩住核心步骤,失败就地回滚,排查日志只开一个项目。站网形态:支付站扣款成功后向队列广播"订单已付"事件,通知站消费事件后调用短信通道——扣款与短信之间隔着网络与队列,短信延迟、丢失、重复送达都成为可能。
走完就能读懂两条工程纪律的由来。其一,核心与边缘分开对待:扣款属于核心步骤(必须成功、必须一致),短信属于边缘步骤(允许延迟、允许重试),把边缘步骤从事务边界里挪出去,正是拆分时保证核心不变慢的前提。其二,每条跨站链路都要设计失败剧本:事件丢失靠消息重投兜底,重复消费靠消费方的幂等设计(同一次支付只发一条短信)兜底。幂等不玄,落到实现就是"处理前先查处理标记":
<?php // 通知站消费事件:幂等处理,重复投递只生效一次 function onOrderPaid(array $event): void { $markDb = openMarkConnection(); // 标记存储(表或缓存均可) $key = 'sms_sent_' . $event['order_id']; if (!acquireMark($markDb, $key)) { return; // 已处理过:静默丢弃本次重复投递 } try { sendSms($event['phone'], "您的订单 {$event['order_id']} 已支付成功"); } catch (Throwable $e) { releaseMark($markDb, $key); // 发送失败释放标记,等待下次重投 logError('notify', $e->getMessage()); } }
十来行代码,把"至少一次投递的消息语义"矫正成"业务上的恰好一次效果"——这是站网协作中最常用的小构件,值得抄进你的工具库。推演到这里你也能体会到 3.3 开头结论的分寸:拆分引入的不是复杂度总量增加,而是复杂度搬家——从代码内部搬到了服务之间,而服务间的复杂度需要更重的工具与纪律来管理。这笔账算清,"要不要拆"就不再是信仰问题。
⚠️ 为了简历拆微服务:运维体系没跟上就拆,故障定位从"翻一个项目的日志"变成"串六个站的日志"。
⚠️ 分布式单体的假拆:服务独立了,数据库还共连一台库互相查表——耦合原样保留,网络成本倒是新增了。
下一章抬眼站网之外:语言与生态正在往哪里演化,你的经验该往哪个方向积累。