设计模式与服务发现:观察者模式发现服务、策略模式选择服务、工厂模式创建客户端
服务发现,说白了就是让服务之间能找到彼此。在微服务架构里,服务实例的地址是动态变化的——今天这个节点还在,明天可能就挂了。怎么让客户端自动感知这些变化?怎么在多个可用服务里挑一个最合适的?怎么创建出正确的客户端对象?
我这些年做微服务治理,发现这三个问题其实可以用三种经典设计模式优雅地解决。观察者模式负责「发现」,策略模式负责「选择」,工厂模式负责「创建」。把它们组合起来,就是一个完整的服务发现与调用链路。
观察者模式:让客户端感知服务变化
服务注册中心(比如 Consul、Eureka、Nacos)会维护一份服务实例列表。客户端需要实时知道这个列表的变化——新服务上线了、旧服务下线了、某个节点负载过高了。
观察者模式在这里扮演的角色很自然:注册中心是被观察者(Subject),客户端是观察者(Observer)。一旦服务列表有变动,注册中心就通知所有观察者。
我在项目中遇到过一个问题:如果每次变化都通知所有客户端,高并发场景下注册中心会被打爆。后来我们加了一个「合并通知」的机制——短时间内多次变化只触发一次通知。嗯,这里要注意,观察者模式虽然简单,但性能优化不能省。
// 观察者接口
public interface ServiceObserver {
void onServiceChanged(String serviceName, List<ServiceInstance> instances);
}
// 被观察者:服务注册中心
public class ServiceRegistry {
private Map<String, List<ServiceObserver>> observers = new ConcurrentHashMap<>();
private Map<String, List<ServiceInstance>> services = new ConcurrentHashMap<>();
public void registerObserver(String serviceName, ServiceObserver observer) {
observers.computeIfAbsent(serviceName, k -> new CopyOnWriteArrayList<>()).add(observer);
}
public void removeObserver(String serviceName, ServiceObserver observer) {
List<ServiceObserver> list = observers.get(serviceName);
if (list != null) list.remove(observer);
}
public void updateService(String serviceName, List<ServiceInstance> instances) {
services.put(serviceName, instances);
// 通知所有观察者
List<ServiceObserver> list = observers.get(serviceName);
if (list != null) {
for (ServiceObserver obs : list) {
obs.onServiceChanged(serviceName, instances);
}
}
}
}
你看,代码其实很直白。观察者模式的核心就是「订阅-通知」机制。客户端只需要注册一次,后续的变化都由框架自动推送。
策略模式:从多个服务实例中选一个
现在客户端拿到了服务实例列表,比如有 5 个节点都提供「订单服务」。选哪个?
不同的场景需要不同的选择策略:
- 轮询(Round Robin):按顺序轮流,适合所有节点性能相近的场景
- 加权轮询(Weighted Round Robin):给高性能节点分配更多流量
- 最少连接(Least Connections):选当前活跃连接最少的节点
- 一致性哈希(Consistent Hash):保证相同请求落到相同节点,适合有状态服务
策略模式正好解决这个问题——把「选择算法」抽象成接口,每种算法是一个具体策略。客户端可以随时切换策略,甚至可以在运行时动态调整。
// 策略接口
public interface LoadBalanceStrategy {
ServiceInstance select(List<ServiceInstance> instances, String requestKey);
}
// 具体策略:轮询
public class RoundRobinStrategy implements LoadBalanceStrategy {
private AtomicInteger index = new AtomicInteger(0);
@Override
public ServiceInstance select(List<ServiceInstance> instances, String requestKey) {
if (instances.isEmpty()) return null;
int idx = index.getAndIncrement() % instances.size();
return instances.get(idx);
}
}
// 具体策略:一致性哈希
public class ConsistentHashStrategy implements LoadBalanceStrategy {
@Override
public ServiceInstance select(List<ServiceInstance> instances, String requestKey) {
int hash = requestKey.hashCode();
int idx = hash & (instances.size() - 1); // 假设 size 是 2 的幂
return instances.get(idx);
}
}
// 上下文:使用策略的客户端
public class ServiceClient {
private LoadBalanceStrategy strategy;
public ServiceClient(LoadBalanceStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(LoadBalanceStrategy strategy) {
this.strategy = strategy;
}
public ServiceInstance chooseInstance(List<ServiceInstance> instances, String requestKey) {
return strategy.select(instances, requestKey);
}
}
我个人习惯把策略对象做成可配置的——通过配置文件或者注册中心动态下发。这样线上切换策略不需要重启服务,灰度发布也方便。
工厂模式:创建正确的客户端实例
选好了目标服务实例,接下来要创建客户端去调用它。但问题来了——不同的服务协议不同,有的用 HTTP,有的用 gRPC,有的用 Dubbo。客户端对象的创建逻辑很复杂,而且可能随着服务版本升级而变化。
工厂模式就是干这个的。它把「创建客户端」的逻辑封装起来,调用方只需要告诉工厂「我要连哪个服务」,工厂就返回一个可用的客户端对象。
// 产品接口
public interface ServiceClient {
Response call(Request request);
}
// 具体产品:HTTP 客户端
public class HttpClient implements ServiceClient {
private String url;
public HttpClient(String url) { this.url = url; }
@Override
public Response call(Request request) {
// 发送 HTTP 请求
return new Response(200, "OK");
}
}
// 具体产品:gRPC 客户端
public class GrpcClient implements ServiceClient {
private String host;
private int port;
public GrpcClient(String host, int port) { this.host = host; this.port = port; }
@Override
public Response call(Request request) {
// 发送 gRPC 请求
return new Response(200, "OK");
}
}
// 工厂类
public class ServiceClientFactory {
public ServiceClient createClient(ServiceInstance instance) {
String protocol = instance.getProtocol();
switch (protocol) {
case "http":
return new HttpClient(instance.getUrl());
case "grpc":
return new GrpcClient(instance.getHost(), instance.getPort());
case "dubbo":
return new DubboClient(instance.getAddress());
default:
throw new IllegalArgumentException("Unsupported protocol: " + protocol);
}
}
}
你想想看,如果没有工厂模式,每次创建客户端都要写一堆 if-else,而且创建逻辑散落在各处。一旦协议升级或者新增协议,改起来就痛苦了。工厂模式把变化集中到一处,维护成本大大降低。
三种模式如何协同工作
现在我们把三个模式串起来,看看完整的调用流程:
- 观察者模式:客户端订阅服务「订单服务」,注册中心推送最新的实例列表
- 策略模式:客户端用「最少连接策略」从列表里选出一个最优实例
- 工厂模式:根据选中实例的协议(比如 gRPC),创建对应的客户端对象
- 客户端用这个对象发起调用
这个链路里,每个模式各司其职,互不干扰。而且每个模式都可以独立扩展——比如新增一种选择策略,不需要改观察者和工厂的代码。
实际项目中的组合技巧
这三种模式组合起来,能应对大部分服务发现场景。但有几个细节我想提醒你:
| 模式 | 常见坑 | 我的建议 |
|---|---|---|
| 观察者模式 | 通知风暴:服务频繁上下线导致大量通知 | 加一个「去重窗口」,比如 100ms 内的变化合并成一次通知 |
| 策略模式 | 策略切换导致调用中断 | 用「双缓冲」——新策略生效前,旧策略继续服务 |
| 工厂模式 | 客户端创建失败没有降级 | 工厂返回一个「空对象」或「兜底客户端」,保证调用链路不断 |
我曾经在一个金融项目里,把这三个模式封装成一个「服务发现 SDK」。业务方只需要配置服务名和选择策略,剩下的全部由 SDK 自动完成。上线后服务调用的稳定性从 99.9% 提升到了 99.99%。
公众号:蓝海资料掘金营,微信deep3321