设计模式与分布式系统:服务注册中的观察者、负载均衡中的策略、熔断器中的状态

分布式系统,说白了就是一堆服务凑在一起干活。但服务多了,问题就来了——怎么找到对方?怎么分配任务?怎么防止雪崩?

我这些年做分布式系统,发现一个有意思的现象:很多看似复杂的问题,用经典设计模式去套,反而豁然开朗。今天咱们就聊聊三个最典型的场景:服务注册、负载均衡、熔断器。它们分别对应观察者模式、策略模式、状态模式。

一、服务注册与发现:观察者模式

先说说服务注册。你想想看,一个微服务系统里,可能有几十上百个服务实例。它们随时可能启动、停止、扩容、缩容。如果让服务之间硬编码地址,那维护成本就太高了。

观察者模式在这里扮演的角色,其实就是「发布-订阅」的机制。服务注册中心是主题(Subject),各个服务实例是观察者(Observer)。

核心思想:服务实例启动时向注册中心注册,关闭时注销。其他服务通过订阅注册中心的事件,实时感知服务的变化。

我记得有一次做电商系统,订单服务需要调用库存服务。一开始我们用的是轮询方式,每隔几秒去查一次注册中心。结果呢?服务下线了,订单还在往那边发请求,一堆报错。

后来改成观察者模式,注册中心主动推送变更事件。代码大概长这样:

// 注册中心 - 主题
public class ServiceRegistry {
    private Map<String, List<ServiceObserver>> observers = 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 notifyObservers(String serviceName, ServiceEvent event) {
        List<ServiceObserver> list = observers.get(serviceName);
        if (list != null) {
            for (ServiceObserver observer : list) {
                observer.onServiceChanged(serviceName, event);
            }
        }
    }
    
    // 服务实例上下线时调用
    public void instanceOnline(String serviceName, ServiceInstance instance) {
        // 保存实例信息...
        notifyObservers(serviceName, new ServiceEvent(EventType.ONLINE, instance));
    }
}

// 观察者接口
public interface ServiceObserver {
    void onServiceChanged(String serviceName, ServiceEvent event);
}

// 调用方 - 具体观察者
public class OrderService implements ServiceObserver {
    private List<ServiceInstance> inventoryInstances = new ArrayList<>();
    
    @Override
    public void onServiceChanged(String serviceName, ServiceEvent event) {
        if ("inventory-service".equals(serviceName)) {
            if (event.getType() == EventType.ONLINE) {
                inventoryInstances.add(event.getInstance());
                System.out.println("库存服务上线: " + event.getInstance().getAddress());
            } else if (event.getType() == EventType.OFFLINE) {
                inventoryInstances.remove(event.getInstance());
                System.out.println("库存服务下线: " + event.getInstance().getAddress());
            }
        }
    }
}
避坑指南:我曾经在观察者回调里做了耗时操作,结果注册中心线程被阻塞,导致其他服务收不到通知。记住,观察者回调一定要异步处理,或者用消息队列解耦。

二、负载均衡:策略模式

服务找到了,接下来就是怎么分配请求。负载均衡算法五花八门:轮询、随机、加权、最少连接、一致性哈希……

策略模式在这里简直是天作之合。每种算法就是一个策略,客户端根据场景自由切换。我习惯把策略定义成接口,然后每种算法一个实现类。

// 策略接口
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) {
        int idx = Math.abs(index.getAndIncrement() % instances.size());
        return instances.get(idx);
    }
}

// 一致性哈希策略
public class ConsistentHashStrategy implements LoadBalanceStrategy {
    private static final int VIRTUAL_NODES = 160;
    
    @Override
    public ServiceInstance select(List<ServiceInstance> instances, String requestKey) {
        TreeMap<Integer, ServiceInstance> ring = new TreeMap<>();
        for (ServiceInstance instance : instances) {
            for (int i = 0; i < VIRTUAL_NODES; i++) {
                int hash = (instance.getId() + "#" + i).hashCode();
                ring.put(hash, instance);
            }
        }
        int hash = requestKey.hashCode();
        SortedMap<Integer, ServiceInstance> tailMap = ring.tailMap(hash);
        Integer key = tailMap.isEmpty() ? ring.firstKey() : tailMap.firstKey();
        return ring.get(key);
    }
}

// 上下文 - 客户端
public class LoadBalancer {
    private LoadBalanceStrategy strategy;
    
    public LoadBalancer(LoadBalanceStrategy strategy) {
        this.strategy = strategy;
    }
    
    public void setStrategy(LoadBalanceStrategy strategy) {
        this.strategy = strategy;
    }
    
    public ServiceInstance select(List<ServiceInstance> instances, String requestKey) {
        if (instances == null || instances.isEmpty()) {
            throw new RuntimeException("没有可用的服务实例");
        }
        return strategy.select(instances, requestKey);
    }
}

实际项目中,我一般会根据业务场景动态切换策略。比如普通请求用轮询,带用户ID的请求用一致性哈希保证会话粘性,高并发时用最少连接策略。

注意:策略模式虽然灵活,但不要滥用。如果只有一两种算法,硬编码反而更清晰。我见过有人为了「扩展性」搞了十几个策略类,结果实际只用了一个,这就是过度设计。

三、熔断器:状态模式

熔断器这东西,说白了就是防止雪崩的保险丝。当某个服务频繁超时或报错,熔断器就打开,直接返回降级结果,不再发起真实调用。

熔断器有三个状态:关闭、打开、半开。状态之间的转换逻辑,用状态模式来管理再合适不过了。

// 状态接口
public interface CircuitBreakerState {
    boolean allowRequest();
    void onSuccess();
    void onFailure();
    CircuitBreakerState nextState();
}

// 关闭状态
public class ClosedState implements CircuitBreakerState {
    private int failureCount = 0;
    private int threshold;
    private long lastFailureTime;
    
    public ClosedState(int threshold) {
        this.threshold = threshold;
    }
    
    @Override
    public boolean allowRequest() {
        return true; // 关闭状态允许所有请求
    }
    
    @Override
    public void onSuccess() {
        failureCount = 0; // 成功一次就重置失败计数
    }
    
    @Override
    public void onFailure() {
        failureCount++;
        lastFailureTime = System.currentTimeMillis();
        if (failureCount >= threshold) {
            System.out.println("失败次数达到阈值,熔断器打开");
        }
    }
    
    @Override
    public CircuitBreakerState nextState() {
        if (failureCount >= threshold) {
            return new OpenState(lastFailureTime);
        }
        return this;
    }
}

// 打开状态
public class OpenState implements CircuitBreakerState {
    private long openTime;
    private static final long TIMEOUT = 5000; // 5秒后尝试半开
    
    public OpenState(long openTime) {
        this.openTime = openTime;
    }
    
    @Override
    public boolean allowRequest() {
        if (System.currentTimeMillis() - openTime > TIMEOUT) {
            System.out.println("熔断超时,尝试半开");
            return true; // 放行一个请求试探
        }
        return false; // 直接拒绝
    }
    
    @Override
    public void onSuccess() {
        // 半开状态下成功,会切换到关闭状态
    }
    
    @Override
    public void onFailure() {
        // 半开状态下失败,回到打开状态
    }
    
    @Override
    public CircuitBreakerState nextState() {
        // 这里由上下文根据请求结果决定
        return this;
    }
}

// 熔断器上下文
public class CircuitBreaker {
    private CircuitBreakerState state;
    
    public CircuitBreaker(int threshold) {
        this.state = new ClosedState(threshold);
    }
    
    public boolean allowRequest() {
        return state.allowRequest();
    }
    
    public void recordSuccess() {
        state.onSuccess();
        if (state instanceof OpenState) {
            state = new ClosedState(5); // 半开成功,回到关闭
        }
    }
    
    public void recordFailure() {
        state.onFailure();
        state = state.nextState();
    }
}
关键点:状态模式把每个状态的行为封装成独立的类,状态转换逻辑清晰。你想想看,如果用一堆 if-else 来管理状态,代码会变成什么样?维护起来绝对头疼。

我曾经在一个支付系统里用过这个模式。当时第三方支付接口经常超时,如果不加熔断,整个订单服务都会被拖垮。用了状态模式后,每个状态的行为一目了然,后来加了个「手动半开」的功能,扩展起来也很方便。

四、三者如何协同工作

这三个模式在分布式系统里不是孤立的。我画了一张图,帮你理清它们的关系:

分布式系统三大设计模式协同 服务注册中心 观察者模式 服务A(生产者) 注册/注销 服务B(消费者) 订阅/通知 负载均衡器 策略模式 可选策略 轮询 随机 加权 一致性哈希 熔断器 状态模式 状态转换 关闭 → 打开 打开 → 半开 半开 → 关闭 半开 → 打开 服务注册提供实例列表 → 负载均衡选择实例 → 熔断器保护调用链路

你看,服务注册中心用观察者模式把实例列表推送给负载均衡器,负载均衡器用策略模式选出一个实例,最后熔断器用状态模式保护调用过程。三个模式各司其职,配合得天衣无缝。

我的建议:设计分布式系统时,不要一上来就想着用什么中间件。先想想业务场景适合哪种设计模式。很多时候,一个简单的观察者模式就能解决服务发现的问题,根本不需要引入 ZooKeeper 或 Consul。

好了,今天就聊到这儿。这三个模式在分布式系统里的应用,说白了就是「怎么找到服务、怎么分配请求、怎么保护系统」。你想想看,是不是这个理?


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