设计模式与服务发现:观察者模式发现服务、策略模式选择服务、工厂模式创建客户端

服务发现,说白了就是让服务之间能找到彼此。在微服务架构里,服务实例的地址是动态变化的——今天这个节点还在,明天可能就挂了。怎么让客户端自动感知这些变化?怎么在多个可用服务里挑一个最合适的?怎么创建出正确的客户端对象?

我这些年做微服务治理,发现这三个问题其实可以用三种经典设计模式优雅地解决。观察者模式负责「发现」,策略模式负责「选择」,工厂模式负责「创建」。把它们组合起来,就是一个完整的服务发现与调用链路。

核心思路:观察者模式监听注册中心的变化 → 策略模式从可用服务列表中选出目标 → 工厂模式根据选中的服务信息创建客户端实例。

观察者模式:让客户端感知服务变化

服务注册中心(比如 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 这种长连接客户端,创建成本很高。我曾经见过一个项目每调用一次就新建一个 gRPC 客户端,结果连接数飙到几万,把服务器搞挂了。

三种模式如何协同工作

现在我们把三个模式串起来,看看完整的调用流程:

  1. 观察者模式:客户端订阅服务「订单服务」,注册中心推送最新的实例列表
  2. 策略模式:客户端用「最少连接策略」从列表里选出一个最优实例
  3. 工厂模式:根据选中实例的协议(比如 gRPC),创建对应的客户端对象
  4. 客户端用这个对象发起调用

这个链路里,每个模式各司其职,互不干扰。而且每个模式都可以独立扩展——比如新增一种选择策略,不需要改观察者和工厂的代码。

服务发现与调用完整流程 服务注册中心 观察者模式 订阅服务变化 策略模式 选择最优实例 工厂模式 创建客户端对象 推送实例列表 获取可用实例 传递列表 选中实例 客户端发起远程调用 返回客户端对象 观察者模式 策略模式 工厂模式 最终调用

实际项目中的组合技巧

这三种模式组合起来,能应对大部分服务发现场景。但有几个细节我想提醒你:

模式 常见坑 我的建议
观察者模式 通知风暴:服务频繁上下线导致大量通知 加一个「去重窗口」,比如 100ms 内的变化合并成一次通知
策略模式 策略切换导致调用中断 用「双缓冲」——新策略生效前,旧策略继续服务
工厂模式 客户端创建失败没有降级 工厂返回一个「空对象」或「兜底客户端」,保证调用链路不断

我曾经在一个金融项目里,把这三个模式封装成一个「服务发现 SDK」。业务方只需要配置服务名和选择策略,剩下的全部由 SDK 自动完成。上线后服务调用的稳定性从 99.9% 提升到了 99.99%。

总结一下:观察者模式解决「知道谁活着」,策略模式解决「选谁干活」,工厂模式解决「怎么干活」。三个模式各管一摊,组合起来就是一个健壮的服务发现框架。你写代码的时候,也可以试着把这种「职责分离」的思路用到自己的项目里。

公众号:蓝海资料掘金营,微信deep3321