微服务架构的初衷是拆分与解耦,但拆着拆着,很多团队会发现服务之间的调用关系越来越难画:订单服务要调库存、要调支付、要通知物流,库存变更了又要反向通知订单和营销。这种组件之间两两互调的网状依赖,就是典型的复杂度失控信号。中介者模式(Mediator Pattern)提供了一种治理思路:引入一个专职的协调者对象,把所有跨服务的交互逻辑集中到它身上,各服务只与中介者对话,彼此不再直接感知。这篇文章就来聊聊这个模式在分布式系统中的具体玩法、落地代码和需要注意的坑。

为什么微服务之间会越调越乱
假设一个电商系统有五个服务:订单、库存、支付、物流、通知。如果它们之间直接通过REST或gRPC调用,最坏情况下调用关系数量接近 n(n-1)/2,五个服务就是十条依赖边。更要命的是,这些依赖不是静态的:订单创建成功后要扣库存、发起支付,支付成功后要通知物流发货、给用户发通知,物流签收后又要回写订单状态。任何一条链路都是一条隐式业务流程,散落在各个服务的代码里。
这种网状结构带来三类具体问题。第一是耦合扩散,库存服务的接口一旦调整,所有调用方都要跟着改,回归测试范围无法收敛。第二是流程不可见,想搞清楚一个订单从创建到签收经历了哪些环节,需要翻遍五六个代码仓库。第三是故障传播难以控制,下游某个服务超时,上游服务往往因为同步调用链过长而被一起拖垮。
中介者模式的核心思想正好对症下药:定义一个中介者接口,各同事对象(Colleague)只依赖这个接口,把原本对象之间的直接交互改为通过中介者转发和协调。在单机面向对象设计里,中介者是一个普通类;而在分布式系统里,这个角色通常由一个独立部署的编排服务、一个消息中枢或者一段集中的流程引擎代码来承担。无论形态如何,本质都是把多对多的交互收敛成多对一。
中介者模式的核心结构与分布式映射
经典的GoF中介者模式包含四个角色:抽象中介者接口(定义同事之间通信的契约)、具体中介者(持有所有同事的引用并实现协调逻辑)、抽象同事类、具体同事类。同事之间不互相持有引用,一切消息都通过中介者路由。我们先用一段简洁的Java代码回顾这个骨架:
// 抽象中介者
public interface Mediator {
void notify(Object sender, String event);
}
// 抽象同事
public abstract class Colleague {
protected Mediator mediator;
public Colleague(Mediator mediator) {
this.mediator = mediator;
}
}
// 具体中介者:集中协调所有交互
public class OrderMediator implements Mediator {
private InventoryService inventory;
private PaymentService payment;
private LogisticsService logistics;
@Override
public void notify(Object sender, String event) {
if ("ORDER_CREATED".equals(event)) {
inventory.deduct();
} else if ("PAYMENT_SUCCESS".equals(event)) {
logistics.ship();
}
}
}映射到分布式环境,抽象中介者对应一个独立部署的编排服务(或称流程协调服务),具体同事就是各个微服务,同事与中介者之间的通信不再是进程内方法调用,而是HTTP、gRPC或消息队列。中介者内部维护的不是对象引用,而是服务注册中心的地址信息以及当前流程实例的状态。
需要澄清一个容易混淆的点:API网关、消息队列和服务网格都带有一定的中介色彩,但它们并不等价于中介者模式。API网关只做南北向流量的入口聚合与鉴权,不承载业务流程;消息队列实现了服务间的解耦,但流程逻辑仍然分散在各个消费者的代码里,谁订阅了什么事件只有翻代码才知道;服务网格解决的是通信层面的治理,与业务编排无关。只有当某个组件开始集中描述和管理业务流程状态时,它才真正扮演了中介者角色。
实战:用编排服务实现订单履约的中介者
下面通过一个订单履约场景演示完整落地。业务流程是:创建订单、锁定库存、发起支付、支付成功后安排物流、任意环节失败则执行补偿。我们用一个基于事件驱动的中介者服务来承载整个流程,用Go语言实现核心骨架:
// 中介者内部维护的流程状态
type OrderFlow struct {
OrderID string
Status string // CREATED / INVENTORY_LOCKED / PAID / SHIPPED / COMPENSATED
StepResults map[string]error
}
// 中介者的处理入口:所有服务的事件都汇报到这里
func (m *MediatorServer) HandleEvent(ctx context.Context, ev DomainEvent) {
switch ev.Type {
case "ORDER_CREATED":
// 由中介者主动调用库存服务,而不是订单服务直接调
err := m.inventoryClient.Lock(ev.OrderID)
m.persistStep(ev.OrderID, "inventory_lock", err)
if err != nil {
m.compensate(ctx, ev.OrderID)
return
}
m.paymentClient.CreatePayment(ev.OrderID)
case "PAYMENT_CALLBACK":
if ev.Success {
m.logisticsClient.ScheduleShipping(ev.OrderID)
} else {
m.compensate(ctx, ev.OrderID)
}
case "SHIPPING_SCHEDULED":
m.orderClient.UpdateStatus(ev.OrderID, "SHIPPED")
}
}
// 补偿逻辑也集中在中介者,避免散落各处
func (m *MediatorServer) compensate(ctx context.Context, orderID string) {
m.inventoryClient.Unlock(orderID)
m.orderClient.UpdateStatus(orderID, "CANCELLED")
}这段代码体现了中介者模式的几个关键价值。首先是流程集中可见,整个订单履约的状态机完整地写在一个地方,新人入职看一个仓库就能理解业务全貌。其次是变更影响可控,库存服务从同步扣减改成异步预占,只需要改中介者里的调用方式,订单服务一行代码都不用动。第三是补偿逻辑有了统一归宿,以前取消订单要释放库存、退款、撤回通知,这些操作分散在多个服务的代码里,现在统一由中介者的compensate方法编排。
落地时有三个工程细节必须处理好。第一是幂等,中介者可能会重复收到同一个支付回调,每个步骤执行前要检查流程状态是否允许推进,可以用数据库乐观锁或者引入幂等表。第二是持久化,流程状态必须落库,中介者实例崩溃后要能从断点恢复,否则就会出现库存锁了但支付没人发起的悬挂状态。第三是超时兜底,对长时间停留在中间状态的流程实例,要有定时任务主动巡检并触发补偿或重试。
代价与权衡:中介者不是免费的午餐
任何架构决策都有代价,中介者模式最直接的问题是它可能成为新的单点和性能瓶颈。所有跨服务交互都要经过它,它的可用性直接决定整个流程的可用性;它的吞吐上限也约束了业务峰值处理能力。缓解手段包括:中介者本身做成无状态多实例部署,状态放在数据库或Redis中;调用链路上对读多写少的操作允许服务间直连,只把真正需要编排的写操作交给中介者;引入异步事件削峰,中介者只消费事件流而不承受同步调用的并发压力。
第二个代价是中介者内部逻辑可能演化成上帝对象。如果无节制地把各种判断、补偿、重试都塞进一个类,最终会得到一个几千行、无人敢改的流程怪物。实践上要按业务域拆分中介者,订单域一个、售后域一个,每个中介者内部再用状态机或规则引擎组织代码,而不是一长串if-else。第三个代价是调试链路变长,一次业务操作要跨多个服务和中介者,排查问题必须依赖分布式链路追踪,建议在事件中强制携带trace ID并让中介者输出完整的流程日志。
最后一个判断标准:不要为了用模式而用模式。如果系统只有三四个服务、调用关系简单稳定,直接调用加上良好的接口设计可能更务实。当服务数量超过七八个、调用关系开始成环、业务流程跨三个以上服务、且流程本身频繁变更时,引入中介者模式的收益才会明显超过它带来的复杂度。架构决策的依据永远是问题本身,而不是模式的流行程度。
总结
中介者模式在分布式系统中的本质,是把散落在各个微服务里的隐式业务流程,收敛为一个显式、集中、可观测的编排层。它用多对一的结构替换多对多的网状调用,换来的是流程可视、变更可控和补偿有据。落地时重点做好幂等、状态持久化和超时兜底三件事,同时警惕单点瓶颈和上帝对象的反模式。如果你的系统正在被服务间混乱的调用关系折磨,不妨从最痛的那条业务链路开始,先给它建一个轻量的中介者试试水。