项目3:从0开始的RPC框架(扩展版)-3

七. 负载均衡

1. 需求分析

目前我们的RPC框架仅允许消费者读取第一个服务提供者的服务节点,但在实际应用中,同一个服务会有多个服务提供者上传节点信息。如果消费者只读取第一个,势必会增大单个节点的压力,并且也浪费了其它节点资源。

//获取第一个服务节点
ServiceMetaInfo selectedServiceMetaInfo = serviceMetaInfoList.get(0);

因此,RPC框架应该允许对服务节点进行选择,而非调用固定的一个。采用负载均衡能够有效解决这个问题。

2. 设计方案

(1)什么是负载均衡

负载,可以理解为要处理的工作和压力,比如网络请求、事务、数据处理任务等。

均衡,就是将工作和压力平均地分配给多个工作者,从而分摊每个工作者的压力。

即将工作负载均匀分配到多个计算资源(如服务器、网络链路、硬盘驱动器等)的技术,以优化资源使用、最大化吞吐量、最小化响应时间和避免过载。

在RPC框架中,负载均衡的作用是从一组可用的服务节点中选择一个进行调用。

(2)常见的负载均衡算法

所谓负载均衡算法,就是按照何种策略选择资源。不同的算法有不同的适用场景,要根据实际情况选择。

  • 轮询(Round Robin):按照循环的顺序将请求分配给每个服务器,适用于各服务器性能相近的情况。
//5台服务器节点,请求调用顺序:
1,2,3,4,5,1,2,3,4,5
  • 随机(Random):随机选择一个服务器处理请求,适用于各服务器性能相近且当前负载均匀的情况。
//5台服务器节点,请求调用顺序:
2,4,1,3,2,5,1,4,4,1
  • 加权轮询(Weighted Round Robin):根据服务器的性能或权重来分配请求,权重更高的服务器会获得更多的请求,适用于服务器性能不均匀的情况。
//假设有1台千兆带宽的服务器节点和4台百兆带宽的服务器节点,请求调用顺序:
1,1,1,2, 1,1,1,3, 1,1,1,4, 1,1,1,5
  • 加权随机(Weighted Random):根据服务器的权重随机选择一个处理请求,适用于服务器性能不均匀的情况。
//假设有2台千兆带宽,3台百兆带宽的服务器节点,请求调用顺序:
1,2,2,1,3, 1,1,1,2,4, 2,2,2,1,5
  • 最小连接数(Least Connections):选择当前连接数最少的服务器处理请求,适用于长连接场景。
  • IP Hash:根据客户端IP地址的哈希值选择服务器处理请求,确保同一客户端的请求始终被分配到同一台服务器上,适用于需要保持会话一致性的场景。

另一种比较经典的哈希算法,

一致性哈希(Consistent Hashing):将整个哈希值空间划分成一个环状结构,每个节点或服务器在环上占据一个位置,每个请求根据其哈希值映射到环上的一个点,然后顺时针寻找第一个遇见的节点,将请求路由到该节点上。

一致性哈希结构图如下图所示,并且请求A会在服务器C进行处理。

此外,一致性哈希还能有效解决节点下线倾斜问题

  • 节点下线:某个节点下线后,其负载会被平均分摊到其它节点上,不会影响到整个系统的稳定性,只会产生局部变动。

当服务器C下线后,请求A会交由服务器A处理。服务器B接收到的请求不变。

  • 倾斜问题:如果服务节点在哈希环上分布不均匀,可能会导致大部分请求全都集中在某一台服务器上,造成数据分布不均匀。通过引入虚拟节点,对每个服务节点计算多个哈希,每个计算结果位置都放置该服务节点,即一个实际物理节点对应多个虚拟节点,使得请求能够被均匀分布,减少节点间的负载差异。

如上图所示节点分布情况,大部分请求都会落在服务器C中,服务器A中的请求会很少。

引入虚拟节点后,每个服务器会有多个节点,使得请求分布更加均匀。

3. 具体实现

(1)实现三种负载均衡器

根据轮询、随机和一致性哈希三种负载均衡算法实现对应的负载均衡器。

创建loadbalancer包,将所有负载均衡器相关的代码都放在该包下。

创建LoadBalancer负载均衡器通用接口。

提供一个选择服务方法,接收请求参数和可用服务列表,可以根据这些信息进行选择:

package com.khr.krpc.loadbalancer;

import com.khr.krpc.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;

/**
 * 负载均衡器(消费端使用)
 */
public interface LoadBalancer {

    /**
     * 选择服务调用
     *
     * @param requestParams 请求参数
     * @param serviceMetaInfoList 可用服务列表
     * @return
     */
    ServiceMetaInfo select(Map<String,Object> requestParams,List<ServiceMetaInfo>serviceMetaInfoList);
}

实现轮询负载均衡器。

使用到了JUC包的AtomicInteger实现原子计数器,防止并发冲突。

package com.khr.krpc.loadbalancer;

import com.khr.krpc.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 轮询负载均衡器
 */

public class RoundRobinLoadBalancer implements LoadBalancer{

    /**
     * 当前轮询的下标
     */
    private final AtomicInteger currentIndex = new AtomicInteger(0);

    @Override
    public ServiceMetaInfo select(Map<String,Object> requestParams,List<ServiceMetaInfo> serviceMetaInfoList){
        if (serviceMetaInfoList.isEmpty()){
            return null;
        }
        //只有一个服务,无需轮询
        int size = serviceMetaInfoList.size();
        if (size == 1){
            return serviceMetaInfoList.get(0);
        }
        //取模算法轮询
        int index = currentIndex.getAndIncrement() % size;
        return  serviceMetaInfoList.get(index);
    }
}

currentIndex是一个AtomixInteger,它是一个线程安全的整数变量。getAndIncrement() 方法会返回当前值,然后将其递增。例如 currentIndex 当前值是0,那么 getAndIncrement() 会返回0,并将currentIndex设置为1,依次类推。

size是 serviceMetaInfoList 的大小,取模运算大致流程如下:

//假设size=5
第一次调用:currentIndex.getAndIncrement() % 5 -> 0 % 5 = 0 返回服务1。
第二次调用:currentIndex.getAndIncrement() % 5 -> 1 % 5 = 1 返回服务2。
第三次调用:currentIndex.getAndIncrement() % 5 -> 2 % 5 = 2 返回服务3。
第四次调用:currentIndex.getAndIncrement() % 5 -> 3 % 5 = 3 返回服务4。
第五次调用:currentIndex.getAndIncrement() % 5 -> 4 % 5 = 4 返回服务5。
第六次调用:currentIndex.getAndIncrement() % 5 -> 5 % 5 = 0 返回服务1。
(循环开始)

实现随机负载均衡器。

使用Java自带的Random类实现随机选取:

package com.khr.krpc.loadbalancer;

import com.khr.krpc.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * 随机负载均衡器
 */
public class RandomLoadBalancer implements LoadBalancer{

    private final Random random = new Random();

    @Override
    public ServiceMetaInfo select(Map<String,Object> requestParams,List<ServiceMetaInfo> serviceMetaInfoList){
        int size = serviceMetaInfoList.size();
        if (size == 0){
            return null;
        }
        //只有一个服务,不用随机
        if (size == 1){
            return serviceMetaInfoList.get(0);
        }
        return serviceMetaInfoList.get(random.nextInt(size));
    }
}

实现一致性哈希负载均衡器。

使用TreeMap实现一致性哈希环,该数据结构提供了 ceilingEntry 和 firstEntry 两个方法,便于获取符合算法要求的节点:

package com.khr.krpc.loadbalancer;

import com.khr.krpc.model.ServiceMetaInfo;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * 一致性哈希负载均衡器
 */

public class ConsistentHashLoadBalancer implements LoadBalancer{

    /**
     * 一致性 Hash 环,存放虚拟节点
     */
    private final TreeMap<Integer, ServiceMetaInfo> virtualNodes = new TreeMap<>();

    /**
     * 虚拟节点数
     */
    private static final int VIRTUAL_NODE_NUM = 100;

    @Override
    public ServiceMetaInfo select(Map<String, Object> requestParams,List<ServiceMetaInfo> serviceMetaInfoList){
        if (serviceMetaInfoList.isEmpty()){
            return null;
        }

        //构建虚拟节点
        for (ServiceMetaInfo serviceMetaInfo : serviceMetaInfoList){
            for (int i = 0; i < VIRTUAL_NODE_NUM; i++){
                int hash = getHash(serviceMetaInfo.getServiceAddress() + "#" + i);
                virtualNodes.put(hash,serviceMetaInfo);
            }
        }

        //获取调用请求的 hash 值
        int hash = getHash(requestParams);

        //选择最接近且大于等于调用请求 hash 值的虚拟节点
        Map.Entry<Integer,ServiceMetaInfo> entry = virtualNodes.ceilingEntry(hash);
        if (entry == null){
            //如果没有大于等于调用请求 hash 值的虚拟节点,则返回环首部的节点
            entry = virtualNodes.firstEntry();
        }
        return  entry.getValue();
    }

    /**
     * Hash 算法
     *
     * @param key
     * @return
     */
    private int getHash(Object key){
        return key.hashCode();
    }
}

该均衡器给每个服务实例创建100个虚拟节点,并且每个节点的哈希值由其服务地址 getServiceAddress() 和虚拟节点索引 i 组成一个字符串构成。

//某服务1地址为"192.168.0.1":
虚拟节点 0 的哈希值:getHash("192.168.0.1#0")
虚拟节点 1 的哈希值:getHash("192.168.0.1#1")
虚拟节点 2 的哈希值:getHash("192.168.0.1#2")
……

//某服务2地址为"192.168.0.2":
虚拟节点 0 的哈希值:getHash("192.168.0.2#0")
虚拟节点 1 的哈希值:getHash("192.168.0.2#1")
虚拟节点 2 的哈希值:getHash("192.168.0.2#2")
……

依次类推

此外,该一致性哈希均衡器中的 Hash 算法只是简单调用了对象的 hashCode 方法,可以自定义更复杂的哈希算法。

(2)支持配置和扩展负载均衡器

像注册中心和序列化器一样,负载均衡器同样支持自定义,让开发者能够填写配置来指定使用的负载均衡器。依旧使用工厂创建对象、使用SPI动态加载。

在loadbalancer包下创建LoadBalancerKeys类,列举所有支持的负载均衡器键名:

package com.khr.krpc.loadbalancer;

/**
 * 负载均衡器键名常量
 */
public interface LoadBalancerKeys {

    /**
     * 轮询
     */
    String ROUND_ROBIN = "roundRobin";

    /**
     * 随机
     */
    String RANDOM = "random";

    /**
     * 一致性哈希
     */
    String CONSISTENT_HASH = "consistentHash";
}

在loadbalancer包下创建LoadBalancerFactory类。

使用工厂模式,支持根据 key 从SPI获取负载均衡器对象实例(和SerializerFactory几乎一致):

package com.khr.krpc.loadbalancer;

import com.khr.krpc.spi.SpiLoader;

/**
 * 负载均衡器工厂(工厂模式,用于获取负载均衡器对象)
 */
public class LoadBalancerFactory {

    static {
        SpiLoader.load(LoadBalancer.class);
    }

    /**
     * 默认负载均衡器
     */
    private static final LoadBalancer DEFAULT_LOAD_BALANCER = new RoundRobinLoadBalancer();

    /**
     * 获取实例
     *
     * @param key
     * @return
     */
    public static LoadBalancer getInstance(String key){
        return SpiLoader.getInstance(LoadBalancer.class, key);
    }
}

在META-INF的rpc/system目录下编写负载均衡器接口的SPI配置文件,文件名称为com.khr.krpc.loadbalancer.LoadBalancer:

roundRobin=com.khr.krpc.loadbalancer.RoundRobinLoadBalancer
random=com.khr.krpc.loadbalancer.RandomLoadBalancer
consistentHash=com.khr.krpc.loadbalancer.ConsistentHashLoadBalancer

在RpcConfig类中新增负载均衡器全局配置:

package com.khr.krpc.config;

import com.khr.krpc.loadbalancer.LoadBalancerKeys;
import com.khr.krpc.serializer.SerializerKeys;
import lombok.Data;

/**
 * RPC框架配置
 */
@Data
public class RpcConfig {

     ……

    /**
     * 负载均衡器
     */
    private String loadBalancer = LoadBalancerKeys.ROUND_ROBIN;
}

(3)应用负载均衡器

修改ServiceProxy类中针对服务节点调用的代码,改为调用负载均衡器获取节点:

//负载均衡器
 LoadBalancer loadBalancer = LoadBalancerFactory.getInstance(rpcConfig.getLoadBalancer());
//将调用方法名(请求路径)作为负载均衡参数
Map<String, Object> requestParams = new HashMap<>();
requestParams.put("methodName", rpcRequest.getMethodName());
ServiceMetaInfo selectedServiceMetaInfo = loadBalancer.select(requestParams,serviceMetaInfoList);

4. 测试

(1)测试负载均衡算法

编写单元测试类LoadBalancerTest:

package com.khr.rpc.loadbalancer;

import com.khr.krpc.loadbalancer.ConsistentHashLoadBalancer;
import com.khr.krpc.loadbalancer.LoadBalancer;
import com.khr.krpc.loadbalancer.RandomLoadBalancer;
import com.khr.krpc.loadbalancer.RoundRobinLoadBalancer;
import com.khr.krpc.model.ServiceMetaInfo;
import org.junit.Assert;
import org.junit.Test;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.*;

/**
 * 负载均衡器测试
 */
public class LoadBalancerTest {

    final LoadBalancer loadBalancer = new RandomLoadBalancer();

    @Test
    public void select(){
        //请求参数
        Map<String, Object> requestParams = new HashMap<>();
        requestParams.put("methodName","apple");
        //服务列表
        ServiceMetaInfo serviceMetaInfo1 = new ServiceMetaInfo();
        serviceMetaInfo1.setServiceName("MyService");
        serviceMetaInfo1.setServiceVersion("1.0");
        serviceMetaInfo1.setServiceHost("localhost");
        serviceMetaInfo1.setServicePort(1234);
        ServiceMetaInfo serviceMetaInfo2 = new ServiceMetaInfo();
        serviceMetaInfo2.setServiceName("MyService");
        serviceMetaInfo2.setServiceVersion("1.0");
        serviceMetaInfo2.setServiceHost("khr.icu");
        serviceMetaInfo2.setServicePort(80);
        List<ServiceMetaInfo> serviceMetaInfoList = Arrays.asList(serviceMetaInfo1,serviceMetaInfo2);
        //连续调用3次
        ServiceMetaInfo serviceMetaInfo = loadBalancer.select(requestParams,serviceMetaInfoList);
        System.out.println(serviceMetaInfo);
        Assert.assertNotNull(serviceMetaInfo);
        serviceMetaInfo = loadBalancer.select(requestParams,serviceMetaInfoList);
        System.out.println(serviceMetaInfo);
        Assert.assertNotNull(serviceMetaInfo);
        serviceMetaInfo = loadBalancer.select(requestParams,serviceMetaInfoList);
        System.out.println(serviceMetaInfo);
        Assert.assertNotNull(serviceMetaInfo);

    }
}

替换loadBalancer对象为不同的负载均衡器,有不同的运行结果:

Random

RoundRobin

 Consistent Hashing

(2)测试负载均衡调用

在不同的端口启动两个服务提供者,然后启动消费者,通过Debug或控制台输出来观察每次请求的节点地址即可。这里不再做详细演示。

至此,扩展功能,负载均衡完成。

八. 重试机制

1. 需求分析

目前的RPC框架中,如果消费者调用接口失败,就会直接报错。

// rpc请求
RpcResponse rpcResponse = VertxTcpClient.doRequest(rpcRequest, selectedServiceMetaInfo);
return rpcResponse.getData();
} catch (Exception e) {
    throw new RuntimeException("调用失败");
}

调用接口失败可能是因为网络问题,也可能是服务提供者的问题。这种情况下,我们更希望消费者拥有自动重试的能力,提高系统可用性。

2. 设计方案

(1)什么是重试机制

当调用方发起的请求失败时,RPC框架可以进行自动重试,重新发送请求,用户可以设置是否开启重试机制以及重试次数等。

调用方或消费者发起RPC调用时,会通过负载均衡器选择一个合适的节点发送请求。如果请求失败或收到异常信息,就可以捕获异常并触发重试机制,即重新通过负载均衡器选择节点发送请求,同时记录重试的次数,当达到用户设置的最大次数限制后,抛出异常给消费者,否则一直重试。

(2)为什么需要重试机制

  • 提高系统可用性可靠性:当远程服务调用失败,重试机制可以让系统自动重新发送请求,保证接口的调用执行。
  • 有效处理临时性错误:重试机制能够有效缓解如网络延迟、连接异常等临时性错误的影响,提高调用成功率。
  • 降低调用端复杂性:重试机制将捕获异常并触发重试的逻辑封装在框架内部,无需手动操作。

(3)重试算法

固定间隔重试(Fixed Interval Retry):

  • 每次重试间隔一个固定时间,如 1 秒。
  • 适用于对响应时间要求不严格的场景。

指数退避重试(Exponential Backoff Retry):

  • 每次重试间隔的时间呈指数增长,如 1 秒、2 秒、4 秒、8 秒等。
  • 适用于网络波动较大的场景,避免短时间内发送大量重复请求。

随机延迟重试(Random Delay Retry):

  • 每次重试的时间间隔随机,在一定范围内波动。
  • 适用于避免重试请求同步的场景,比如防止雪崩效应。

可变延迟重试(Variable Delay Retry):

  • 根据先前重试的成功或失败情况,动态调整下一次重试的延迟时间。

不重试(No Retry):

  • 直接返回失败结果,不重试。
  • 适用于对响应时间要求较高的场景。

这些算法或策略可以组合使用,应当根据具体的业务场景灵活调整。比如可以先使用指数退避重试。如果连续多次重试失败,则切换到固定间隔重试策略。

(4)重试条件

即什么时候或者什么条件下触发重试。

考虑到提高系统可用性,当发生网络波动或其它异常情况时,触发重试。

(5)停止重试

通常情况下,重试是有次数限制的,不能无限制重试,否则会造成雪崩。

主流的停止重试策略:

  • 最大尝试次数:重试达到最大次数后不再重试。
  • 超时停止:重试达到最大时间后不再重试。

(6)重试工作

通常重试就是执行原本要做的操作,比如重新发送请求。

但如果重试达到上限,也需要进行一些其它操作,如:

  • 通知告警:通知开发者人工介入。
  • 降级容错:改为调用其它接口或执行其它操作。

3. 具体实现

(1)2 种重试策略实现

创建 fault.retry 包,将所有重试相关的代码都放到该包下。

创建 RetryStrategy 重试策略通用接口。

提供一个重试方法,接收一个具体的任务参数,使用 Callable 类代表一个任务:

package com.khr.krpc.fault.retry;

import com.khr.krpc.model.RpcResponse;
import java.util.concurrent.Callable;

/**
 * 重试策略
 */
public interface RetryStrategy {

    /**
     * 重试
     *
     * @param callable
     * @return
     * @throws Exception
     */
    RpcResponse doRetry(Callable<RpcResponse> callable) throws Exception;
}

引入 Guava-Retrying 重试库,使用该库能够轻松实现多种不同的重试算法:

 <dependency>
     <groupId>com.github.rholder</groupId>
     <artifactId>guava-retrying</artifactId>
     <version>2.0.0</version>
 </dependency>

不重试策略

即直接执行一次任务:

package com.khr.krpc.fault.retry;

import com.khr.krpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;

/**
 * 重试策略 —— 不重试
 */
@Slf4j
public class NoRetryStrategy implements RetryStrategy{

    /**
     * 重试
     *
     * @param callable
     * @return
     * @throws Exception
     */
    public RpcResponse doRetry(Callable<RpcResponse> callable) throws Exception {
        return callable.call();
    }
}

固定间隔重试策略

使用 Guave-Retrying 提供的 RetryerBuilder 能够轻松指定重试条件、重试等待策略、重试停止策略、重试工作等:

package com.khr.krpc.fault.retry;

import com.github.rholder.retry.*;
import com.khr.krpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * 重试策略 —— 固定时间间隔
 */
@Slf4j
public class FixedIntervalRetryStrategy implements RetryStrategy {

    /**
     * 重试
     *
     * @param callable
     * @return
     * @throws ExecutionException
     * @throws RetryException
     */
    public RpcResponse doRetry(Callable<RpcResponse> callable) throws ExecutionException, RetryException{
        Retryer<RpcResponse> retryer = RetryerBuilder.<RpcResponse>newBuilder()
                .retryIfExceptionOfType(Exception.class)
                .withWaitStrategy(WaitStrategies.fixedWait(3L,TimeUnit.SECONDS))
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                .withRetryListener(new RetryListener() {
                    @Override
                    public <V> void onRetry(Attempt<V> attempt) {
                        log.info("重试次数 {}", attempt.getAttemptNumber());
                    }
                })
                .build();
        return retryer.call(callable);
    }
}
  • retryIfExceptionOfType 方法指定重试条件,出现 Exception 异常时触发重试。
  • withWaitStrategy  方法指定重试策略,选择 fixedWait 固定时间间隔策略。
  • withStopStrategy 方法指定停止重试策略,选择 stopAfterAttempt 超过最大重试次数停止。
  • withRetryListener 方法监听重试,每次重试时记录当前次数。

(2)支持配置和扩展重试策略

和序列化器、注册中心、负载均衡器一样,重试策略本身也可以使用SPI + 工厂设计模式,从而允许开发者动态配置和扩展自定义重试策略。

在 fault.retry 包下创建 RetryStrategyKeys 类,列举所有支持的重试策略键名:

package com.khr.krpc.fault.retry;

/**
 * 重试策略键名常量
 */
public interface RetryStrategyKeys {

    /**
     * 不重试
     */
    String NO = "no";

    /**
     * 固定时间间隔
     */
    String FIXED_INTERVAL = "fixedInterval";
}

创建 RetryStrategyFactory 类,使用工厂模式,支持根据 key 从SPI获取重试策略对象实例:

package com.khr.krpc.fault.retry;

import com.khr.krpc.spi.SpiLoader;

/**
 * 重试策略工厂(用于获取重试器对象)
 */

public class RetryStrategyFactory {

    static {
        SpiLoader.load(RetryStrategy.class);
    }

    /**
     * 默认重试器
     */
    private static final RetryStrategy DEFAULT_RETRY_STRATEGY = new NoRetryStrategy();

    /**
     * 获取实例
     *
     * @pram key
     * @return
     */
    public static RetryStrategy getInstance(String key) {
        return SpiLoader.getInstance(RetryStrategy.class, key);
    }
}

在META-INF的 rpc/system 目录下创建重试策略接口的SPI配置文件,名称为com.khr.krpc.fault.retry.RetryStrategy:

no=com.khr.krpc.fault.retry.NoRetryStrategy
fixedInterval=com.khr.krpc.fault.retry.FixedIntervalRetryStrategy

在 RpcConfig 类中增加重试策略的全局配置:

package com.khr.krpc.config;

import com.khr.krpc.fault.retry.RetryStrategy;
import com.khr.krpc.fault.retry.RetryStrategyKeys;
import com.khr.krpc.loadbalancer.LoadBalancerKeys;
import com.khr.krpc.serializer.SerializerKeys;
import lombok.Data;

/**
 * RPC框架配置
 */
@Data
public class RpcConfig {

    ……

    /**
     * 负载均衡器
     */
    private String loadBalancer = LoadBalancerKeys.ROUND_ROBIN;

    /**
     * 重试策略
     */
    private String retryStrategy = RetryStrategyKeys.NO;
}

(3)应用重试功能

修改 ServiceProxy 中请求失败部分的逻辑,先从工厂中获取重试器,然后将VertxTcpClient.doRequest 封装为一个可重试的任务,一个 Callable接口。并将请求失败作为重试条件,触发重试机制,系统自动执行重试。

//发送 TCP 请求
//使用重试机制
RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(rpcConfig.getRetryStrategy());
RpcResponse rpcResponse = retryStrategy.doRetry(() ->
                    VertxTcpClient.doRequest(rpcRequest, selectedServiceMetaInfo)
            ); //使用 Lambda 表达式将 VertxTcpClient.doRequest 封装为一个匿名函数
            return rpcResponse.getData();
        } catch (IOException e){
            throw new RuntimeException("调用失败");
        }

4. 测试

启动服务提供者后,使用Debug模模式启动消费者,当消费者发起调用时,立刻停止服务提供者,能看到调用失败后重试的情况。

至此,扩展功能,重试机制完成。

九. 容错机制

1. 需求分析

重试机制提升了消费端的可靠性和健壮性。但如果RPC框架在重试了数次之后依然失败,应该如何进一步处理呢?或者当出现调用失败时,是否有除了重试之外的其他策略?

通过容错机制,能够进一步提升消费端的可用性,带来更好的用户体验。

2. 设计方案

(1)什么是容错机制

当系统出现异常情况时,通过一定的策略保证系统稳定运行,从而提高系统的可靠性和健壮性。

容错机制对于构建高可用、高可靠的分布式系统尤为关键。分布式系统中的各个组件都可能存在网络故障或节点故障等问题,容错机制通过顾全大局,来尽可能消除单点故障对系统带来的整体影响。

容错机制主要包括以下几个方面:

故障检测和隔离

  • 系统需要能够及时发现并定位故障,将故障节点隔离,防止扩散。
  • 可以采用心跳检测、状态检查等手段检测故障。
  • 发现故障后可以进行服务熔断将故障节点隔离。

请求重试和超时控制

  • 临时性的网络服务异常,可以采用重试机制提高调用成功率。
  • 超时控制需设置合理的超时时间,超时后停止重试,避免无限重试。

容错路由和负载均衡

  • 当某个节点故障时,可以通过容错路由将请求重新分配给其它可用节点。
  • 负载均衡策略应根据不同的业务场景进行选择。

服务降级和业务兜底

  • 某服务节点故障时,可用通过服务降级使用其它备选服务。
  • 通过业务层面的兜底措施,保证核心功能可用。

资源隔离和限流

  • 将不同类别的服务或模块的资源进行隔离,比如使用容器。
  • 对关键热门服务节点施加限流措施。

数据备份和恢复

  • 定期备份系统状态和业务数据,便于故障恢复。
  • 备份方案包括数据备份、日志备份、配置备份等。

监控报警和自愈机制

  • 通过监听机制,实时监测系统运行状态,并及时告警。
  • 通过各类容错方案的结合,建立故障的自动修复能力。

(2)常用的容错策略

  • Fail-Over 故障转移:服务节点调用失败后,切换至其它节点再次调用,和重试类似。通常会和负载均衡策略搭配使用,确保流量能够自动分配到可用节点。
  • Fail-Back 失败自动恢复:可以理解为降级策略,某服务调用失败后,通过其他方法完成正常调用。最大程度上减少服务中断的时间。
  • Fail-Safe 静默处理:忽略出现的异常情况,不做处理,或者返回一个安全的默认值或空值。适用于对返回结果容忍度较高的场景,比如日志记录。
  • Fail-Fast 快速失败:系统出现调用错误时,立刻报错,交给外层调用方处理。适用于对响应时间敏感的场景,如用户交互界面。

除了上述几种策略外,还有一些其它技术也具有容错作用:

  • 重试:本质上就是一种降级策略,失败后再试一次。
  • 限流:限制请求的数量或频率,保证系统不过载。
  • 降级:系统出现错误后,改为执行其它更加稳定可用的操作。是一种兜底策略,牺牲一定的服务质量来保证系统的稳定运行。
  • 熔断:系统出现故障后,暂时中断对故障服务的请求调用。
  • 超时控制:给请求或重试设置超时时间,以防止资源无限制占用。

(3)容错方案设计

  • 先容错再重试

系统发生异常后先触发容错机制,比如记录日志、告警等,再进行重试。

  • 先重试再容错

系统发生异常后先尝试重试,如果重试多次后依然失败,则触发容错机制。

在本项目中采用先重试再容错的方案。先通过重试解决一些临时性问题,如果重试失败,或者问题较严重,再触发容错,进行服务降级、快速失败、熔断等。

3. 具体实现

(1)2 种基础容错策略实现

创建 fault.tolerant 包,将所有容错相关的代码都放在该包下。

创建 TolerantStrategy 容错策略通用接口,提供一个容错方法,使用Map类型的参数接收上下文信息(用于灵活地传递容错处理需要用到的数据),并且接受一个具体的异常类参数,并且由于容错是应用到发送请求操作的,所以返回值是 RpcResponse:

package com.khr.krpc.fault.tolerant;

import com.khr.krpc.model.RpcResponse;

import java.util.Map;

/**
 * 容错策略
 */
public interface TolerantStrategy {

    /**
     * 容错
     *
     * @param context 上下文,用于传递数据
     * @param e 异常
     * @return
     */
    RpcResponse doTolerant(Map<String,Object> context, Exception e);
}

 Map<String, Object> context

该参数是一个上下文对象,用于传递接受容错处理过程中的数据。在分布式系统中,一个远程调用出现异常时,需要根据当前的上下文信息来决定下一步如何进行容错处理,这个信息可能包括:请求参数、调用链路信息、服务实例元数据、重试次数等。通过该对象来得到丰富有效的信息,从而做出更加合理的容错决策。

Exception e

表示调用时出现的异常,容错策略需要根据异常类型、错误信息等决定容错措施。比如网络波动可用选择重试,业务异常可以采用降级等。

RpcResponse doTolerant(Map<String,Object> context, Exception e)

该方法定义了容错的具体实现,接收一个上下文信息和异常对象作为参数,返回一个 RpcResponse 作为处理结果。

Fail-Fast 快速失败容错策略

 出现异常后直接抛出报错:

package com.khr.krpc.fault.tolerant;

import com.khr.krpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;

/**
 * 快速失败 容错策略(立刻通知外层调用方)
 */
@Slf4j
public class FailFastTolerantStrategy implements TolerantStrategy{

    @Override
    public RpcResponse doTolerant(Map<String, Object> context, Exception e){
        log.error("Fail-Fast处理异常",e);
        throw new RuntimeException("服务报错",e);

    }
}

Fail-Safe 静默处理容错策略

发生异常后记录日志,然后正常返回一个响应对象,好像没出现过报错一样:

package com.khr.krpc.fault.tolerant;

import com.khr.krpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;

/**
 * 静默处理 容错策略
 */
@Slf4j
public class FailSafeTolerantStrategy implements TolerantStrategy{

    @Override
    public RpcResponse doTolerant(Map<String,Object> context, Exception e){
        log.info("Fail-Safe处理异常",e);
        return new RpcResponse();
    }
}

其它策略可以自己实现。

 (2)支持配置和扩展容错策略

与序列化器、注册中心、负载均衡器、重试策略一样,允许消费者自定义容错策略。依然采用工厂设计模式 + SPI动态加载。

在 fault.tolerant 包下创建 TolerantStrategyKeys类,列举所有支持的容错策略键名:

package com.khr.krpc.fault.tolerant;

/**
 * 容错策略键名常量
 */

public interface TolerantStrategyKeys {

    /**
     * 故障恢复
     */
    String FAIL_BACK = "failBack";

    /**
     * 快速失败
     */
    String FAIL_FAST = "failFast";

    /**
     * 故障转移
     */
    String FAIL_OVER = "failOver";

    /**
     * 静默处理
     */
    String FAIL_SAFE = "failSafe";
}

创建 TolerantStrategyFactory类,使用工厂设计模式,支持根据 key 从SPI获取容错策略对象实例:

package com.khr.krpc.fault.tolerant;

import com.khr.krpc.spi.SpiLoader;

/**
 * 容错策略工厂(工厂模式,用于获取容错策略对象)
 */

public class TolerantStrategyFactory {

    static {
        SpiLoader.load(TolerantStrategy.class);
    }

    /**
     * 默认容错策略
     */
    private static final TolerantStrategy DEFAULT_TOLERANT_STRATEGY = new FailFastTolerantStrategy();

    /**
     * 获取实例
     *
     * @param key
     * @return
     */
    public static TolerantStrategy getInstance(String key){
        return SpiLoader.getInstance(TolerantStrategy.class, key);
    }
}

在 META-INF 的rpc/system目录下创建容错策略接口的SPI配置文件,名称为com.khr.krpc.fault.tolerant.TolerantStrategy:

failBack=com.khr.krpc.tolerant.FailBackTolerantStrategy
failFast=com.khr.krpc.tolerant.FailFastTolerantStrategy
failOver=com.khr.krpc.tolerant.FailOverTolerantStrategy
failSafe=com.khr.krpc.tolerant.FailSafeTolerantStrategy

在 RpcConfig 类中增加容错策略的全局配置:

package com.khr.krpc.config;

import com.khr.krpc.fault.retry.RetryStrategy;
import com.khr.krpc.fault.retry.RetryStrategyKeys;
import com.khr.krpc.fault.tolerant.TolerantStrategy;
import com.khr.krpc.fault.tolerant.TolerantStrategyKeys;
import com.khr.krpc.loadbalancer.LoadBalancerKeys;
import com.khr.krpc.serializer.SerializerKeys;
import lombok.Data;

/**
 * RPC框架配置
 */
@Data
public class RpcConfig {
 
    ……

    /**
     * 负载均衡器
     */
    private String loadBalancer = LoadBalancerKeys.ROUND_ROBIN;

    /**
     * 重试策略
     */
    private String retryStrategy = RetryStrategyKeys.NO;

    /**
     * 容错策略
     */
    private String tolerantStrategy = TolerantStrategyKeys.FAIL_FAST;
}

(3)应用容错策略

修改 ServiceProxy 中重试后抛出异常部分的逻辑,改为执行容错策略:

            //发送 TCP 请求
            //使用重试机制
            RpcResponse rpcResponse;
            try {
            RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(rpcConfig.getRetryStrategy());
            rpcResponse = retryStrategy.doRetry(() ->
                    VertxTcpClient.doRequest(rpcRequest, selectedServiceMetaInfo)
            );
        } catch (IOException e){
            //容错机制
            TolerantStrategy tolerantStrategy = TolerantStrategyFactory.getInstance(rpcConfig.getTolerantStrategy());
            rpcResponse = tolerantStrategy.doTolerant(null,e);
        }
        return rpcResponse.getData()

完整的 ServiceProxy 代码为:

package com.khr.krpc.proxy;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.khr.krpc.RpcApplication;
import com.khr.krpc.config.RpcConfig;
import com.khr.krpc.constant.RpcConstant;
import com.khr.krpc.fault.retry.RetryStrategy;
import com.khr.krpc.fault.retry.RetryStrategyFactory;
import com.khr.krpc.fault.tolerant.TolerantStrategy;
import com.khr.krpc.fault.tolerant.TolerantStrategyFactory;
import com.khr.krpc.loadbalancer.LoadBalancer;
import com.khr.krpc.loadbalancer.LoadBalancerFactory;
import com.khr.krpc.model.RpcRequest;
import com.khr.krpc.model.RpcResponse;
import com.khr.krpc.model.ServiceMetaInfo;
import com.khr.krpc.protocol.*;
import com.khr.krpc.registry.Registry;
import com.khr.krpc.registry.RegistryFactory;
import com.khr.krpc.serializer.Serializer;
import com.khr.krpc.serializer.SerializerFactory;
import com.khr.krpc.server.tcp.VertxTcpClient;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.net.NetClient;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.SocketAddress;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;


/**
 * 服务代理(JDK动态代理)
 */

public class ServiceProxy implements InvocationHandler {

    /**
     * 调用代理
     *
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
        //指定序列化器
        final Serializer serializer = SerializerFactory.getInstance(RpcApplication.getRpcConfig().getSerializer());

        //构造请求
        String serviceName = method.getDeclaringClass().getName();
        RpcRequest rpcRequest = RpcRequest.builder()
                .serviceName(serviceName)
                .methodName(method.getName())
                .parameterTypes(method.getParameterTypes())
                .args(args)
                .build();

            //从注册中心获取服务提供者请求地址
            RpcConfig rpcConfig = RpcApplication.getRpcConfig();
            Registry registry = RegistryFactory.getInstance(rpcConfig.getRegistryConfig().getRegistry());
            ServiceMetaInfo serviceMetaInfo = new ServiceMetaInfo();
            serviceMetaInfo.setServiceName(serviceName);
            serviceMetaInfo.setServiceVersion(RpcConstant.DEFAULT_SERVICE_VERSION);
            List<ServiceMetaInfo> serviceMetaInfoList = registry.serviceDiscovery(serviceMetaInfo.getServiceKey());
            if (CollUtil.isEmpty(serviceMetaInfoList)) {
                throw new RuntimeException("暂无服务地址");
            }

            //负载均衡器
            LoadBalancer loadBalancer = LoadBalancerFactory.getInstance(rpcConfig.getLoadBalancer());
            //将调用方法名(请求路径)作为负载均衡参数
            Map<String, Object> requestParams = new HashMap<>();
            requestParams.put("methodName", rpcRequest.getMethodName());
            ServiceMetaInfo selectedServiceMetaInfo = loadBalancer.select(requestParams,serviceMetaInfoList);

            //发送 TCP 请求
            //使用重试机制
            RpcResponse rpcResponse;
            try {
            RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(rpcConfig.getRetryStrategy());
            rpcResponse = retryStrategy.doRetry(() ->
                    VertxTcpClient.doRequest(rpcRequest, selectedServiceMetaInfo)
            );
        } catch (IOException e){
            //容错机制
            TolerantStrategy tolerantStrategy = TolerantStrategyFactory.getInstance(rpcConfig.getTolerantStrategy());
            rpcResponse = tolerantStrategy.doTolerant(null,e);
        }
        return rpcResponse.getData();
    }
}

4. 测试

测试流程与重试机制类型。重试多次后,即可看到容错策略的执行。

至此,扩展功能,容错机制完成。

十. 启动机制与注解驱动

1. 需求分析

希望能够让开发者使用最简单的方式使用框架,一行代码完成使用。

2. 设计方案

(1)启动机制设计

将开发者使用框架时所有的启动代码封装成一个专门的启动类,由服务提供者或消费者直接调用。

但由于服务提供者和消费者需要初始化的模块不同,比如消费者不需要启动Web服务器,因此分别针对服务提供者和消费者创建一个启动类,如果时二者都需要初始化的模块,则放到全局应用类 RpcApplication 中,复用代码。

(2)注解驱动设计

Dubbo中框架的使用通过注解驱动,更加快速和便捷。只需要在服务提供者实现类上加@DubboService注解,即可快速注册服务;在消费者实现类上加@DubboReference注解,即可快速使用服务。并且Dubbo还提供了 Spring Boot Starter,用于在Spring Boot项目中使用框架。

因此,可以仿照Dubbo的设计模式,创建一个 Spring Boot Starter 项目,并通过注解驱动框架初始化,完成服务注册和获取引用。

3. 具体实现

(1)启动机制

创建 bootstrap 包,所有和框架启动初始化相关的代码都放到该包下。

服务提供者启动类

创建 ProviderBootstrap 类,直接复制 ProviderExample 类,然后略微修改,支持用户传入自己要注册的服务,比如注册服务时,需要填入服务名称、服务实现类等字段:

// 注册服务
String serviceName = UserService.class.getName();
LocalRegistry.register(serviceName, UserServiceImpl.class)

将这些字段单独封装,在 model 包下创建 ServiceRegisterInfo 类:

package com.khr.krpc.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 服务注册信息类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServiceRegisterInfo<T> {

    /**
     * 服务名称
     */
    private String serviceName;

    /**
     * 实现类
     */
    private Class<? extends T> implClass;
}

这样服务提供者的初始化方法只需要接受封装的注册信息列表作为参数即可,类似于 ServiceMetaInfoList:

package com.khr.krpc.bootstrap;

import com.khr.krpc.RpcApplication;
import com.khr.krpc.config.RegistryConfig;
import com.khr.krpc.config.RpcConfig;
import com.khr.krpc.model.ServiceMetaInfo;
import com.khr.krpc.model.ServiceRegisterInfo;
import com.khr.krpc.registry.LocalRegistry;
import com.khr.krpc.registry.Registry;
import com.khr.krpc.registry.RegistryFactory;
import com.khr.krpc.server.tcp.VertxTcpServer;

import java.util.List;

/**
 * 服务提供者示例
 */
public class ProviderBootstrap {

    /**
     * 初始化
     */

    public static void init(List<ServiceRegisterInfo<?>> serviceRegisterInfoList){
        // RPC 框架初始化(配置和注册中心)
        RpcApplication.init();
        // 全局配置
        final RpcConfig rpcConfig = RpcApplication.getRpcConfig();

        //注册服务
        for (ServiceRegisterInfo<?> serviceRegisterInfo : serviceRegisterInfoList) {
            String serviceName = serviceRegisterInfo.getServiceName();
            //本地注册
            LocalRegistry.register(serviceName, serviceRegisterInfo.getImplClass());

            //注册服务到注册中心
            RegistryConfig registryConfig = rpcConfig.getRegistryConfig();
            Registry registry = RegistryFactory.getInstance(registryConfig.getRegistry());
            ServiceMetaInfo serviceMetaInfo = new ServiceMetaInfo();
            serviceMetaInfo.setServiceName(serviceName);
            serviceMetaInfo.setServiceHost(rpcConfig.getServerHost());
            serviceMetaInfo.setServicePort(Integer.valueOf(rpcConfig.getServerPort()));
            try {
                registry.register(serviceMetaInfo);
            } catch (Exception e) {
                throw new RuntimeException(serviceName + "服务注册失败", e);
            }
        }

        //启动 TCP 服务
        VertxTcpServer vertxTcpServer = new VertxTcpServer();
        vertxTcpServer.doStart(Integer.parseInt(rpcConfig.getServerPort()));

    }
}

现在,在服务提供者中使用RPC框架只需要定义要注册的服务列表,然后一行代码 ProviderBootstrap.init 即可调用,完成初始化:

package com.khr.example.provider;

import com.khr.example.common.service.UserService;
import com.khr.krpc.bootstrap.ProviderBootstrap;
import com.khr.krpc.model.ServiceRegisterInfo;

import java.util.ArrayList;
import java.util.List;

/**
 * 服务提供者示例
 */
public class ProviderExample {

    public static void main(String[] args){
        // 要注册服务
        List<ServiceRegisterInfo<?>> serviceRegisterInfoList = new ArrayList<>();
        ServiceRegisterInfo serviceRegisterInfo = new ServiceRegisterInfo(UserService.class.getName(), UserServiceImpl.class);
        serviceRegisterInfoList.add(serviceRegisterInfo);

        // 服务提供者初始化
        ProviderBootstrap.init(serviceRegisterInfoList);
    }
}

消费者启动类

消费者不需要注册服务,也不需要启动Web服务器,只需执行 RpcApplication.init方法即可完成初始化操作:

package com.khr.krpc.bootstrap;

import com.khr.krpc.RpcApplication;

/**
 * 消费者启动类(初始化)
 */
public class ConsumerBootstrap {

    /**
     * 初始化
     */
    public static void init(){
        // RPC框架初始化(配置和注册中心)
        RpcApplication.init();
    }
}

ConsumerExample 类修改为调用启动类:

package com.khr.example.consumer;

import com.khr.example.common.model.User;
import com.khr.example.common.service.UserService;
import com.khr.krpc.bootstrap.ConsumerBootstrap;
import com.khr.krpc.config.RpcConfig;
import com.khr.krpc.proxy.ServiceProxyFactory;
import com.khr.krpc.utils.ConfigUtils;

/**
 * 服务消费者示例
 */
public class ConsumerExample {

    public static void main(String[] args){
        // 服务提供者初始化
        ConsumerBootstrap.init();

        //RpcConfig rpc = ConfigUtils.loadConfig(RpcConfig.class,"rpc");
        //System.out.println(rpc);

        //获取代理
        UserService userService = ServiceProxyFactory.getProxy(UserService.class);
        User user = new User();
        user.setName("KHR");
        //调用
        User newUser = userService.getUser(user);
        if (newUser != null){
           System.out.println(newUser.getName());
        } else {
            System.out.println("user == null");
        }
    }
}

(2)Spring Boot Starter 注解驱动

在根目录中创建新模块 khr-rpc-spring-boot-starter,配置为:

Spring Initializr -> Server URL: start.aliyun.com -> Spring Boot: 2.6.13 -> Dependencies: Spring Configuration Processor

引入之前开放RPC框架依赖:

<dependency>
      <groupId>com.khr</groupId>
      <artifactId>khr-rpc-core</artifactId>
      <version>1.0-SNAPSHOT</version>
</dependency>

模块初始化完成后,首先要定义注解,也就是考虑需要哪些注解。

Dubbo 中的常用注解有:

  • @EnableDubbo:启动Dubbo。
  • @DubboReference:在消费者中使用,声明Dubbo服务引用。
  • @DubboService:在提供者中使用,声明Dubbo服务。
  • @DubboMethod:在提供者和消费者中使用,配置Dubbo方法参数。

基于此,在本项目中定义 3 个基础注解:

  • @EnableRpc
  • @RpcReference
  • @RpcService

在 khr-rpc-spring-boot-starter 包中创建 annotation 包,将所有注解代码都放到该包下。

@EnableRpc 用于全局标识项目需要引入RPC框架,执行初始化方法:

package com.khr.krpc.springboot.starter.annotation;

import org.springframework.context.annotation.Import;
import com.khr.krpc.springboot.starter.bootstrap.RpcConsumerBootstrap;
import com.khr.krpc.springboot.starter.bootstrap.RpcInitBootstrap;
import com.khr.krpc.springboot.starter.bootstrap.RpcProviderBootstrap;

import java.lang.annotation.*;

/**
 * 启用 Rpc 注解
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({RpcInitBootstrap.class,RpcProviderBootstrap.class,RpcConsumerBootstrap.class})
public @interface EnableRpc {

    /**
     * 需要启动 server
     *
     * @return
     */
    boolean needServer() default true;
}

@Target({ElementType.TYPE})  用于指定 @EnableRpc 注解可以被应用于哪些程序元素。此处它被限定为只能应用于类型(Type)级别,即类、接口或枚举。

@Retention(RetentionPolicy.RUNTIME) 用于指定 @EnableRpc 注解的保留策略。RUNTIME 表示该注解会在运行时被JVM读取和使用,可以被反射机制访问。

@Import({RpcInitBootstrap.class,RpcProviderBootstrap.class,RpcConsumerBootstrap.class}) 用于导入其它配置类,此处导入的这三个引导类会自己额外定义。当 @EnableRpc 注解被应用到一个类上时,Spring容器会自动注册这三个引导类。

boolean needServer() default true;是注解中定义的一个属性。用于指定是否需要启动RPC服务器,默认值为 true。

@EnableRpc 注解封装了RPC的相关配置和初始化流程,后续开发者只需要加上该注解即可完成相关操作。

@RpcService 服务提供者注解,在需要注册和提供的服务类上使用。该注解中,指定了服务注册信息属性,比如服务接口实现类、版本号等:

package com.khr.krpc.springboot.starter.annotation;

import com.khr.krpc.constant.RpcConstant;
import org.springframework.stereotype.Component;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 服务提供者注解(用于注册服务)
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RpcService {

    /**
     * 服务接口类
     */
    Class<?> interfaceClass() default void.class;

    /**
     * 版本
     */
    String serviceVersion() default RpcConstant.DEFAULT_SERVICE_VERSION;
}

@Target({ElementType.TYPE}) 和 @Retention(RetentionPolicy.RUNTIME) 注解与 @EnableRpc中的作用相同。

@Component 用于将 @RpcService 标记为Spring组件,也就是被 @Component 标注的类会被Spring容器自动扫描和注册。

Class<?> interfaceClass() default void.class; 该属性指定服务接口类,默认值为 void.class。

String serviceVersion() default RpcConstant.DEFAULT_SERVICE_VERSION; 该属性指定服务版本号,默认值为 RpcConstant.DEFAULT_SERVICE_VERSION。

@RpcService 注解简化了服务提供者的配置,被该注解标注后,Spring容器会自动扫描并注册该服务,同时也会提取服务接口类和版本号等元信息,以备后续调用使用。

@RpcReference 消费者注解,在需要注入代理对象的属性上使用,类似于 Spring 中的 @Resource。该注解中指定了调用服务相关的属性,比如服务接口类、版本号、负载均衡器、重试策略等:

package com.khr.krpc.springboot.starter.annotation;

import com.khr.krpc.constant.RpcConstant;
import com.khr.krpc.fault.retry.RetryStrategyKeys;
import com.khr.krpc.fault.tolerant.TolerantStrategyKeys;
import com.khr.krpc.loadbalancer.LoadBalancerKeys;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 消费者注解(用于注入服务)
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RpcReference {

    /**
     * 服务接口类
     */
    Class<?> inferfaceClass() default void.class;

    /**
     * 版本
     */
    String serviceVersion() default RpcConstant.DEFAULT_SERVICE_VERSION;

    /**
     * 负载均衡器
     */
    String loadBalancer() default LoadBalancerKeys.ROUND_ROBIN;

    /**
     * 重试策略
     */
    String retryStrategy() default RetryStrategyKeys.NO;

    /**
     * 容错策略
     */
    String tolerantStrategy() default TolerantStrategyKeys.FAIL_FAST;

    /**
     * 模拟调用
     */
    boolean mock() default false;
}

该注解简化了消费者的配置,当一个字段被该注解标注,Spring容器会自动注入一个代理对象,该对象负责执行RPC调用。开发者可以通过设置注解属性来配置负载均衡器、重试、容错等策略。

在 starter 包中创建 bootstrap 包,用来存放上面定义的 3 个注解的启动类。

Rpc框架全局启动类 RpcInitBootstrap

在Spring框架初始化时,获取@EnableRpc注解的属性并执行初始化操作。因此通过实现 ImportBeanDefinitionRegistrar 接口,并且在 registerBeanDefinitions 方法中获取到项目的注解和属性:

package com.khr.krpc.springboot.starter.bootstrap;

import com.khr.krpc.RpcApplication;
import com.khr.krpc.config.RpcConfig;
import com.khr.krpc.server.tcp.VertxTcpServer;
import com.khr.krpc.springboot.starter.annotation.EnableRpc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;


/**
 * Rpc 框架启动
 */
@Slf4j
public class RpcInitBootstrap implements ImportBeanDefinitionRegistrar {

    /**
     * Spring 初始化时执行,初始化 RPC 框架
     *
     * @param importingClassMetadata
     * @param registry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){
        // 获取 EnableRpc 注解的属性值
        boolean needServer = (boolean) importingClassMetadata.getAnnotationAttributes(EnableRpc.class.getName())
                .get("needServer");

        // RPC 框架初始化(配置和注册中心)
        RpcApplication.init();

        // 全局配置
        final RpcConfig rpcConfig = RpcApplication.getRpcConfig();

        // 启动服务器
        if (needServer) {
            VertxTcpServer vertxTcpServer = new VertxTcpServer();
            vertxTcpServer.doStart(Integer.parseInt(rpcConfig.getServerPort()));
        } else {
            log.info("不启动 server");
        }

    }
}

public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) ,从Spring 元信息在获取到EnableRpc注解的needServer属性,并通过它判断是否要启动服务器。

Rpc服务提供者启动类 RpcProviderBootstrap

用来获取到所有包含 @RpcService 注解的类,并通过注解的属性和反射机制,获取到要注册的服务信息,完成服务注册。通过实现 BeanPostProcessor 接口的 postProcessAfterInitialization 方法,就可以在某个服务提供者 Bean 初始化后,执行注册服务等操作:

package com.khr.krpc.springboot.starter.bootstrap;

import com.khr.krpc.RpcApplication;
import com.khr.krpc.config.RegistryConfig;
import com.khr.krpc.config.RpcConfig;
import com.khr.krpc.model.ServiceMetaInfo;
import com.khr.krpc.registry.LocalRegistry;
import com.khr.krpc.registry.Registry;
import com.khr.krpc.registry.RegistryFactory;
import com.khr.krpc.springboot.starter.annotation.RpcService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.BeansException;

/**
 * Rpc 服务提供者启动
 */
@Slf4j
public class RpcProviderBootstrap implements BeanPostProcessor {

    /**
     * Bean 初始化后执行,注册服务
     *
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException{
        Class<?> beanClass = bean.getClass();
        RpcService rpcService = beanClass.getAnnotation(RpcService.class);
        if (rpcService != null){
            // 需要注册服务
            // 1.获取服务基本信息
            Class<?> inferfaceClass = rpcService.interfaceClass();
            // 默认值处理
            if (inferfaceClass == void.class){
                inferfaceClass = beanClass.getInterfaces()[0];
            }
            String serviceName = inferfaceClass.getName();
            String serviceVersion = rpcService.serviceVersion();
            // 2.注册服务
            // 本地注册
            LocalRegistry.register(serviceName, beanClass);

            // 全局配置
            final RpcConfig rpcConfig = RpcApplication.getRpcConfig();
            // 注册服务到注册中心
            RegistryConfig registryConfig = rpcConfig.getRegistryConfig();
            Registry registry = RegistryFactory.getInstance(registryConfig.getRegistry());
            ServiceMetaInfo serviceMetaInfo = new ServiceMetaInfo();
            serviceMetaInfo.setServiceName(serviceName);
            serviceMetaInfo.setServiceVersion(serviceVersion);
            serviceMetaInfo.setServiceHost(rpcConfig.getServerHost());
            serviceMetaInfo.setServicePort(Integer.valueOf(rpcConfig.getServerPort()));
            try {
                registry.register(serviceMetaInfo);
            } catch (Exception e){
                throw new RuntimeException(serviceName + "服务注册失败", e);
            }
        }

        return BeanPostProcessor.super.postProcessAfterInitialization(bean,beanName);
    }
}

Rpc消费者启动类 RpcConsumerBootstrap

和服务提供者启动类实现方式类似,在 Bean 初始化后,通过反射获取到 Bean 的所有属性,如果属性包含 @RpcReference 注解,那么就为该属性动态生成代理对象并赋值:

package com.khr.krpc.springboot.starter.bootstrap;

import com.khr.krpc.proxy.ServiceProxyFactory;
import com.khr.krpc.springboot.starter.annotation.RpcReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.BeansException;

import java.lang.reflect.Field;

@Slf4j
public class RpcConsumerBootstrap implements BeanPostProcessor {

    /**
     *  Bean 初始化后执行,注入服务
     *
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException{
        Class<?> beanClass = bean.getClass();
        //遍历对象的所有属性
        Field[] declaredFields = beanClass.getDeclaredFields();
        for (Field field : declaredFields){
            RpcReference rpcReference = field.getAnnotation(RpcReference.class);
            if (rpcReference != null){
                // 为属性生成代理对象
                Class<?> interfaceClass = rpcReference.inferfaceClass();
                if (interfaceClass == void.class){
                    interfaceClass = field.getType();
                }
                field.setAccessible(true);
                Object proxyObject = ServiceProxyFactory.getProxy(interfaceClass);
                try {
                    field.set(bean,proxyObject);
                    field.setAccessible(false);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("为字段注入代理对象失败", e);
                }
            }
        }
        return BeanPostProcessor.super.postProcessAfterInitialization(bean,beanName);
    }
}

beanClass.getDeclaredFields(); 核心方法,用于获取类的所有属性。

4. 测试

创建 2 个示例项目:

  • Spring Boot 提供者:example-springboot-provider
  • Spring Boot 消费者:example-springboot-consumer

 全都引入依赖:

    <dependency>
      <groupId>com.khr</groupId>
      <artifactId>khr-rpc-spring-boot-starter</artifactId>
      <version>0.0.1-SNAPSHOT</version>
    </dependency>

    <dependency>
      <groupId>com.khr</groupId>
      <artifactId>example-common</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>

给示例服务提供者类加上 @EnableRpc 注解:

package com.khr.examplespringbootprovider;

import com.khr.krpc.springboot.starter.annotation.EnableRpc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 示例 Spring Boot 服务提供者应用
 */

@SpringBootApplication
@EnableRpc
public class ExampleSpringbootProviderApplication {

    public static void main(String[] args){
        SpringApplication.run(ExampleSpringbootProviderApplication.class, args);
    }
}

服务提供者提供一个简单服务:

package com.khr.examplespringbootprovider;

import com.khr.example.common.model.User;
import com.khr.example.common.service.UserService;
import com.khr.krpc.springboot.starter.annotation.RpcService;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;

/**
 * 用户服务实现类
 */
@Slf4j
@Service
@RpcService
public class UserServiceImpl implements UserService{

    public User getUser(User user) {
        System.out.println("用户名:" + user.getName());
        return user;
    }
}

给示例消费者类加上 @EnableRpc(needServer = false) 注解,标识启动RPC框架,但不启动Web服务器:

package com.khr.examplespringbootconsumer;

import com.khr.krpc.springboot.starter.annotation.EnableRpc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableRpc(needServer = false)
public class ExampleSpringbootConsumerApplication {

    public static void main(String[] args){
        SpringApplication.run(ExampleSpringbootConsumerApplication.class,args);
    }
}

消费者编写一个 Spring 的 Bean,引入 UserService 属性并添加 @RpcReference 注解,表示需要使用远程服务提供者的服务:

package com.khr.examplespringbootconsumer;

import com.khr.example.common.model.User;
import com.khr.example.common.service.UserService;
import com.khr.krpc.springboot.starter.annotation.RpcReference;
import org.springframework.stereotype.Service;

@Service
public class ExampleServiceImpl {

    /**
     * 使用 Rpc 框架注入
     */

    @RpcReference
    private UserService userService;

    public void test(){
        User user = new User();
        user.setName("xiaokong");
        User resultUser = userService.getUser(user);
        System.out.println(resultUser.getName());
    }
}

创建消费者单元测试,验证能否调用远程服务:

package com.khr.examplespringbootconsumer;

import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;

import org.junit.jupiter.api.Test;


@SpringBootTest
class ExampleServiceImplTest {

    @Resource
    private ExampleServiceImpl exampleService;

    @Test
    void test() {
        exampleService.test();
    }

}

分别启动服务提供者、消费者和单元测试后,运行结果如下:

至此,扩展功能,启动机制与注解驱动完成。

RPC项目扩展版也正式完成,后续可能会根据项目内某些知识点进行额外分享。 

  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Phoenixxxxxxxxxxxxx

感谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值