Spring Cloud Alibaba 教程 | Dubbo(八):集群容错

InvokerInvocationHandler

在上一篇文章【Dubbo服务引用】的最后,我们介绍到通过标签<dubbo:reference>引用一个远程服务接口之后,会得到一个代理对象,该代理对象包含了一个调用处理器InvokerInvocationHandler,它实现了InvocationHandler接口。

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
public class InvokerInvocationHandler implements InvocationHandler {

	/**真正实例是MockClusterInvoker*/
    private final Invoker<?> invoker;

    public InvokerInvocationHandler(Invoker<?> handler) {
        this.invoker = handler;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        //省略部分代码......
        return invoker.invoke(new RpcInvocation(method, args)).recreate();
    }

}

InvocationHandler是一个JDK的接口,它由代理对象的调用处理器负责实现该接口。每一个代理对象实例都有一个关联的InvocationHandler实例,当通过代理对象调用方法时,该方法调用将被编码和转发到InvocationHandler实例的invoke()方法。所以对于Dubbo框架来说,InvokerInvocationHandler的invoke()方法将是消费者服务发起服务远程调用的入口。在发起远程服务调用之前需要先经过Cluster层(集群容错层),下面我们就来介绍Dubbo框架的集群容错。

集群容错整体流程

下图是集群容错层的流程图:
在这里插入图片描述
InvokerInvocationHandler包含了一个Invoker类型变量invoker,它的实例对象是MockClusterInvoker,通过它执行invoker.invoke(new RpcInvocation(method, args)),进入MockClusterInvoker的invoke()方法:

public class MockClusterInvoker<T> implements Invoker<T> {

	/**实例对象是RegistryDirectory*/
    private final Directory<T> directory;

    /**实例对象是FailoverClusterInvoker*/
    private final Invoker<T> invoker;

    public MockClusterInvoker(Directory<T> directory, Invoker<T> invoker) {
        this.directory = directory;
        this.invoker = invoker;
    }

    @Override
    public Result invoke(Invocation invocation) throws RpcException {
        Result result = null;

        String value = directory.getUrl().
        getMethodParameter(invocation.getMethodName(), 
        Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
        if (value.length() == 0 || 
        value.equalsIgnoreCase("false")) {//@1
            //no mock
            result = this.invoker.invoke(invocation);
        } else if (value.startsWith("force")) {//@2
            if (logger.isWarnEnabled()) {
                logger.info("force-mock: " + 
                invocation.getMethodName() + " force-mock enabled , url : " +
                 directory.getUrl());
            }
            //force:direct mock
            result = doMockInvoke(invocation, null);
        } else {//@3
            //fail-mock
            try {
                result = this.invoker.invoke(invocation);
            } catch (RpcException e) {
                if (e.isBiz()) {
                    throw e;
                } else {
                    if (logger.isWarnEnabled()) {
                        logger.warn("fail-mock: " + invocation.getMethodName() + 
                        " fail-mock enabled , url : " + directory.getUrl(), e);
                    }
                    result = doMockInvoke(invocation, e);
                }
            }
        }
        return result;
    }
}

代码@1:处理没有使用Mock的情况(Dubbo通过Mock机制实现服务降级),不做其他额外操作,直接执行this.invoker.invoke(invocation)交给FailoverClusterInvoker处理。

代码@2:处理强制Mock情况,主要逻辑在方法doMockInvoke(invocation, null)

代码@3:处理失败调用Mock情况,该情况下会处理捕获this.invoker.invoke(invocation)出现的RpcException异常,如果是业务异常则直接抛出,否则会执行 doMockInvoke(invocation, e)

我们先来介绍正常流程,FailoverClusterInvoker继承了AbstractClusterInvoker抽象类,进入到AbstractClusterInvoker的invoke()方法。

@Override
public Result invoke(final Invocation invocation) throws RpcException {
    checkWhetherDestroyed();
    LoadBalance loadbalance = null;
    List<Invoker<T>> invokers = list(invocation);//@1
    if (invokers != null && !invokers.isEmpty()) {
    	//Constants.LOADBALANCE_KEY = "loadbalance"
    	//Constants.DEFAULT_LOADBALANCE = "random"
        loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).
        getExtension(invokers.get(0).getUrl()
                .getMethodParameter(invocation.getMethodName(), 
                Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));//@2
    }
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    return doInvoke(invocation, invokers, loadbalance); //@3
}

代码@1:获取所有可用的Invoker集合,这里一个Invoker代表了一个实现该远程接口的服务提供者。

protected List<Invoker<T>> list(Invocation invocation) throws RpcException {
    List<Invoker<T>> invokers = directory.list(invocation);
    return invokers;
}

通过directory获取Invoker集合,Directory接口有两个实现类:RegistryDirectory和StaticDirectory,RegistryDirectory继承AbstractDirectory并实现了NotifyListener,主要负责处理注册中心的事件通知,刷新本地目录缓存和Invoker缓存(前面章节已经详细介绍过)。StaticDirectory表示“静态目录”,内容不会改变,不常使用。

@Override
public List<Invoker<T>> list(Invocation invocation) throws RpcException {
    if (destroyed) {
        throw new RpcException("Directory already destroyed .url: " + getUrl());
    }
    List<Invoker<T>> invokers = doList(invocation);
    List<Router> localRouters = this.routers; // local reference
    if (localRouters != null && !localRouters.isEmpty()) {
        for (Router router : localRouters) {
            try {
                if (router.getUrl() == null || 
                router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) {
                    invokers = router.route(invokers, getConsumerUrl(), invocation);
                }
            } catch (Throwable t) {
                logger.error("Failed to execute router: " + getUrl() + ", 
                cause: " + t.getMessage(), t);
            }
        }
    }
    return invokers;
}

进入AbstractDirectory的list()方法,首先执行模板方法doList()将获取Invoker集合的任务交给子类RegistryDirectory处理,在获取到Invoker集合之后,还要再执行路由操作router.route(invokers, getConsumerUrl(), invocation),过滤掉部分Invoker。

代码@2:获取负载均衡扩展接口LoadBalance实现类对象,默认使用RandomLoadBalance,随机负载均衡策略。

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url,
    	Invocation invocation) throws RpcException;
}

代码@3:调用doInvoke()模板方法,传递筛选之后的Invoker集合、调用参数Invocation和负载均衡对象,交给子类FailoverClusterInvoker(默认实现)处理。FailoverClusterInvoker是一个Invoker,同时它又是一个负责处理容错策略的类,这里涉及到一个容错扩展接口Cluster。

@SPI(FailoverCluster.NAME)
public interface Cluster {
    @Adaptive
    <T> Invoker<T> join(Directory<T> directory) throws RpcException;
}

在【Dubbo服务引用】文章中我们提到过,服务引用时会通过cluster调用join()方法,将多个Invoker合并成一个Invoker。默认扩展接口Cluster的实现类是FailoverCluster,称为失败转移容错策略类,该类的join()方法通过Directory构建了FailoverClusterInvoker实例对象。

public class FailoverCluster implements Cluster {

    public final static String NAME = "failover";
    
    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new FailoverClusterInvoker<T>(directory);
    }
    
}

FailoverClusterInvoker的doList()方法,负责发起远程调用,并且处理调用失败后的处理情况,后面会详细介绍。

MockClusterInvoker的Invoker的实例对象是FailoverClusterInvoker,那么MockClusterInvoker又是怎么来的呢?其实就是在服务引用执行cluster.join()方法时,获取到目标扩展对象FailoverCluster,因为扩展接口存在包装类MockClusterWrapper,所以先执行了MockClusterWrapper的join()方法,在该方法里面初始化了MockClusterInvoker:

public class MockClusterWrapper implements Cluster {

    private Cluster cluster;

    public MockClusterWrapper(Cluster cluster) {
        this.cluster = cluster;
    }
    
    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new MockClusterInvoker<T>(directory,
                this.cluster.join(directory));
    }
}

Dubbo整个集群容错流程并不难,MockClusterInvoker负责处理正常流程、强制Mock流程和失败Mock流程三种情况。正常流程交给ClusterInvoker(多个实现类)执行,ClusterInvoker继承了AbstractClusterInvoker,总体的流程步骤都在AbstractClusterInvoker这个类,执行list()方法通过Directory和Router筛选Invoker,实例化负载均衡对象LoadBalance,最后交给ClusterInvoker发起远程调用并处理调用失败情况。

Dubbo整个集群容错涉及到了四个非常重要的接口:Directory、Router、Cluster和LoadBalance,下面我们分别对他们进行详细介绍。

Directory

通过上面的集群容错流程,我们知道通过Directory可以获取到所有可用的Invoker,获取Invoker集合之后再交给Router(路由)进一步路由筛选。
获取所有可用的Invoker在Directory实现类的doList()方法,这里我们主要介绍RegistryDirectory,进入RegistryDirectory的doList()方法:

@Override
public List<Invoker<T>> doList(Invocation invocation) {
    if (forbidden) {
        // 1. No service provider 2. Service providers are disabled
        throw new RpcException(RpcException.FORBIDDEN_EXCEPTION,
            "No provider available from registry " + getUrl().getAddress() + " 
            for service " + getConsumerUrl().getServiceKey() + 
            " on consumer " +  NetUtils.getLocalHost()
            + " use dubbo version " + Version.getVersion() + ", 
            + please check status of 
            + providers(disabled, not registered or in blacklist).");
    }
    List<Invoker<T>> invokers = null;
    Map<String, List<Invoker<T>>> localMethodInvokerMap 
    = this.methodInvokerMap;//@1 local reference
    if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
        String methodName = RpcUtils.getMethodName(invocation);
        Object[] args = RpcUtils.getArguments(invocation);
        if (args != null && args.length > 0 && args[0] != null
                && (args[0] instanceof String || args[0].getClass().isEnum())) {
            invokers = localMethodInvokerMap.get(methodName + "." + args[0]); // The routing can be enumerated according to the first parameter
        }
        if (invokers == null) {
            invokers = localMethodInvokerMap.get(methodName);
        }
        if (invokers == null) {
            invokers = localMethodInvokerMap.get(Constants.ANY_VALUE);
        }
        if (invokers == null) {
            Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator();
            if (iterator.hasNext()) {
                invokers = iterator.next();
            }
        }
    }
    return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers;
}

代码@1:该方法最重要的一行代码是Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap;,通过这行代码我们知道,RegistryDirectory是通过methodInvokerMap这个集合变量存储了服务接口和对应的Invoker信息。

// Map<methodName, Invoker> cache service method to invokers mapping.
private volatile Map<String, List<Invoker<T>>> methodInvokerMap;

那么methodInvokerMap又是什么时候被赋值的呢?答案就在RegistryDirectory的refreshInvoker(List<URL> invokerUrls)方法里:

private void refreshInvoker(List<URL> invokerUrls) {
	//省略部分代码......
	Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map
	Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // Change method name to map Invoker Map
	
	this.methodInvokerMap = multiGroup ? 
	toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
	
	try {
	    destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
	} catch (Exception e) {
	    logger.warn("destroyUnusedInvokers error. ", e);
	}
}

该方法首先执行toInvokers()toMethodInvokers(),将invokerUrls转换为Invoker集合,接着赋值给methodInvokerMap,最后执行destroyUnusedInvokers()关闭掉没有使用的Invoker。

refreshInvoker()方法是在notify()方法中被调用的,notify()方法在服务引用时会被调用(执行),还有接收到注册中心事件通知后也会被调用。

Router

路由分为条件路由、文件路由、脚本路由,对应dubbo-admin中三种不同的规则配置方式,条件路由是通过Dubbo定义的语法规则编写的路由规则,文件路由则是一个包含路由规则的文件,脚本路由则是使用JDK自身的脚本引擎解析路由规则脚本。下图是路由相关类的关系图。
在这里插入图片描述
我们主要介绍条件路由,条件路由可以在dubbo-admin管理台的路由规则菜单进行配置。

我们来做个实验,将提供者服务部署在两台机器上:192.168.0.205和192.168.0.149。提供者服务在服务接口实现中,通过InetAddress获取本机地址以便知道消息来自哪台服务器:

@Override
public List<UserAddress> getUserAddressList(String userId) {
	System.out.println("---------getUserAddressList-----------userId:"+userId);
	UserAddress address1 = new UserAddress(1, "北京天安门广场",
			userId, "Luke", "010-7984654", "Y");
	try {
		//获取ip地址
		address1.setIp(InetAddress.getLocalHost().getHostAddress());
	} catch (UnknownHostException e) {
		e.printStackTrace();
	}
	return Arrays.asList(address1);
}

消费者服务将负载均衡策略设置为轮询(轮询策略让消费者依次交替调用205和149机器上的提供者服务,后面会详细介绍),同时将结果打印出来:

<dubbo:reference id="userService"
 interface="com.luke.dubbo.api.service.UserService" 
 loadbalance="roundrobin"/>
for(int i = userIdInt; i < 1000; i++){
  List<UserAddress> userAddressList = userService.getUserAddressList(i+"");
  userAddressList.forEach((userAddress -> {
  	//答应ip地址
    System.out.println("RESPONSE FROM 【"+userAddress.getIp()+"】:address:"+
   userAddress.getUserAddress()+",userId:"+userAddress.getUserId());
  }));
  Thread.sleep(1_000);
}

启动两个提供者服务和一个消费者服务,观察消费者服务控制台,轮询调用服务提供者:

RESPONSE FROM 【192.168.0.205】:address:北京天安门广场,userId:751
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:752
RESPONSE FROM 【192.168.0.205】:address:北京天安门广场,userId:753
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:754
RESPONSE FROM 【192.168.0.205】:address:北京天安门广场,userId:755
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:756
RESPONSE FROM 【192.168.0.205】:address:北京天安门广场,userId:757
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:758
RESPONSE FROM 【192.168.0.205】:address:北京天安门广场,userId:759

在dubbo-admin配置路由规则,过滤192.168.0.205服务提供者:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
再次查看消费者控制台,结果全部来自192.168.0.149服务提供者:

RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:17
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:18
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:19
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:20
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:21
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:22
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:23
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:24
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:25
RESPONSE FROM 【192.168.0.149】:address:北京天安门广场,userId:26

路由规则在配置之后,会改变注册中心信息,注册中心回调消费者事件通知,执行RegistryDirectory.notify()方法,路由规则事件通知主要做两件事情:

1、刷新本地路由规则缓存

例如上面的例子中增加了一条路由规则,会在notify()方法中刷新本地路由规则缓存:

// routers
if (routerUrls != null && !routerUrls.isEmpty()) {
    List<Router> routers = toRouters(routerUrls);
    if (routers != null) { // null - do nothing
        setRouters(routers);
    }
}

路由规则以"route"协议开头:
在这里插入图片描述
在这里插入图片描述
2、通过路由规则刷新本地Invoker缓存methodInvokerMap

通知方法notify()最后执行refreshInvoker(invokerUrls)方法,在该方法里面会执行toMethodInvokers()方法,完成对Invoker的过滤,过滤之后更新newMethodInvokerMap。

private void refreshInvoker(List<URL> invokerUrls) {
	//省略部分代码......
	
	//获取所有Invoker,赋值给methodInvokerMap
    Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); 
    this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
}

toMethodInvokers()方法会调用route(invokersList, null),交给路由过滤处理,过滤掉205机器的服务提供者。

private Map<String, List<Invoker<T>>> toMethodInvokers(Map<String, Invoker<T>> invokersMap) {
	//省略部分代码......
    Map<String, List<Invoker<T>>> newMethodInvokerMap = new HashMap<String, List<Invoker<T>>>();
    //执行路由过滤
    List<Invoker<T>> newInvokersList = route(invokersList, null);
    newMethodInvokerMap.put(Constants.ANY_VALUE, newInvokersList);
    return Collections.unmodifiableMap(newMethodInvokerMap);
}
private List<Invoker<T>> route(List<Invoker<T>> invokers, String method) {
    Invocation invocation = new RpcInvocation(method, new Class<?>[0], new Object[0]);
    List<Router> routers = getRouters();
    if (routers != null) {
        for (Router router : routers) {
            if (router.getUrl() != null) {
                invokers = router.route(invokers, getConsumerUrl(), invocation);
            }
        }
    }
    return invokers;
}

所以当我们断点调试上面的集群容错调用,你会发现执行到AbstractDirectory的list()方法时,该行代码List<Invoker<T>> invokers = doList(invocation);获取的Invoker集合就已经是只有149服务提供者,因为doList()方法中就是从methodInvokerMap获取的Invoker集合,而methodInvokerMap在路由刷新的时候也已经跟着刷新了。

@Override
public List<Invoker<T>> list(Invocation invocation) throws RpcException {
    if (destroyed) {
        throw new RpcException("Directory already destroyed .url: " + getUrl());
    }
    List<Invoker<T>> invokers = doList(invocation);
    List<Router> localRouters = this.routers; // local reference
    if (localRouters != null && !localRouters.isEmpty()) {
        for (Router router : localRouters) {
            try {
                if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) {
                    invokers = router.route(invokers, getConsumerUrl(), invocation);
                }
            } catch (Throwable t) {
                logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
            }
        }
    }
    return invokers;
}

配置路由规则可以屏蔽某些提供者服务,还可以通过配置消费者黑白名单来屏蔽某些消费者服务,例如在访问控制菜单项添加配置205的消费者服务为黑名单,这样205的消费者服务远程调用时将会抛出RpcException异常。

在这里插入图片描述
在这里插入图片描述

Cluster

Cluster接口被称为容错接口(注意区分Cluster接口和Cluster层,Cluster层表示集群容错层,Cluster接口是Cluster层的一部分),Dubbo框架提供了多种集群容错策略:

Failover Cluster

失败自动切换,当出现失败,重试其它服务器 。通常用于读操作,但重试会带来更长延迟。可通过 retries="2"来设置重试次数(不含第一次)。

Failfast Cluster

快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

Failsafe Cluster

失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

Failback Cluster

失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

Forking Cluster

并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2"来设置最大并行数。

Broadcast Cluster

广播调用所有提供者,逐个调用,任意一台报错则报错 [2]。通常用于通知所有提供者更新缓存或日志等本地资源信息。

集群模式配置

按照以下示例在服务提供方和消费方配置集群模式

<dubbo:service cluster="failsafe" /><dubbo:reference cluster="failsafe" />

我们重点介绍前面两种容错策略Failover Cluster和Failfast Cluster。

Failover Cluster

public class FailoverCluster implements Cluster {

    public final static String NAME = "failover";

    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new FailoverClusterInvoker<T>(directory);
    }

}

FailoverCluster在join()方法构造了FailoverClusterInvoker实例对象,FailoverClusterInvoker继承了AbstractClusterInvoker,AbstractClusterInvokerto在invoke()方法最后调用模板方法doInvoke()方法,交给子类去执行。

@Override
public Result invoke(final Invocation invocation) throws RpcException {
    checkWhetherDestroyed();
    LoadBalance loadbalance = null;
    List<Invoker<T>> invokers = list(invocation);
    if (invokers != null && !invokers.isEmpty()) {
        loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
                .getMethodParameter(invocation.getMethodName(), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
    }
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    return doInvoke(invocation, invokers, loadbalance);//默认方法,交给子类处理
}

@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    List<Invoker<T>> copyinvokers = invokers;
    checkInvokers(copyinvokers, invocation);
    int len = getUrl().getMethodParameter(invocation.getMethodName(), 
    Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;//@1
    if (len <= 0) {
        len = 1;
    }
    // retry loop.
    RpcException le = null; // last exception.
    List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
    Set<String> providers = new HashSet<String>(len);
    for (int i = 0; i < len; i++) {
        //Reselect before retry to avoid a change of candidate `invokers`.
        //NOTE: if `invokers` changed, then `invoked` also lose accuracy.
        if (i > 0) {//@2
            checkWhetherDestroyed();
            copyinvokers = list(invocation);
            // check again
            checkInvokers(copyinvokers, invocation);
        }
        Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);//@3
        invoked.add(invoker);
        RpcContext.getContext().setInvokers((List) invoked);
        try {
            Result result = invoker.invoke(invocation);//@4
            if (le != null && logger.isWarnEnabled()) {
                logger.warn("Although retry the method " + invocation.getMethodName()
                        + " in the service " + getInterface().getName()
                        + " was successful by the provider " + invoker.getUrl().getAddress()
                        + ", but there have been failed providers " + providers
                        + " (" + providers.size() + "/" + copyinvokers.size()
                        + ") from the registry " + directory.getUrl().getAddress()
                        + " on the consumer " + NetUtils.getLocalHost()
                        + " using the dubbo version " + Version.getVersion() + ". Last error is: "
                        + le.getMessage(), le);//@5
            }
            return result;
        } catch (RpcException e) {//@6
            if (e.isBiz()) { // biz exception.
                throw e;
            }
            le = e;
        } catch (Throwable e) {
            le = new RpcException(e.getMessage(), e);
        } finally {
            providers.add(invoker.getUrl().getAddress());
        }
    }
    throw new RpcException(le != null ? le.getCode() : 0, "Failed to invoke the method "
            + invocation.getMethodName() + " in the service " + getInterface().getName()
            + ". Tried " + len + " times of the providers " + providers
            + " (" + providers.size() + "/" + copyinvokers.size()
            + ") from the registry " + directory.getUrl().getAddress()
            + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
            + Version.getVersion() + ". Last error is: "
            + (le != null ? le.getMessage() : ""), le != null && le.getCause() != null ? 
            + le.getCause() : le);//@7
}

代码@1:获取调用失败最大重试次数,默认是三次。

代码@2:当i>0的时候说明已经发生了调用失败,调用失败后的重试操作都需要调用list()方法重新获取Invoker集合。

代码@3:通过负载均衡器LoadBalance从Invoker集合里面选出一个Invoker。

代码@4:发起远程调用,这块会在后面的文章中详细介绍。

代码@5:le用来存储最近一次调用失败的异常信息,如果最后调用成功但le不为空,说明发生过重复调用,输出警告日志。

代码@6:捕获远程调用抛出的RpcException异常,如果是业务异常则直接抛出,否则赋值给le。

代码@7:如果执行超过最大重试次数仍然失败的话,就向外抛出调用失败异常信息。

Failfast Cluster

public class FailfastCluster implements Cluster {

    public final static String NAME = "failfast";

    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new FailfastClusterInvoker<T>(directory);
    }
}

FailfastCluster在join()方法则是返回FailfastClusterInvoker实例对象。

public class FailfastClusterInvoker<T> extends AbstractClusterInvoker<T> {

    public FailfastClusterInvoker(Directory<T> directory) {
        super(directory);
    }

    @Override
    public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        checkInvokers(invokers, invocation);
        Invoker<T> invoker = select(loadbalance, invocation, invokers, null);//@1
        try {
            return invoker.invoke(invocation);//@2
        } catch (Throwable e) {//@3
            if (e instanceof RpcException && ((RpcException) e).isBiz()) { // biz exception.
                throw (RpcException) e;
            }
            throw new RpcException(e instanceof RpcException ? 
            ((RpcException) e).getCode() : 0, "Failfast invoke providers " + 
            invoker.getUrl() + " " + 
            loadbalance.getClass().getSimpleName() + " select from all providers " + 
            invokers
             + " for service " + getInterface().getName() + " method " + 
             + invocation.getMethodName() + " on consumer " + NetUtils.getLocalHost() 
             + + " use dubbo version 
             + " + Version.getVersion() + ", 
             + but no luck to perform the invocation. Last error is: "  
             + e.getMessage(), e.getCause() != null ? e.getCause() : e);
        }
    }
}

代码@1:通过负载均衡器从Invoker集合里面选出一个Invoker。

代码@2:发起远程调用,FailfastClusterInvoker和FailoverClusterInvoker最大的区别就是前者远程调用失败后,不再执行重试操作。

代码@3:只执行一次调用,调用失败就直接向外抛出异常信息。

LoadBalance

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, 
    		Invocation invocation) throws RpcException;
}

Dubbo框架提供了多种负载均衡策略(默认使用随机策略):

Random LoadBalance

随机,按权重设置随机概率。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。

RoundRobin LoadBalance

轮询,按公约后的权重设置轮询比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

LeastActive LoadBalance

最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。

ConsistentHash LoadBalance

一致性 Hash,相同参数的请求总是发到同一提供者。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

配置方式

1、服务端服务级别:

<dubbo:service interface="..." loadbalance="roundrobin" />

2、客户端服务级别:

<dubbo:reference interface="..." loadbalance="roundrobin" />

3、服务端方法级别:

<dubbo:service interface="...">
    <dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>

4、客户端方法级别

<dubbo:reference interface="...">
    <dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>

我们重点介绍前面两种:Random LoadBalance和RoundRobin LoadBalance。

RandomLoadBalance

@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    //Invoker的数量
    int length = invokers.size(); // Number of invokers
    //全部Invoker合起来的权重
    int totalWeight = 0; // The sum of weights
	//各个Invoker权重是否一样
    boolean sameWeight = true; // Every invoker has the same weight?
    for (int i = 0; i < length; i++) {
        int weight = getWeight(invokers.get(i), invocation);//@1
        totalWeight += weight; // Sum
        if (sameWeight && i > 0
                && weight != getWeight(invokers.get(i - 1), invocation)) {//@2
            sameWeight = false;
        }
    }
    if (totalWeight > 0 && !sameWeight) {//@3
        // If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
        int offset = random.nextInt(totalWeight);//@4
        // Return a invoker based on the random value.
        for (int i = 0; i < length; i++) {
            offset -= getWeight(invokers.get(i), invocation);//@5
            if (offset < 0) {
                return invokers.get(i);
            }
        }
    }
    // If all invokers have the same weight value or totalWeight=0, return evenly.
    return invokers.get(random.nextInt(length));//@6
}

随机负载均衡策略是通过权重来实现的。

代码@1:通过getWeight()方法获取Invoker所占权重,默认获取到的权重是100,后面会详细介绍该方法。

代码@2:totalWeight存储累加所有Invoker的权重值总和,sameWeight用于判断是否所有的Invoker权重都是相同的,通过将当前的Invoker权重和Invoker集合的前一个Invoker权重做比较,如果不一样,那么sameWeight赋值为false。

代码@3:当权重值大于零,且各个Invoker的权重值不完全相同时执行if逻辑,否则执行代码@6。

代码@4:通过Random获取0到totalWeight-1之间的一个值,赋值给offset。

代码@5:for循环依次获取集合里的每个Invoker,再次执行getWeight()获取Invoker的权重,接着执行offset = offset - getWeigth()运算。如果offset为负值,那么该Invoker将被选中,否则继续下一个。我们可以举一个例子来帮助大家更好理解:
在这里插入图片描述
首先假设现在有三个Invoker,获取的总权重是200,其中Invoker1是100,剩余两个各50,那么Invoker1将占0-99,Invoker2将占100-149,Invoker2将占150-199,此时代码@4计算出来的随机数offset的范围就是0-199之间,如果offset算出是0-99之间的一个数,那么执行offset = offset -getWeigth()后offset的值将小于零,选中了Invoker1,如果offset是100,那么将选中Invoker2,后面以此类推。

代码@6:如果每个Invoker的权重都是一样的,那么将随机返回Invoker集合中的一个。

下面我们来详细介绍一下getWeight()方法:

protected int getWeight(Invoker<?> invoker, Invocation invocation) {
    int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(),
     Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);//@1
    if (weight > 0) {
        long timestamp = invoker.getUrl().
        getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);//@2
        if (timestamp > 0L) {
            int uptime = (int) (System.currentTimeMillis() - timestamp);//@3
            int warmup = invoker.getUrl().
            getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);//@4
            if (uptime > 0 && uptime < warmup) {
                weight = calculateWarmupWeight(uptime, warmup, weight);//@5
            }
        }
    }
    return weight;
}

代码@1:获取默认权重,Constants.DEFAULT_WEIGHT = 100。

代码@2:获取提供者服务的启动时间。

代码@3:获取提供者服务自启动后到现在过去的时间(即运行时长),赋值给uptime。

代码@4:获取默认的唤醒时间Constants.DEFAULT_WARMUP=60000(即10分钟),赋值给warmup。

代码@5:如果uptime < warmup,即提供者服务自启动后的运行时长还不满足10分钟的时候,将执行calculateWarmupWeight()去计算预热权重,计算的结果值在100以内,预热权重值随运行的时长增加而增大。Dubbo框架认为提供者服务启动后运行时长在10分钟以内时,不应该给予100%的权重,而是先通过一个预热阶段,不断增加它的权重,10分钟后才分配给他和其他提供者服务相同的权重。

RoundRobinLoadBalance

@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
    int length = invokers.size(); // Number of invokers
    int maxWeight = 0; // The maximum weight
    int minWeight = Integer.MAX_VALUE; // The minimum weight
    final LinkedHashMap<Invoker<T>, IntegerWrapper> invokerToWeightMap = new LinkedHashMap<Invoker<T>, IntegerWrapper>();
    int weightSum = 0;
    for (int i = 0; i < length; i++) {
        int weight = getWeight(invokers.get(i), invocation);//@1
        maxWeight = Math.max(maxWeight, weight); // Choose the maximum weight
        minWeight = Math.min(minWeight, weight); // Choose the minimum weight
        if (weight > 0) {
            invokerToWeightMap.put(invokers.get(i), new IntegerWrapper(weight));//@2
            weightSum += weight;//@3
        }
    }
    AtomicPositiveInteger sequence = sequences.get(key);//@4
    if (sequence == null) {
        sequences.putIfAbsent(key, new AtomicPositiveInteger());
        sequence = sequences.get(key);
    }
    int currentSequence = sequence.getAndIncrement();
    if (maxWeight > 0 && minWeight < maxWeight) {//@5
        /**将请求次数对权重总和取模*/
        int mod = currentSequence % weightSum;
        for (int i = 0; i < maxWeight; i++) {
            for (Map.Entry<Invoker<T>, IntegerWrapper> each : invokerToWeightMap.entrySet()) {
                final Invoker<T> k = each.getKey();
                final IntegerWrapper v = each.getValue();//取出Invoker权重
                if (mod == 0 && v.getValue() > 0) {//如果mod为0且权重值大于0,则返回
                    return k;
                }
                if (v.getValue() > 0) {//权重值大于0,权重值减1,模式mod也减1。
                    v.decrement();
                    mod--;
                }
            }
        }
    }
    // Round robin
    return invokers.get(currentSequence % length);//@6
}

代码@1:获取Invoker权重。

代码@2:将每一个Invoker和对应的权重存储到invokerToWeightMap。

代码@3:累加每个Invoker权重,存储到weightSum。

代码@4:使用原子变量存储接口和对应的累计请求次数。

代码@5:如果各个Invoker权重并不完全相同,那么将进行权重比较计算。这部分代码稍复杂,我们同样通过举一个例子来说明,假如现在有三个提供者服务的Invoker,servers = [A, B, C],对应的权重为 weights = [2, 5, 1]。那么随着调用次数currentSequence的累加,mod的值始终是保持在0-7之间反复循环,那么选中的Invoker将如下:

mod A计算后权重 B计算后权重 C计算后权重 选中的Invoker
0 2 5 1 A
1 1 5 1 B
2 1 4 1 C
3 1 4 0 A
4 0 4 0 B
5 0 3 0 B
6 0 2 0 B
7 0 1 0 B

所以按照三个Invoker权重的大小,在每8次请求内,A将被选中两次,B将被选中5次,C则被选中1次,可见选中概率和权重大小保持一致。

代码@6:如果每个Invoker权重都一样则直接通过取模获取对应Invoker。

可以通过dubbo-admin调节权重分配
在这里插入图片描述

服务降级

服务降级是指临时屏蔽某些服务,并自定义这些服务接口的返回策略。通常这些服务是系统的非关键服务,不会影响到核心业务。例如一些系统在某些时段会突然出现业务流量激增的情况(比如双11),这时候可以通过服务降级,临时屏蔽一些不重要的服务,腾出资源给关键服务,让系统平稳度过这段流量激增时期。服务降级可以在dubbo-admin管理台配置,也可以通过代码指定。

服务降级类型

  • mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。

在这里插入图片描述

  • mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。

在这里插入图片描述
源码解读服务降级

查看MockClusterInvoker的invoke()方法,一旦配置了mock,那么将不走no mock逻辑:

@Override
public Result invoke(Invocation invocation) throws RpcException {
    Result result = null;

    String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), 
    Constants.MOCK_KEY, 
    Boolean.FALSE.toString()).trim();
    if (value.length() == 0 || value.equalsIgnoreCase("false")) {
        //no mock
        result = this.invoker.invoke(invocation);
    } else if (value.startsWith("force")) {
        if (logger.isWarnEnabled()) {
            logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl());
        }
        //force:direct mock
        result = doMockInvoke(invocation, null);//@1
    } else {
        //fail-mock
        try {
            result = this.invoker.invoke(invocation);//@2
        } catch (RpcException e) {//@3
            if (e.isBiz()) {
                throw e;
            } else {
                if (logger.isWarnEnabled()) {
                    logger.warn("fail-mock: " + invocation.getMethodName() +
                     " fail-mock enabled , url : " + directory.getUrl(), e);
                }
                result = doMockInvoke(invocation, e);
            }
        }
    }
    return result;
}

代码@1:如果是强制Mock,即屏蔽服务,直接执行doMockInvoke(invocation, null)方法。

代码@2:如果是失败Mock,那么仍然会执行一次正常调用。

代码@3:捕获RpcException异常,如果出现业务异常那么直接向外抛出,否则输出警告日志,接着执行doMockInvoke(invocation, null)方法。

进入doMockInvoke(invocation, null)方法:

private Result doMockInvoke(Invocation invocation, RpcException e) {
    Result result = null;
    Invoker<T> minvoker;

    List<Invoker<T>> mockInvokers = selectMockInvoker(invocation);//@1
    if (mockInvokers == null || mockInvokers.isEmpty()) {
        minvoker = (Invoker<T>) new MockInvoker(directory.getUrl());//@2
    } else {
        minvoker = mockInvokers.get(0);//@3
    }
    try {
        result = minvoker.invoke(invocation);//@4
    } catch (RpcException me) {
        if (me.isBiz()) {
            result = new RpcResult(me.getCause());
        } else {
            throw new RpcException(me.getCode(), 
            getMockExceptionMessage(e, me), me.getCause());
        }
    } catch (Throwable me) {
        throw new RpcException(getMockExceptionMessage(e, me), me.getCause());
    }
    return result;
}

代码@1:获取Mock类型Invoker集合,下面会详细介绍。

代码@2:如果mockInvokers为空,那么将实例化MockInvoker对象并返回,否则执行代码@3。

代码@3:获取第一个Mock类型Invoker,赋值给minvoker。

代码@4:通过minvoker执行invoke()调用,并且捕获RpcException,如果是业务异常则包装成RpcResult对象返回,否则抛出RpcException异常。

private List<Invoker<T>> selectMockInvoker(Invocation invocation) {
    List<Invoker<T>> invokers = null;
    //TODO generic invoker?
    if (invocation instanceof RpcInvocation) {
    	//Constants.INVOCATION_NEED_MOCK = "invocation.need.mock"
        ((RpcInvocation) invocation).setAttachment(Constants.INVOCATION_NEED_MOCK,
         Boolean.TRUE.toString());//@1
        try {
            invokers = directory.list(invocation);//@2
        } catch (RpcException e) {
            if (logger.isInfoEnabled()) {
                logger.info("Exception when try to invoke mock. 
                Get mock invokers error for service:"
                        + directory.getUrl().getServiceInterface() + ", 
                        + method:" + invocation.getMethodName()
                        + ", will contruct a new mock with 'new MockInvoker()'.", e);
            }
        }
    }
    return invokers;
}

代码@1:主动让Invocation对象添加参数项invocation.need.mock=true

代码@2:交给RegistryDirectory的list()方法获取Mock类型的Invoker。

@Override
public List<Invoker<T>> list(Invocation invocation) throws RpcException {
    if (destroyed) {
        throw new RpcException("Directory already destroyed .url: " + getUrl());
    }
    List<Invoker<T>> invokers = doList(invocation);//@1
    List<Router> localRouters = this.routers; // local reference
    if (localRouters != null && !localRouters.isEmpty()) {
        for (Router router : localRouters) {
            try {
                if (router.getUrl() == null || 
                router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) {
                    invokers = router.route(invokers, getConsumerUrl(), invocation);//@2
                }
            } catch (Throwable t) {
                logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
            }
        }
    }
    return invokers;
}

代码@1:获取Invoker集合,作者这里使用前面的例子,所以获取到149和205两个Invoker。

代码@2:router的实例对象是MockInvokersSelector,执行路由过滤route()

@Override
public <T> List<Invoker<T>> route(final List<Invoker<T>> invokers,
                                  URL url, final Invocation invocation) throws RpcException {
    if (invocation.getAttachments() == null) {
        return getNormalInvokers(invokers);
    } else {
        String value = invocation.getAttachments().get(Constants.INVOCATION_NEED_MOCK);
        if (value == null)
            return getNormalInvokers(invokers);
        else if (Boolean.TRUE.toString().equalsIgnoreCase(value)) {
            return getMockedInvokers(invokers);
        }
    }
    return invokers;
}

该方法根据Constants.INVOCATION_NEED_MOCK选择调用getNormalInvokers()或者getMockedInvokers()

  • getNormalInvokers():获取所有非mock协议的Invoker。

  • getMockedInvokers():获取所有mock协议的Invoker。

作者通过dubbo-admin配置了mock,所以没有获取到mock协议的Invoker,这样149和205的两个Invoker都将被过滤掉。

回到MockClusterInvoker的doMockInvoke()方法,这样selectMockInvoker(invocation)就获取为空,那么将会创建MockInvoker实例对象,执行minvoker.invoke(invocation),进入MockInvoker的invoke()方法:

@Override
public Result invoke(Invocation invocation) throws RpcException {
    String mock = getUrl().getParameter(invocation.getMethodName() + "." + 
    Constants.MOCK_KEY);//@1
    if (invocation instanceof RpcInvocation) {
        ((RpcInvocation) invocation).setInvoker(this);
    }
    if (StringUtils.isBlank(mock)) {
        mock = getUrl().getParameter(Constants.MOCK_KEY);//@2
    }

    if (StringUtils.isBlank(mock)) {
        throw new RpcException(new IllegalAccessException("mock can not be null. url :" + url));
    }
    mock = normallizeMock(URL.decode(mock));//@3
    if (Constants.RETURN_PREFIX.trim().equalsIgnoreCase(mock.trim())) {//@4
        RpcResult result = new RpcResult();
        result.setValue(null);
        return result;
    } else if (mock.startsWith(Constants.RETURN_PREFIX)) {//@5
        mock = mock.substring(Constants.RETURN_PREFIX.length()).trim();
        mock = mock.replace('`', '"');
        try {
            Type[] returnTypes = RpcUtils.getReturnTypes(invocation);
            Object value = parseMockValue(mock, returnTypes);
            return new RpcResult(value);
        } catch (Exception ew) {
            throw new RpcException("mock return invoke error. method :" + invocation.getMethodName() + ", mock:" + mock + ", url: " + url, ew);
        }
    } else if (mock.startsWith(Constants.THROW_PREFIX)) {//@6
        mock = mock.substring(Constants.THROW_PREFIX.length()).trim();
        mock = mock.replace('`', '"');
        if (StringUtils.isBlank(mock)) {
            throw new RpcException(" mocked exception for Service degradation. ");
        } else { // user customized class
            Throwable t = getThrowable(mock);
            throw new RpcException(RpcException.BIZ_EXCEPTION, t);
        }
    } else { //impl mock
        try {
            Invoker<T> invoker = getInvoker(mock);//@7
            return invoker.invoke(invocation);
        } catch (Throwable t) {
            throw new RpcException("Failed to create mock implemention class " + mock, t);
        }
    }
}

代码@1:获取方法名+Constants.MOCK_KEY参数值赋值给mock。

代码@2:如果代码@1获取为空,从URL地址获取Constants.MOCK_KEY参数值。

代码@3:解析出mock的值,比如“return null”。

代码@4:如果mock的值为“return”,那么构建RpcResult对象返回。

代码@5:如果mock的值以“return”开头,那么解析后面的返回值类型,构建RpcResult对象返回。

代码@6:如果mock的值以“throw”开头,那么抛出RpcException异常。

代码@7:获取mock类型Invoker,执行invoke()调用。

那么如何才能有mock协议的Invoker呢?答案是通过自定义服务接口的Mock实现类:

public class UserServiceMock implements UserService {

    @Override
    public List<UserAddress> getUserAddressList(String userId) {
        System.out.println("返回容错数据");
        return new ArrayList<>();
    }
}
<dubbo:reference id="userService" interface="com.luke.dubbo.api.service.UserService"
					 loadbalance="roundrobin" cluster="failfast" 
					 mock="com.luke.dubbo.order.service.mock.UserServiceMock"/>

消费者服务在<dubbo:reference>标签通过mock属性指定服务接口的Mock实现类。

自定义了Mock类之后,消费者服务调用时将执行到MockInvoker.invke()方法时,将执行代码@7逻辑,获取一个Mock类的Invoker,进入MockInvoker的getInvoker()方法:

@SuppressWarnings("unchecked")
private Invoker<T> getInvoker(String mockService) {
    Invoker<T> invoker = (Invoker<T>) mocks.get(mockService);//@1
    if (invoker != null) {
        return invoker;
    } else {
        Class<T> serviceType = (Class<T>) ReflectUtils.
        forName(url.getServiceInterface());//@2
        if (ConfigUtils.isDefault(mockService)) {
            mockService = serviceType.getName() + "Mock";
        }

        Class<?> mockClass = ReflectUtils.forName(mockService);//@3
        if (!serviceType.isAssignableFrom(mockClass)) {
            throw new IllegalArgumentException("The mock implemention class " + mockClass.getName() + " not implement interface " + serviceType.getName());
        }

        if (!serviceType.isAssignableFrom(mockClass)) {
            throw new IllegalArgumentException("The mock implemention class " + mockClass.getName() + " not implement interface " + serviceType.getName());
        }
        try {
            T mockObject = (T) mockClass.newInstance();//@4
            invoker = proxyFactory.getInvoker(mockObject, (Class<T>) serviceType, url);
            if (mocks.size() < 10000) {
                mocks.put(mockService, invoker);//@5
            }
            return invoker;
        } catch (InstantiationException e) {
            throw new IllegalStateException("No such empty constructor \"public " + mockClass.getSimpleName() + "()\" in mock implemention class " + mockClass.getName(), e);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }
}

代码@1:从缓存mocks获取该服务接口的mock类型Invoker。

代码@2:获取服务接口Class。

代码@3:获取Mock实现类的Class。

代码@4:实例化Mock实现类对象。

代码@5:通过代理工厂利用mockObject,serviceType和url构建Invoker实例对象。

所以消费者服务调用UserService接口服务时,一旦远程调用发生RpcException异常,将会执行UserServiceMock,返回自定义Mock结果。

关注公众号了解更多原创博文

Alt

发布了122 篇原创文章 · 获赞 127 · 访问量 93万+

猜你喜欢

转载自blog.csdn.net/u010739551/article/details/104606770