码恋 码恋

ALL YOUR SMILES, ALL MY LIFE.

目录
Dubbo系列笔记之特殊示例用法
/  

Dubbo系列笔记之特殊示例用法

注:本文是对 Dubbo 官方文档的示例部分整理而成。

一、引言

日常使用 Dubbo 一般来说仅仅使用了其服务远程调用的最基础功能,其他的额外功能例如参数验证、结果缓存、异步、回调等都需要各服务自己去处理。

其实在使用 Dubbo 的时候,除了 RPC ,Dubbo 还为我们提供了很多的增强功能和特性。学习技术是一个 怎么用->会用->用好->不够用自己加 的一个过程。在会用的基础上,逐渐走向用好。用好的前提一部分除了对 Dubbo 本身的特殊功能的了解使用,还有其底层的工作逻辑也是一部分。So,在对 Dubbo 的工作逻辑进行深入了解前,在本篇,先来看看 Dubbo 为我们提供了哪些额外福利吧?

二、调优扩展类

  • 启动时检查

    • 说明:
      • 默认情况,Dubbo 在启动时会检查依赖的服务是否正常可用,如果不可用时抛出异常,阻止 Spring 容器的初始化过程,以便在上线前及时发现问题。
      • 在消费者中可以对 没有服务提供者 进行检查;在注册中心中可以对 注册订阅失败 操作时进行检查。
    • 方式:
      通过 check="true/false" 指定是否开启启动检查。

    详细参考:《Dubbo示例-启动检查》

  • 集群容错

    • 说明:

      • 一个服务可能有多个提供者,这些提供者构成服务集群。如果被调用的服务提供者服务挂掉或异常时,可以根据需求通过设置集群的容错模式来适应不同的错误处理方式。
      • Dubbo 提供了多种容错模式,默认为 Failover Cluster 模式,即通过重试的方式来进行处理,可以通过 retries="2" 来设置重试次数。
    • 方式:
      通过 cluster="failsafe" 指定集群容错模式。

    详细参考:《Dubbo示例-集群容错》

  • 负载均衡

    • 说明:

      • Dubbo 提供了多种负载均衡策略,为消费者实现了软负载均衡。
    • 方式:
      通过loadbalance="roundrobin"指定负载均衡策略。

    详细参考:《Dubbo示例-负载均衡》

  • 线程模型

    • 说明:
      • 如果事件处理的逻辑能迅速完成,并且不会发起新的 IO 请求,比如只是在内存中记个标识,则直接在 IO 线程上处理更快,因为减少了线程池调度。
      • 但如果事件处理逻辑较慢,或者需要发起新的 IO 请求,比如需要查询数据库,则必须派发到线程池,否则 IO 线程阻塞,将导致不能接收其它请求。
      • Dubbo 提供了不同的派发策略和不同的线程池配置来应对不同的场景。
    • 方式:
      通过dispatcher="all"threadpool="fixed" 分别来设置不同的分发策略和线程池配置。

    详细参考:《Dubbo示例-线程模型》

  • 多协议

    • 说明:
      Dubbo 允许配置多协议,在不同服务上支持不同协议或者同一服务上同时支持多种协议。

      • 不同服务在性能上适用不同协议进行传输,比如大数据用短连接协议,小数据大并发用长连接协议。
      • 需要与 http 客户端互操作。
    • 方式:
      配置多个协议,不同服务可以通过protocol="dubbo,hessian"的方式来指定暴露服务的协议。

    详细参考:《Dubbo示例-多协议》

  • 多注册中心

    • 说明:
      Dubbo 支持同一服务向多注册中心同时注册,或者不同服务分别注册到不同的注册中心上去,甚至可以同时引用注册在不同注册中心上的同名服务。
    • 方式:
      可以配置多个注册中心,然后通过 registry="xxxRegistry"的方式制定注册中心。

    详细参考:《Dubbo示例-多注册中心》

  • 服务分组

    • 说明:
      一个接口有不同的实现,可以通过指定 group 来进行区分。
    • 方式:
      通过group="member"区分不同实现。

    详细参考:《Dubbo示例-服务分组》

  • 多版本

    • 说明:
      当一个接口出现不兼容的升级时,可用通过版本号的方式来解决。
    • 方式:
      不同版本的服务通过version="1.0.0"来指定不同的版本号,版本号不同的服务之间相互不引用。

    详细参考:《Dubbo示例-多版本》

  • 分组聚合

    • 说明:
      当一个服务有多个分组时,可以对调用结果进行聚合。
    • 方式:
      <dubbo:reference interface="com.xxx.MenuService" group="*" merger="true" />

    详细参考:《Dubbo示例-分组聚合》

  • 参数验证

  • 结果缓存

    • 说明:
      用于加速热门数据的访问速度,Dubbo 提供声明式缓存,以减少用户加缓存的工作量。
    • 方式:
      通过指定服务调用者的 cache="lru" 来选择缓存的策略。

    详细参考:《Dubbo示例-结果缓存》

  • 上下文信息:

    • 说明:
      Dubbo 使用 ThreadLocal 维护了一个名为 RpcContext 的上下文信息。
    • 方式:
      可以通过RpcContext获得服务的上下文信息。

    详细参考:《Dubbo示例-上下文信息》

  • 隐式参数

    • 说明:
      可以通过 Dubbo 的上下文对象设置隐式参数,后面的远程调用都会隐式将这些参数发送到服务器端。
    • 方式:
      在服务消费方端设置隐式参数

    setAttachment 设置的 KV 对,在完成下面一次远程调用会被清空,即多次远程调用要多次设置。

    RpcContext.getContext().setAttachment("index", "1"); // 隐式传参,后面的远程调用都会隐式将这些参数发送到服务器端,类似cookie,用于框架集成,不建议常规业务使用
    xxxService.xxx(); // 远程调用
    // ...
    

    在服务提供方端获取隐式参数

    public class XxxServiceImpl implements XxxService {
    
      public void xxx() {
        	// 获取客户端隐式传入的参数,用于框架集成,不建议常规业务使用
       	 String index = RpcContext.getContext().getAttachment("index"); 
     	}
    }
    

    详细参考:《Dubbo示例-隐式参数》

  • Consumer 异步调用

    • 说明:

      • Dubbo 支持异步编程,从v2.7.0开始,Dubbo的所有异步编程接口开始以CompletableFuture为基础。
      • 基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
    • 方式:
      需要服务提供者事先定义CompletableFuture签名的服务,具体参见服务端异步执行接口定义:

    public interface AsyncService {
        CompletableFuture<String> sayHello(String name);
    }
    

    注意接口的返回类型是CompletableFuture<String>

    XML引用服务:

    <dubbo:reference id="asyncService" timeout="10000" interface="com.alibaba.dubbo.samples.async.api.AsyncService"/>
    

    调用远程服务:

    // 调用直接返回CompletableFuture
    CompletableFuture<String> future = asyncService.sayHello("async call request");
    // 增加回调
    future.whenComplete((v, t) -> {
        if (t != null) {
            t.printStackTrace();
        } else {
            System.out.println("Response: " + v);
        }
    });
    // 早于结果输出
    System.out.println("Executed before response return.");
    

    详细参考:Dubbo示例-Consumer 异步调用》

  • Provider异步执行

    • 说明:
      Provider端异步执行将阻塞的业务从 Dubbo 内部线程池切换到业务自定义线程,避免Dubbo线程池的过度占用,有助于避免不同服务间的互相影响。异步执行无益于节省资源或提升RPC响应性能,因为如果业务执行需要阻塞,则始终还是要有线程来负责执行。
    • 方式:

    定义CompletableFuture签名的接口

    服务接口定义:

    public interface AsyncService {
        CompletableFuture<String> sayHello(String name);
    }
    

    服务实现:

    public class AsyncServiceImpl implements AsyncService {
       @Override
        public CompletableFuture<String> sayHello(String name) {
            RpcContext savedContext = RpcContext.getContext();
            // 建议为supplyAsync提供自定义线程池,避免使用JDK公用线程池
            return CompletableFuture.supplyAsync(() -> {
                System.out.println(savedContext.getAttachment("consumer-key1"));
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "async response from provider.";
            });
        }
    }
    

    通过return CompletableFuture.supplyAsync(),业务执行已从Dubbo线程切换到业务线程,避免了对Dubbo线程池的阻塞。

    使用AsyncContext

    Dubbo提供了一个类似Serverlet 3.0的异步接口AsyncContext,在没有CompletableFuture签名接口的情况下,也可以实现Provider端的异步执行。

    服务接口定义:

    public interface AsyncService {
        String sayHello(String name);
    }
    

    服务暴露,和普通服务完全一致:

    <bean id="asyncService" class="org.apache.dubbo.samples.governance.impl.AsyncServiceImpl"/>
    <dubbo:service interface="org.apache.dubbo.samples.governance.api.AsyncService" ref="asyncService"/>
    

    服务实现:

    public class AsyncServiceImpl implements AsyncService {
        public String sayHello(String name) {
            final AsyncContext asyncContext = RpcContext.startAsync();
            new Thread(() -> {
                // 如果要使用上下文,则必须要放在第一句执行
                asyncContext.signalContextSwitch();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 写回响应
                asyncContext.write("Hello " + name + ", response from provider.");
            }).start();
            return null;
        }
    }
    

    详细参考:《Dubbo示例-Provider异步执行》

  • 本地调用

    • 说明:
      • 本地调用使用了 injvm 协议,是一个伪协议,它不开启端口,不发起远程调用,只在 JVM 内直接关联,但执行 Dubbo 的 Filter 链。
      • 2.2.0 开始,每个服务默认都会在本地暴露。在引用服务的时候,默认优先引用本地服务。如果希望引用远程服务可以使用一下配置强制引用远程服务。
    <dubbo:reference ... scope="remote" />
    
    • 方式:
      可以通过设置服务的协议为 injvm 来实现本地调用。

    详细参考:《Dubbo示例-本地调用》

  • 参数回调

    • 说明:
      Dubbo 将基于长连接生成反向代理,这样就可以从服务器端调用客户端逻辑。
    • 方式:
      由服务提供者自行实现 Listener ,并添加相关配置:
    bean id="callbackService" class="com.callback.impl.CallbackServiceImpl" />
    <dubbo:service interface="com.callback.CallbackService" ref="callbackService" connections="1" callbacks="1000">
        <dubbo:method name="addListener">
            <dubbo:argument index="1" callback="true" />
            <!--也可以通过指定类型的方式-->
            <!--<dubbo:argument type="com.demo.CallbackListener" callback="true" />-->
        </dubbo:method>
    </dubbo:service>
    

    详细参考:《Dubbo示例-参数回调》

  • 事件通知

    • 说明:
      在调用之前、调用之后、出现异常时,会触发 oninvokeonreturnonthrow 三个事件,可以配置当事件发生时,通知哪个类的哪个方法。
    • 方式:
      自定义Callback 接口及实现,并在消费者中配置相应的事件处理,onreturn = "demoCallback.onreturn" 。
    <bean id ="demoCallback" class = "org.apache.dubbo.callback.implicit.NofifyImpl" />
    <dubbo:reference id="demoService" interface="org.apache.dubbo.callback.implicit.IDemoService" version="1.0.0" group="cn" >
          <dubbo:method name="get" async="true" onreturn = "demoCallback.onreturn" onthrow="demoCallback.onthrow" />
    </dubbo:reference>
    

    详细参考:《Dubbo示例-事件通知》

  • 本地存根

    • 说明:
      远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,比如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。

    图片.png

    • 方式:
      在 spring 配置文件中按以下方式配置:
    <dubbo:service interface="com.foo.BarService" stub="true" />
    

    <dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />
    

    提供 Stub 的实现 [2]

    package com.foo;
    public class BarServiceStub implements BarService {
    	private final BarService barService;
    
    	// 构造函数传入真正的远程代理对象
     public BarServiceStub(BarService barService){
       		 this.barService = barService;
       }
    
     public String sayHello(String name) {
        	// 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
        	try {
           	 return barService.sayHello(name);
        	} catch (Exception e) {
           	 // 你可以容错,可以做任何AOP拦截事项
           	 return "容错数据";
       	 		}
    		}
    }
    
    • 注意:
    1. Stub 必须有可传入 Proxy 的构造函数。
    2. 在 interface 旁边放一个 Stub 实现,它实现 BarService 接口,并有一个传入远程 BarService 实例的构造函数。
  • 本地伪装

    • 说明:
      本地伪装通常用与服务降级,比如说统一鉴权的所有服务提供者全挂了之后,可以在客户端本地伪装返回一个健全失败的消息。
    • 方式:
      在 spring 配置文件中按以下方式配置:
    <dubbo:reference interface="com.foo.BarService" mock="true" />
    

    <dubbo:reference interface="com.foo.BarService" mock="com.foo.BarServiceMock" />
    

    在工程中提供 Mock 实现 [2]

    package com.foo;
    public class BarServiceMock implements BarService {
        public String sayHello(String name) {
            // 你可以伪造容错数据,此方法只在出现RpcException时被执行
            return "容错数据";
        }
    }
    

    如果服务的消费方经常需要 try-catch 捕获异常,如:

    Offer offer = null;
    try {
        offer = offerService.findOffer(offerId);
    } catch (RpcException e) {
       logger.error(e);
    }
    

    请考虑改为 Mock 实现,并在 Mock 实现中 return null。如果只是想简单的忽略异常,在 2.0.11 以上版本可用:

    <dubbo:reference interface="com.foo.BarService" mock="return null" />
    

    详细参考:《Dubbo示例-本地伪装》

  • 延迟暴露

    • 说明:
      如果服务在注册前需要有一些比较耗时的初始化操作,可以使用 Dubbo 的延迟暴露功能。
    • 方式:
      在服务提供者中配置延迟暴露的时间delay="5000"

    详细参考:《Dubbo示例-延迟暴露》

  • 并发控制

    • 说明:
      Dubbo 可以从客户端、服务端维度对接口和方法级别进行并发控制,限制每个方法执行的最大线程数量。

    • 方式:
      在服务提供者配置并发数量和规则:

      • 限制服务端并发执行不超过10
      <dubbo:service interface="com.foo.BarService" executes="10" />
      
      • 限制每个客户端的执行并发数量不超过10
      <dubbo:service interface="com.foo.BarService" actives="10" />
      

      <dubbo:reference interface="com.foo.BarService" actives="10" />
      

    详细参考:《Dubbo示例-并发控制》

  • 连接控制

    • 说明:
      Dubbo 可以设置服务器端接受的连接数和客户端使用的连接数。

    • 方式:

      • 服务端连接数量控制:
        限制服务器端接受的连接不能超过 10 个:
      <dubbo:provider protocol="dubbo" accepts="10" />
      

      <dubbo:protocol name="dubbo" accepts="10" />
      
      • 客户端连接数量控制:
        限制客户端服务使用连接不能超过 10 个 [2]
      <dubbo:reference interface="com.foo.BarService" connections="10" />
      

      <dubbo:service interface="com.foo.BarService" connections="10" />
      

    详细参考:《Dubbo示例-连接控制》

  • 延迟连接

    • 说明:
      针对使用 dubbo 协议暴露的服务,使用延迟连接可以减少长连接的连接数,等到调用时再创建连接。
    • 方式:
      dubbo 协议属性lazy="true"
      <dubbo:protocol name="dubbo" lazy="true" />
  • 粘滞连接

    • 说明:

      • 粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者挂了,再连另一台。
      • 粘滞连接将自动开启延迟连接,以减少长连接数。
      • 支持消费者和方法级别的配置。
    • 方式:
      使用 sticky="true" 开启粘滞连接。

    详细参考:《Dubbo示例-粘滞连接》

  • 令牌验证:

    • 说明:
      Dubbo 提供了令牌验证的功能,用来防止消费者绕过注册中心直接访问服务提供者。
    • 方式:
      在服务提供者侧通过设置token="true"或者指定密码token="123456"开启令牌验证。

    详细参考:《Dubbo示例-令牌验证》

  • 路由规则

三、测试开发类

  • 直连提供者

    • 说明:
      在开发测试阶段,我们想直接调用某个服务提供者,不希望走注册中心。
    • 方式:
      通过url="dubbo://localhost:20890"配置点对点直连。

    详细参考:《Dubbo示例-直连提供者》

  • 只订阅

    • 说明:
      在开发测试阶段,通常只有一个注册中心,如果一个正在开发的服务提供者注册,可能会影响其他消费者。Dubbo 提供了服务是否注册的开关。
    • 方式:
      通过register="false" 对服务是否注册进行控制。

    详细参考:《Dubbo示例-只订阅》

  • 只注册

    • 说明:
      多个注册中心时,可以根据实际情况来选择是否从注册中心订阅服务。
    • 方式:
      通过subscribe="false"对注册中心是否订阅服务来进行控制。

    详细参考:《Dubbo示例-只注册》

  • 静态服务

    • 说明:

      • 可以人工控制服务的上下线,此时将注册中心设置为非动态管理模式。
      • 服务提供者初次注册时为禁用状态,需人工启用。断线时,将不会被自动删除,需人工禁用。
      • 如果是一个第三方服务提供者,比如 memcached,可以直接向注册中心写入提供者地址信息,消费者正常使用。
    • 方式:
      通过dynamic="false" 设置注册中心的非动态管理模式。

    详细参考:《Dubbo示例-静态服务》

  • 泛化引用

    • 说明:
      泛化接口调用方式主要用于客户端没有 API 接口及模型类元的情况,参数及返回值中的所有 POJO 均用 Map 表示,通常用于框架集成。
    • 方式:
      在 Spring 配置申明 generic="true"
    <dubbo:reference id="barService" interface="com.foo.BarService" generic="true" />
    

    在 Java 代码获取 barService 并开始泛化调用:

    GenericService barService = (GenericService) applicationContext.getBean("barService");
    Object result = barService.$invoke("sayHello", new String[] { "java.lang.String" }, new Object[] { "World" });
    

    详细参考:《Dubbo示例-泛化引用》

  • 泛化实现

    -说明:
    泛接口实现方式主要用于服务器端没有API接口及模型类元的情况,参数及返回值中的所有POJO均用Map表示,通常用于框架集成,比如:实现一个通用的远程服务Mock框架,可通过实现GenericService接口处理所有服务请求。

    • 方式:
      在 Java 代码中实现 GenericService 接口:
    package com.foo;
    public class MyGenericService implements GenericService {
    
        public Object $invoke(String methodName, String[] parameterTypes, Object[] args) throws GenericException {
            if ("sayHello".equals(methodName)) {
                return "Welcome " + args[0];
            }
        }
    }
    

    在 Spring 配置申明服务的实现:

    <bean id="genericService" class="com.foo.MyGenericService" />
    <dubbo:service interface="com.foo.BarService" ref="genericService" />
    

    详细参考:《Dubbo示例-泛化实现》

  • 回声测试

    • 说明:
      回声测试用于检测服务是否可用,回声测试按照正常请求流程执行,能够测试整个调用是否通畅,可用于监控。
    • 方式:
      所有服务自动实现 EchoService 接口,只需将任意服务引用强制转型为 EchoService,即可使用。

    Spring 配置:

    <dubbo:reference id="memberService" interface="com.xxx.MemberService" />
    

    代码:

    // 远程服务引用
    MemberService memberService = ctx.getBean("memberService"); 
    
    EchoService echoService = (EchoService) memberService; // 强制转型为EchoService
    
    // 回声测试可用性
    String status = echoService.$echo("OK"); 
    
    assert(status.equals("OK"));
    

    详细参考:《Dubbo示例-回声测试》


关于配置更加详细的说明,参考 Dubbo 官方文档 schema 配置参考手册



❤ 转载请注明本文地址或来源,谢谢合作 ❤


center