Dubbo的示例文档

1. Dubbo文档的示例

1. 启动时检查

  • Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 check="true"

    可以通过 check="false" 关闭检查,比如,测试时,有些服务不关心,或者出现了循环依赖,必须有一方先启动。

    另外,如果你的 Spring 容器是懒加载的,或者通过 API 编程延迟引用服务,请关闭 check,否则服务临时不可用时,会抛出异常,拿到 null 引用,如果 check="false",总是会返回引用,当服务恢复时,能自动连上。

  • 通过spring配置文件

关闭某个服务的启动时检查(没有提供者时报错):

<dubbo:reference interface="" check="false" />

关闭所有服务的启动时检查(没有提供者时报错):

<dubbo:consumer check="false" />

关闭注册中心启动时检查(注册中心订阅失败时报错):

<dubbo:registry check="false" />
  • 通过dubbo.properties
dubbo.reference.com.foo.BarService.check=false
dubbo.reference.check=false
dubbo.consumer.check=false
dubbo.registry.check=false
  • 通过-D参数
java -Ddubbo.reference.com.foo.BarService.check=false
java -Ddubbo.reference.check=false
java -Ddubbo.registry.check=false
  • 配置的含义
dubbo.reference.check=false,强制改变所有 reference 的 check 值,就算配置中有声明,也会被覆盖。

dubbo.consumer.check=false,是设置 check 的缺省值,如果配置中有显式的声明,如:<dubbo:reference check="true"/>,不会受影响。

dubbo.registry.check=false,前面两个都是指订阅成功,但提供者列表是否为空是否报错,如果注册订阅失败时,也允许启动,需使用此选项,将在后台定时重试。

2. 集群容错

  • 在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。

  • 各种点关系:
    1. 这里的 Invoker 是 Provider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息
    2. Directory 代表多个 Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更
    3. Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
    4. Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等
    5. LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选

  • 集群容错模式

    可以自行扩展集群容错策略,参见:集群扩展

    • Failover Cluster

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

    重试次数配置如下:

    <dubbo:service retries="2" />
    

    或者

    <dubbo:reference retries="2" />
    

    或者

    <dubbo:reference>
        <dubbo:method name="findFoo" retries="2" />
    </dubbo:reference>
    
    • Failfast Cluster

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

    • Failsafe Cluster

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

    • Failback Cluster

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

    • Forking Cluster

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

    • Broadcast Cluster

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

  • 集群模式配置

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

    <dubbo:service cluster="failsafe" />
    

    <dubbo:reference cluster="failsafe" />
    

3. 负载均衡

  • 在集群负载均衡时,Dubbo提供了多种负载均衡策略,缺省为random随机调用。可参考负载均衡.

  • 负载均衡策略

    Random LoadBalance

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

    RoundRobin LoadBalance

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

    LeastActive LoadBalance

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

    ConsistentHash LoadBalance

    • 一致性Hash,相同参数的请求总是发送到同一个提供者。
    • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
    • 算法参见:算法配置
    • 缺省只对第一个参数Hash,如果要修改,请配置
    • 缺省用160份虚拟节点,如果要修改,请配置<dubbo:parameter key=“hash.nodes” value=“320” />
  • 配置

    服务端服务级别

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

​ 客户端服务级别

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

​ 服务端方法级别

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

​ 客户端方法级别

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

4.线程模型

  • 如果事件处理的逻辑能迅速完成,并且不会发起新的 IO 请求,比如只是在内存中记个标识,则直接在 IO 线程上处理更快,因为减少了线程池调度。

    但如果事件处理逻辑较慢,或者需要发起新的 IO 请求,比如需要查询数据库,则必须派发到线程池,否则 IO 线程阻塞,将导致不能接收其它请求。

    如果用 IO 线程处理事件,又在事件处理过程中发起新的 IO 请求,比如在连接事件中发起登录请求,会报“可能引发死锁”异常,但不会真死锁。

​ 因此,需要通过不同的派发策略和不同的线程池配置的组合来应对不同的场景:

<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100">

​ Dispatcher

  • all 所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。

  • direct 所有消息都不派发到线程池,全部在 IO 线程上直接执行。

  • message 只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO 线程上执行。

  • execution 只有请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行。

  • connection 在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。

    ThreadPool

  • fixed 固定大小线程池,启动时建立线程,不关闭,一直持有。(缺省)

  • cached 缓存线程池,空闲一分钟自动删除,需要时重建。

  • limited 可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。

  • eager 优先创建Worker线程池。在任务数量大于corePoolSize但是小于maximumPoolSize时,优先创建Worker来处理任务。当任务数量大于maximumPoolSize时,将任务放入阻塞队列中。阻塞队列充满时抛出RejectedExecutionException。(相比于cached:cached在任务数量超过maximumPoolSize时直接抛出异常而不是将任务放入阻塞队列)。

5.直连提供者

  • 在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直连方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。

  • 通过xml配置

    如果是线上需求需要点对点,可在 <dubbo:reference> 中配置 url 指向提供者,将绕过注册中心,多个地址用分号隔开,配置如下 [1]

<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService" url="dubbo://localhost:20890" />
  • 通过-D参数指定

    在JVM启动参数中加入-D参数映射服务地址[2],如:

java -Dcom.alibaba.xxx.XxxService=dubbo://localhost:20890
  • 通过文件映射

    如果服务比较多,也可以用文件映射,用 -Ddubbo.resolve.file 指定映射文件路径,此配置优先级高于<dubbo:reference >中的配置[3],如:

java -Ddubbo.resolve.file=xxx.properties

​ 然后在映射文件 xxx.properties 中加入配置,其中 key 为服务名,value 为服务提供者 URL:

com.alibaba.xxx.XxxService=dubbo://localhost:20890

注意 为了避免复杂化线上环境,不要在线上使用这个功能,只应在测试阶段使用。


    1. 1.0.6 及以上版本支持 ↩︎

    2. key 为服务名,value 为服务提供者 url,此配置优先级最高,1.0.15 及以上版本支持 ↩︎

    3. 1.0.15 及以上版本支持,2.0 以上版本自动加载 ${user.home}/dubbo-resolve.properties文件,不需要配置 ↩︎


6. 只订阅

​ 为方便开发测试,经常会在线下共用一个所有服务可用的注册中心,这时,如果一个正在开发中的服务提供者注册,可能会影响消费者不能正常运行。

可以让服务提供者开发方,只订阅服务(开发的服务可能依赖其它服务),而不注册正在开发的服务,通过直连测试正在开发的服务。

​ 禁用注册配置

<dubbo:registry address="10.20.30.152:9090" registry="false" />

或者

<dubbo:registry address="10.20.30.152:9090?register=false" />

7. 只注册

​ 如果有两个镜像环境,两个注册中心,有一个服务只在其中一个注册中心有部署,另一个注册中心还没来得及部署,而两个注册中心的其它应用都需要依赖此服务。这个时候,可以让服务提供者方只注册服务到另一注册中心,而不从另一注册中心订阅服务。

禁用订阅配置:

<dubbo:registry id="hzRegistry" address=="10.20.30.152:9090" />
<dubbo:registry id="qzRegistry" address=="10.20.30.152:9090" subsribe="false" />

或者

<dubbo:registry id="hzRegistry" address=="10.20.30.152:9090" />
<dubbo:registry id="hzRegistry" address=="10.20.30.152:9090?subscribe=false" />

8. 静态服务

​ 有时候希望人工管理服务提供者的上线和下线,此时需要将注册中心标识为非动态管理模式。

<dubbo:registry address="10.20.30.152:9090" dynamic="false" />

或者

<dubbo:registry address="10.20.30.152?dynamic=false" />

​ 服务提供者初次注册时为禁用状态,需要人工启用。断线时,将不会被自动删除,需要人工禁用。

​ 如果是一个第三方服务提供者,比如memcached,可以直接向注册中心写入提供者地址信息,消费者正常使用:

RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("memcached://10.20.153.11/com.foo.BarService?category=providers&dynamic=false&application=foo"));

9. 多协议

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

​ 不同服务不同协议

​ 不同服务在性能上适用不同协议进行传输,比如大数据用短连接协议,小数据大并发用长连接协议。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"> 
    <dubbo:application name="world"  />
    <dubbo:registry id="registry" address="10.20.141.150:9090" username="admin" password="hello1234" />
    <!-- 多协议配置 -->
    <dubbo:protocol name="dubbo" port="20880" />
    <dubbo:protocol name="rmi" port="1099" />
    <!-- 使用dubbo协议暴露服务 -->
    <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" protocol="dubbo" />
    <!-- 使用rmi协议暴露服务 -->
    <dubbo:service interface="com.alibaba.hello.api.DemoService" version="1.0.0" ref="demoService" protocol="rmi" /> 
</beans>

​ 多协议暴露服务

需要与http客户端互操作:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
    <dubbo:application name="world"  />
    <dubbo:registry id="registry" address="10.20.141.150:9090" username="admin" password="hello1234" />
    <!-- 多协议配置 -->
    <dubbo:protocol name="dubbo" port="20880" />
    <dubbo:protocol name="hessian" port="8080" />
    <!-- 使用多个协议暴露服务 -->
    <dubbo:service id="helloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" protocol="dubbo,hessian" />
</beans>

10. 多注册中心

Dubbo 支持同一服务向多注册中心同时注册,或者不同服务分别注册到不同的注册中心上去,甚至可以同时引用注册在不同注册中心上的同名服务。另外,注册中心是支持自定义扩展的 [1]

​ 多注册中心注册

比如:中文站有些服务来不及在青岛部署,只在杭州部署,而青岛的其它应用需要引用此服务,就可以将服务同时注册到两个注册中心。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
    <dubbo:application name="world"  />
    <!-- 多注册中心配置 -->
    <dubbo:registry id="hangzhouRegistry" address="10.20.141.150:9090" />
    <dubbo:registry id="qingdaoRegistry" address="10.20.141.151:9010" default="false" />
    <!-- 向多个注册中心注册 -->
    <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" registry="hangzhouRegistry,qingdaoRegistry" />
</beans>

​ 不同服务使用不同注册中心

比如:CRM 有些服务是专门为国际站设计的,有些服务是专门为中文站设计的。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
    <dubbo:application name="world"  />
    <!-- 多注册中心配置 -->
    <dubbo:registry id="chinaRegistry" address="10.20.141.150:9090" />
    <dubbo:registry id="intlRegistry" address="10.20.154.177:9010" default="false" />
    <!-- 向中文站注册中心注册 -->
    <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" registry="chinaRegistry" />
    <!-- 向国际站注册中心注册 -->
    <dubbo:service interface="com.alibaba.hello.api.DemoService" version="1.0.0" ref="demoService" registry="intlRegistry" />
</beans>

​ 多注册中心引用

比如:CRM 需同时调用中文站和国际站的 PC2 服务,PC2 在中文站和国际站均有部署,接口及版本号都一样,但连的数据库不一样。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
    <dubbo:application name="world"  />
    <!-- 多注册中心配置 -->
    <dubbo:registry id="chinaRegistry" address="10.20.141.150:9090" />
    <dubbo:registry id="intlRegistry" address="10.20.154.177:9010" default="false" />
    <!-- 引用中文站服务 -->
    <dubbo:reference id="chinaHelloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" registry="chinaRegistry" />
    <!-- 引用国际站站服务 -->
    <dubbo:reference id="intlHelloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" registry="intlRegistry" />
</beans>

如果只是测试环境临时需要连接两个不同注册中心,使用竖号分隔多个不同注册中心地址:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
    <dubbo:application name="world"  />
    <!-- 多注册中心配置,竖号分隔表示同时连接多个不同注册中心,同一注册中心的多个集群地址用逗号分隔 -->
    <dubbo:registry address="10.20.141.150:9090|10.20.154.177:9010" />
    <!-- 引用服务 -->
    <dubbo:reference id="helloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" />
</beans>

可以自行扩展注册中心,参见:注册中心扩展 ↩︎

11. 服务分组

当一个接口有多种实现时,可以用group区分。

​ 服务

<dubbo:service group="feedback" interface="com.xxx.IndexService" />
<dubbo:service group="member" interface="com.xxx.IndexService" />

​ 引用

<dubbo:reference id="feedbackIndexService" group="feedback" interface="com.xxx.IndexService" />
<dubbo:reference id="memberIndexService" group="member" interface="com.xxx.IndexService" />

​ 任意组:

<dubbo:reference id="barService" interface="com.foo.BarService" group="*" />

2.2.0 以上版本支持,总是只调一个可用组的实现 ↩︎

12. 多版本

当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。

可以按照以下的步骤进行版本迁移:

  1. 在低压力时间段,先升级一半提供者为新版本
  2. 再将所有消费者升级为新版本
  3. 然后将剩下的一半提供者升级为新版本

老版本服务提供者配置:

<dubbo:service interface="com.foo.BarService" version="1.0.0" />

新版本服务提供者配置:

<dubbo:service interface="com.foo.BarService" version="2.0.0" />

老版本服务消费者配置:

<dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" />

新版本服务消费者配置:

<dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" />

如果不需要区分版本,可以按照以下的方式配置 [1]

<dubbo:reference id="barService" interface="com.foo.BarService" version="*" />

2.2.0 以上版本支持 ↩︎

13. 分组聚合

按组合并返回结果 [1],比如菜单服务,接口一样,但有多种实现,用group区分,现在消费方需从每种group中调用一次返回结果,合并结果返回,这样就可以实现聚合菜单项。

相关代码可以参考 dubbo 项目中的示例

​ 配置

搜索所有分组

<dubbo:reference interface="com.xxx.MenuService" group="*" merger="true" />

合并指定分组

<dubbo:reference interface="com.xxx.MenuService" group="aaa,bbb" merger="true" />

指定方法合并结果,其它未指定的方法,将只调用一个 Group

<dubbo:reference interface="com.xxx.MenuService" group="*">
    <dubbo:method name="getMenuItems" merger="true" />
</dubbo:reference>

某个方法不合并结果,其它都合并结果

<dubbo:reference interface="com.xxx.MenuService" group="*" merger="true">
    <dubbo:method name="getMenuItems" merger="false" />
</dubbo:reference>

指定合并策略,缺省根据返回值类型自动匹配,如果同一类型有两个合并器时,需指定合并器的名称 [2]

<dubbo:reference interface="com.xxx.MenuService" group="*">
    <dubbo:method name="getMenuItems" merger="mymerge" />
</dubbo:reference>

指定合并方法,将调用返回结果的指定方法进行合并,合并方法的参数类型必须是返回结果类型本身

<dubbo:reference interface="com.xxx.MenuService" group="*">
    <dubbo:method name="getMenuItems" merger=".addAll" />
</dubbo:reference>
  1. 2.1.0 版本开始支持 ↩︎
  2. 参见:合并结果扩展 ↩︎

14. 参数验证

参数验证功能 [1] 是基于 JSR303 实现的,用户只需标识 JSR303 标准的验证 annotation,并通过声明 filter 来实现验证 [2]

​ Maven依赖

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.0.0.GA</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>4.2.0.Final</version>
</dependency>

示例

​ 参数标注示例:

import java.io.Serializable;
import java.util.Date;
 
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
 
public class ValidationParameter implements Serializable {
    private static final long serialVersionUID = 7158911668568000392L;
 
    @NotNull // 不允许为空
    @Size(min = 1, max = 20) // 长度或大小范围
    private String name;
 
    @NotNull(groups = ValidationService.Save.class) // 保存时不允许为空,更新时允许为空 ,表示不更新该字段
    @Pattern(regexp = "^\\s*\\w+(?:\\.{0,1}[\\w-]+)*@[a-zA-Z0-9]+(?:[-.][a-zA-Z0-9]+)*\\.[a-zA-Z]+\\s*$")
    private String email;
 
    @Min(18) // 最小值
    @Max(100) // 最大值
    private int age;
 
    @Past // 必须为一个过去的时间
    private Date loginDate;
 
    @Future // 必须为一个未来的时间
    private Date expiryDate;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getEmail() {
        return email;
    }
 
    public void setEmail(String email) {
        this.email = email;
    }
 
    public int getAge() {
        return age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
 
    public Date getLoginDate() {
        return loginDate;
    }
 
    public void setLoginDate(Date loginDate) {
        this.loginDate = loginDate;
    }
 
    public Date getExpiryDate() {
        return expiryDate;
    }
 
    public void setExpiryDate(Date expiryDate) {
        this.expiryDate = expiryDate;
    }
}

分组验证示例

public interface ValidationService { // 缺省可按服务接口区分验证场景,如:@NotNull(groups = ValidationService.class)   
    @interface Save{} // 与方法同名接口,首字母大写,用于区分验证场景,如:@NotNull(groups = ValidationService.Save.class),可选
    void save(ValidationParameter parameter);
    void update(ValidationParameter parameter);
}

关联验证示例

import javax.validation.GroupSequence;
 
public interface ValidationService {   
    @GroupSequence(Update.class) // 同时验证Update组规则
    @interface Save{}
    void save(ValidationParameter parameter);
 
    @interface Update{} 
    void update(ValidationParameter parameter);
}

参数验证示例

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
 
public interface ValidationService {
    void save(@NotNull ValidationParameter parameter); // 验证参数不为空
    void delete(@Min(1) int id); // 直接对基本类型参数验证
}

配置

​ 在客户端验证参数

<dubbo:reference id="validationService" interface="org.apache.dubbo.examples.validation.api.ValidationService" validation="true" />

​ 在服务器端验证参数

<dubbo:service interface="org.apache.dubbo.examples.validation.api.ValidationService" ref="validationService" validation="true" />

​ 验证异常信息

import javax.validation.ConstraintViolationException;
import javax.validation.ConstraintViolationException;
 
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
import org.apache.dubbo.examples.validation.api.ValidationParameter;
import org.apache.dubbo.examples.validation.api.ValidationService;
import org.apache.dubbo.rpc.RpcException;
 
public class ValidationConsumer {   
    public static void main(String[] args) throws Exception {
        String config = ValidationConsumer.class.getPackage().getName().replace('.', '/') + "/validation-consumer.xml";
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(config);
        context.start();
        ValidationService validationService = (ValidationService)context.getBean("validationService");
        // Error
        try {
            parameter = new ValidationParameter();
            validationService.save(parameter);
            System.out.println("Validation ERROR");
        } catch (RpcException e) { // 抛出的是RpcException
            ConstraintViolationException ve = (ConstraintViolationException) e.getCause(); // 里面嵌了一个ConstraintViolationException
            Set<ConstraintViolation<?>> violations = ve.getConstraintViolations(); // 可以拿到一个验证错误详细信息的集合
            System.out.println(violations);
        }
    } 
}
  1. 2.1.0 版本开始支持, 如何使用可以参考 dubbo 项目中的示例代码 ↩︎
  2. 验证方式可扩展,扩展方式参见开发者手册中的验证扩展 ↩︎

15. 结果缓存

结果缓存 [1],用于加速热门数据的访问速度,Dubbo 提供声明式缓存,以减少用户加缓存的工作量 [2]

​ 缓存类型

  • lru 基于最近最少使用原则删除多余缓存,保持最热的数据被缓存。
  • threadlocal 当前线程缓存,比如一个页面渲染,用到很多 portal,每个 portal 都要去查用户信息,通过线程缓存,可以减少这种多余访问。
  • jcacheJSR107 集成,可以桥接各种缓存实现。

缓存类型可扩展,参见:缓存扩展

​ 配置

<dubbo:reference interface="com.foo.BarService" cache="lru" />

或:

<dubbo:reference interface="com.foo.BarService">
    <dubbo:method name="findBar" cache="lru" />
</dubbo:reference>
  1. 2.1.0 以上版本支持 ↩︎
  2. 示例代码 ↩︎

16. 泛化引用

泛化接口调用方式主要用于客户端没有 API 接口及模型类元的情况,参数及返回值中的所有 POJO 均用 Map 表示,通常用于框架集成,比如:实现一个通用的服务测试框架,可通过 GenericService 调用所有服务实现。

  • 通过spring使用泛化调用

    在 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" });
  • 通过API方式使用泛化调用
import org.apache.dubbo.rpc.service.GenericService; 
... 
 
// 引用远程服务 
// 该实例很重量,里面封装了所有与注册中心及服务提供方连接,请缓存
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>(); 
// 弱类型接口名
reference.setInterface("com.xxx.XxxService");  
reference.setVersion("1.0.0");
// 声明为泛化接口 
reference.setGeneric(true);  

// 用org.apache.dubbo.rpc.service.GenericService可以替代所有接口引用  
GenericService genericService = reference.get(); 
 
// 基本类型以及Date,List,Map等不需要转换,直接调用 
Object result = genericService.$invoke("sayHello", new String[] {"java.lang.String"}, new Object[] {"world"}); 
 
// 用Map表示POJO参数,如果返回值为POJO也将自动转成Map 
Map<String, Object> person = new HashMap<String, Object>(); 
person.put("name", "xxx"); 
person.put("password", "yyy"); 
// 如果返回POJO将自动转成Map 
Object result = genericService.$invoke("findPerson", new String[]
{"com.xxx.Person"}, new Object[]{person}); 
 
...

有关泛化类型的进一步解释:

​ 假如存在POJO 如:

package com.xxx;

public class PersonImpl implements Person {
    private String name;
    private String password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

则POJO数据:

Person person = new PersonImpl(); 
person.setName("xxx"); 
person.setPassword("yyy");

可用下面 Map 表示:

Map<String, Object> map = new HashMap<String, Object>(); 
// 注意:如果参数类型是接口,或者List等丢失泛型,可通过class属性指定类型。
map.put("class", "com.xxx.PersonImpl"); 
map.put("name", "xxx"); 
map.put("password", "yyy");

17. 实现泛化调用

泛接口实现方式主要用于服务器端没有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暴露泛化实现:

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

<bean id="genericService" class="com.foo.MyGenericService" />
<dubbo:service interface="com.foo.BarService" ref="genericService" />
  • 通过API的方式暴露泛化实现:
... 
// 用org.apache.dubbo.rpc.service.GenericService可以替代所有接口实现 
GenericService xxxService = new XxxGenericService(); 

// 该实例很重量,里面封装了所有与注册中心及服务提供方连接,请缓存 
ServiceConfig<GenericService> service = new ServiceConfig<GenericService>();
// 弱类型接口名 
service.setInterface("com.xxx.XxxService");  
service.setVersion("1.0.0"); 
// 指向一个通用服务实现 
service.setRef(xxxService); 
 
// 暴露及注册服务 
service.export();

18. 回声测试

回声测试用于检测服务是否可用,回声测试按照正常请求流程执行,能够测试整个调用是否通畅,可用于监控。

所有服务自动实现 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"));

19. 上下文信息

上下文中存放的是当前调用过程中所需的环境信息。所有配置信息都将转换为 URL 的参数,参见 schema 配置参考手册 中的对应URL参数一列。

RpcContext 是一个 ThreadLocal 的临时状态记录器,当接收到 RPC 请求,或发起 RPC 请求时,RpcContext 的状态都会变化。比如:A 调 B,B 再调 C,则 B 机器上,在 B 调 C 之前,RpcContext 记录的是 A 调 B 的信息,在 B 调 C 之后,RpcContext 记录的是 B 调 C 的信息。

  • 服务消费方:
// 远程调用
xxxService.xxx();
// 本端是否为消费端,这里会返回true
boolean isConsumerSide = RpcContext.getContext().isConsumerSide();
// 获取最后一次调用的提供方IP地址
String serverIP = RpcContext.getContext().getRemoteHost();
// 获取当前服务配置信息,所有配置信息都将转换为URL的参数
String application = RpcContext.getContext().getUrl().getParameter("application");
// 注意:每发起RPC调用,上下文状态会变化
yyyService.yyy();
  • 服务提供方:
public class XxxServiceImpl implements XxxService {
 
    public void xxx() {
        // 本端是否为提供端,这里会返回true
        boolean isProviderSide = RpcContext.getContext().isProviderSide();
        // 获取调用方IP地址
        String clientIP = RpcContext.getContext().getRemoteHost();
        // 获取当前服务配置信息,所有配置信息都将转换为URL的参数
        String application = RpcContext.getContext().getUrl().getParameter("application");
        // 注意:每发起RPC调用,上下文状态会变化
        yyyService.yyy();
        // 此时本端变成消费端,这里会返回false
        boolean isProviderSide = RpcContext.getContext().isProviderSide();
    } 
}

20. 隐式参数

可以通过 RpcContext 上的 setAttachmentgetAttachment 在服务消费方和提供方之间进行参数的隐式传递。[1]

  • 在服务消费方端设置隐式参数

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

RpcContext.getContext().setAttachment("index", "1"); // 隐式传参,后面的远程调用都会隐式将这些参数发送到服务器端,类似cookie,用于框架集成,不建议常规业务使用
xxxService.xxx(); // 远程调用
// ...
  • 在服务提供方端设置隐式参数
public class XxxServiceImpl implements XxxService {
 
    public void xxx() {
        // 获取客户端隐式传入的参数,用于框架集成,不建议常规业务使用
        String index = RpcContext.getContext().getAttachment("index"); 
    }
}

1.注意:path, group, version, dubbo, token, timeout 几个 key 是保留字段,请使用其它值。

21. Consumer异步调用

从v2.7.0开始,Dubbo的所有异步编程接口开始以CompletableFuture为基础

基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小

  • 使用CompletableFuture签名的接口

需要服务提供者事先定义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.");
  • 使用RpcContext

在 consumer.xml 中配置:

<dubbo:reference id="asyncService" interface="org.apache.dubbo.samples.governance.api.AsyncService">
      <dubbo:method name="sayHello" async="true" />
</dubbo:reference>

调用代码:

// 此调用会立即返回null
asyncService.sayHello("world");
// 拿到调用的Future引用,当结果返回后,会被通知和设置到此Future
CompletableFuture<String> helloFuture = RpcContext.getContext().getCompletableFuture();
// 为Future添加回调
helloFuture.whenComplete((retValue, exception) -> {
    if (exception == null) {
        System.out.println(retValue);
    } else {
        exception.printStackTrace();
    }
});

或者,你也可以这样做异步调用:

CompletableFuture<String> future = RpcContext.getContext().asyncCall(
    () -> {
        asyncService.sayHello("oneway call request1");
    }
);

future.get();
  • 重载服务接口

如果你只有这样的同步服务定义,而又不喜欢RpcContext的异步使用方式。

public interface GreetingsService {
    String sayHi(String name);
}

那还有一种方式,就是利用Java 8提供的default接口实现,重载一个带有带有CompletableFuture签名的方法。

有两种方式来实现:

​ 提供方或消费方自己修改接口签名

public interface GreetingsService {
    String sayHi(String name);
    
    // AsyncSignal is totally optional, you can use any parameter type as long as java allows your to do that.
    default CompletableFuture<String> sayHi(String name, AsyncSignal signal) {
        return CompletableFuture.completedFuture(sayHi(name));
    }
}
  1. Dubbo官方提供compiler hacker,编译期自动重写同步方法,请在此讨论和跟进具体进展。

你也可以设置是否等待消息发出: [1]

1. sent="true" 等待消息发出,消息发送失败将抛出异常。
2. sent="false" 不等待消息发出,将消息放入 IO 队列,即刻返回。
<dubbo:method name="findFoo" async="true" sent="true" />

如果你只是想异步,完全忽略返回值,可以配置 return="false",以减少 Future 对象的创建和管理成本:

<dubbo:method name="findFoo" async="true" return="false" />
  1. 异步总是不等待返回

22. Provider 异步执行

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

注意:Provider端异步执行和Consumer端异步调用是相互独立的,你可以任意正交组合两端配置

  • Consumer同步 - Provider同步
  • Consumer异步 - Provider同步
  • Consumer同步 - Provider异步
  • Consumer异步 - Provider异步
  • 定义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线程池的阻塞。

  • 使用AsynContext

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;
    }
}

23. 本地调用

本地调用使用了 injvm 协议,是一个伪协议,它不开启端口,不发起远程调用,只在 JVM 内直接关联,但执行 Dubbo 的 Filter 链。

  • 配置

定义injvm协议

<dubbo:protocol name="injvm" />

设置默认协议

<dubbo:provider protocol="injvm" />

设置服务协议

<dubbo:service protocol="injvm" />

优先使用 injvm

<dubbo:consumer injvm="true" .../>
<dubbo:provider injvm="true" .../>

<dubbo:reference injvm="true" .../>
<dubbo:service injvm="true" .../>

注意:dubbo从 2.2.0 每个服务默认都会在本地暴露,无需进行任何配置即可进行本地引用,如果不希望服务进行远程暴露,只需要在provider将protocol设置成injvm即可

  • 自动暴露,引用本地服务

2.2.0 开始,每个服务默认都会在本地暴露。在引用服务的时候,默认优先引用本地服务。如果希望引用远程服务可以使用一下配置强制引用远程服务。

<dubbo:reference ... scope="remote" />

24. 参数回调

参数回调方式与调用本地 callback 或 listener 相同,只需要在 Spring 的配置文件中声明哪个参数是 callback 类型即可。Dubbo 将基于长连接生成反向代理,这样就可以从服务器端调用客户端逻辑 [1]。可以参考 dubbo 项目中的示例代码

服务示例

CallbackService.java

package com.callback;
 
public interface CallbackService {
    void addListener(String key, CallbackListener listener);
}

CallbackListener.java

package com.callback;
 
public interface CallbackListener {
    void changed(String msg);
}
  • 服务提供者接口实现示例:
package com.callback.impl;
 
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
 
import com.callback.CallbackListener;
import com.callback.CallbackService;
 
public class CallbackServiceImpl implements CallbackService {
     
    private final Map<String, CallbackListener> listeners = new ConcurrentHashMap<String, CallbackListener>();
  
    public CallbackServiceImpl() {
        Thread t = new Thread(new Runnable() {
            public void run() {
                while(true) {
                    try {
                        for(Map.Entry<String, CallbackListener> entry : listeners.entrySet()){
                           try {
                               entry.getValue().changed(getChanged(entry.getKey()));
                           } catch (Throwable t) {
                               listeners.remove(entry.getKey());
                           }
                        }
                        Thread.sleep(5000); // 定时触发变更通知
                    } catch (Throwable t) { // 防御容错
                        t.printStackTrace();
                    }
                }
            }
        });
        t.setDaemon(true);
        t.start();
    }
  
    public void addListener(String key, CallbackListener listener) {
        listeners.put(key, listener);
        listener.changed(getChanged(key)); // 发送变更通知
    }
     
    private String getChanged(String key) {
        return "Changed: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
    }
}
  • 服务提供者配置示例
<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:reference id="callbackService" interface="com.callback.CallbackService" />
  • 服务消费者配置示例:
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:consumer.xml");
context.start();
 
CallbackService callbackService = (CallbackService) context.getBean("callbackService");
 
callbackService.addListener("foo.bar", new CallbackListener(){
    public void changed(String msg) {
        System.out.println("callback1:" + msg);
    }
});

1.2.0.6 及其以上版本支持

25. 时间通知

在调用之前、调用之后、出现异常时,会触发 oninvokeonreturnonthrow 三个事件,可以配置当事件发生时,通知哪个类的哪个方法 [1]

  • 服务提供者与消费者共享服务接口:
interface IDemoService {
    public Person get(int id);
}
  • 服务提供者实现:
class NormalDemoService implements IDemoService {
    public Person get(int id) {
        return new Person(id, "charles`son", 4);
    }
}
  • 服务提供者配置:
<dubbo:application name="rpc-callback-demo" />
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<bean id="demoService" class="org.apache.dubbo.callback.implicit.NormalDemoService" />
<dubbo:service interface="org.apache.dubbo.callback.implicit.IDemoService" ref="demoService" version="1.0.0" group="cn"/>
  • 服务消费者Callback接口:
interface Notify {
    public void onreturn(Person msg, Integer id);
    public void onthrow(Throwable ex, Integer id);
}
  • 服务消费者Callback实现:
class NotifyImpl implements Notify {
    public Map<Integer, Person>    ret    = new HashMap<Integer, Person>();
    public Map<Integer, Throwable> errors = new HashMap<Integer, Throwable>();
    
    public void onreturn(Person msg, Integer id) {
        System.out.println("onreturn:" + msg);
        ret.put(id, msg);
    }
    
    public void onthrow(Throwable ex, Integer id) {
        errors.put(id, ex);
    }
}
  • 服务消费者Callback配置:
<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>

callbackasync 功能正交分解,async=true 表示结果是否马上返回,onreturn 表示是否需要回调。

两者叠加存在以下几种组合情况 [2]

  • 异步回调模式:async=true onreturn="xxx"
  • 同步回调模式:async=false onreturn="xxx"
  • 异步无回调 :async=true
  • 同步无回调 :async=false

测试代码:

IDemoService demoService = (IDemoService) context.getBean("demoService");
NofifyImpl notify = (NofifyImpl) context.getBean("demoCallback");
int requestId = 2;
Person ret = demoService.get(requestId);
Assert.assertEquals(null, ret);
//for Test:只是用来说明callback正常被调用,业务具体实现自行决定.
for (int i = 0; i < 10; i++) {
    if (!notify.ret.containsKey(requestId)) {
        Thread.sleep(200);
    } else {
        break;
    }
}
Assert.assertEquals(requestId, notify.ret.get(requestId).getId());
  1. 支持版本:2.0.7 之后 ↩︎
  2. async=false 默认 ↩︎

26. 本地存根

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

在 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 实例的构造函数 ↩︎

27. 本地伪装

本地伪装 [1] 通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过 Mock 数据返回授权失败。

在 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" />
  • 进阶用法

    • return

    使用 return 来返回一个字符串表示的对象,作为 Mock 的返回值。合法的字符串可以是:

    empty: 代表空,基本类型的默认值,或者集合类的空值
    null: null
    true: true
    false: false
    JSON 格式: 反序列化 JSON 所得到的对象
    
    • throw

    使用 throw 来返回一个 Exception 对象,作为 Mock 的返回值。

    当调用出错时,抛出一个默认的 RPCException:

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

    当调用出错时,抛出指定的 Exception:

    <dubbo:reference interface="com.foo.BarService" mock="throw com.foo.MockException" />
    
    • force和fail

    2.6.6 以上的版本,可以开始在 Spring XML 配置文件中使用 fail:force:force: 代表强制使用 Mock 行为,在这种情况下不会走远程调用。fail: 与默认行为一致,只有当远程调用发生错误时才使用 Mock 行为。force:fail: 都支持与 throw 或者 return 组合使用。

    强制返回指定值:

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

    强制抛出指定异常:

    <dubbo:reference interface="com.foo.BarService" mock="force:throw com.foo.MockException" />
    
    • 在方法级别配置Mock

    Mock 可以在方法级别上指定,假定 com.foo.BarService 上有好几个方法,我们可以单独为 sayHello() 方法指定 Mock 行为。具体配置如下所示,在本例中,只要 sayHello() 被调用到时,强制返回 “fake”:

    <dubbo:reference id="demoService" check="false" interface="com.foo.BarService">
        <dubbo:parameter key="sayHello.mock" value="force:return fake"/>
    </dubbo:reference>
    
    1. Mock 是 Stub 的一个子集,便于服务提供方在客户端执行容错逻辑,因经常需要在出现 RpcException (比如网络失败,超时等)时进行容错,而在出现业务异常(比如登录用户名密码错误)时不需要容错,如果用 Stub,可能就需要捕获并依赖 RpcException 类,而用 Mock 就可以不依赖 RpcException,因为它的约定就是只有出现 RpcException 时才执行。
    2. 在 interface 旁放一个 Mock 实现,它实现 BarService 接口,并有一个无参构造函数

28. 本地暴露

如果你的服务需要预热时间,比如初始化缓存,等待相关资源就位等,可以使用 delay 进行延迟暴露。我们在 Dubbo 2.6.5 版本中对服务延迟暴露逻辑进行了细微的调整,将需要延迟暴露(delay > 0)服务的倒计时动作推迟到了 Spring 初始化完成后进行。你在使用 Dubbo 的过程中,并不会感知到此变化,因此请放心使用。

Dubbo-2.6.5 之前版本

  • 延迟到spring初始化后,再暴露服务:
<dubbo:service delay="-1" />
  • 延迟五秒暴露服务
<dubbo:service delay="5000" />

Dubbo-2.6.5 之后版本

所有服务都将在 Spring 初始化完成后进行暴露,如果你不需要延迟暴露服务,无需配置 delay。

  • 延迟五秒暴露服务
<dubbo:service delay="5000" />

spring2.x初始化死锁问题

  • 触发条件

在 Spring 解析到 <dubbo:service /> 时,就已经向外暴露了服务,而 Spring 还在接着初始化其它 Bean。如果这时有请求进来,并且服务的实现类里有调用 applicationContext.getBean() 的用法。

​ 1.请求线程的 applicationContext.getBean() 调用,先同步 singletonObjects 判断 Bean 是否存在,不存在就同步 beanDefinitionMap 进行初始化,并再次同步 singletonObjects 写入 Bean 实例缓存。

2.而 Spring 初始化线程,因不需要判断 Bean 的存在,直接同步 beanDefinitionMap 进行初始化,并同步 singletonObjects 写入 Bean 实例缓存。

11

这样就导致 getBean 线程,先锁 singletonObjects,再锁 beanDefinitionMap,再次锁 singletonObjects。
而 Spring 初始化线程,先锁 beanDefinitionMap,再锁 singletonObjects。反向锁导致线程死锁,不能提供服务,启动不了。

  • 规避办法
  1. 强烈建议不要在服务的实现类中有 applicationContext.getBean() 的调用,全部采用 IoC 注入的方式使用 Spring的Bean。
  2. 如果实在要调 getBean(),可以将 Dubbo 的配置放在 Spring 的最后加载。
  3. 如果不想依赖配置顺序,可以使用 <dubbo:provider delay=”-1” />,使 Dubbo 在 Spring 容器初始化完后,再暴露服务。
  4. 如果大量使用 getBean(),相当于已经把 Spring 退化为工厂模式在用,可以将 Dubbo 的服务隔离单独的 Spring 容器。

1.基于 Spring 的 ContextRefreshedEvent 事件触发暴露

29. 并发控制

  • 配置样例

样例1

限制 com.foo.BarService 的每个方法,服务器端并发执行(或占用线程池线程数)不能超过 10 个:

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

样例2

限制 com.foo.BarServicesayHello 方法,服务器端并发执行(或占用线程池线程数)不能超过 10 个:

<dubbo:service interface="com.foo.BarService">
    <dubbo:method name="sayHello" executes="10" />
</dubbo:service>

样例3

限制 com.foo.BarService 的每个方法,每客户端并发执行(或占用连接的请求数)不能超过 10 个:

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

或者

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

样例4

限制 com.foo.BarServicesayHello 方法,每客户端并发执行(或占用连接的请求数)不能超过 10 个:

<dubbo:service interface="com.foo.BarService">
    <dubbo:method name="sayHello" actives="10" />
</dubbo:service>

或者

<dubbo:reference interface="com.foo.BarService">
    <dubbo:method name="sayHello" actives="10" />
</dubbo:service>

如果 <dubbo:service><dubbo:reference> 都配了actives,<dubbo:reference> 优先,参见:配置的覆盖策略

  • **Load Balance **均衡

配置服务的客户端的 loadbalance 属性为 leastactive,此 Loadbalance 会调用并发数最小的 Provider(Consumer端并发数)。

<dubbo:reference interface="com.foo.BarService" loadbalance="leastactive" />

或者

<dubbo:service interface="com.foo.BarService" loadbalance="leastactive" />

30. 连接控制

  • 服务端连接控制

限制服务器端接受的连接不能超过 10 个 [1]

<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:service><dubbo:reference> 都配了 connections,<dubbo:reference> 优先,参见:配置的覆盖策略

  1. 因为连接在 Server上,所以配置在 Provider 上
  2. 如果是长连接,比如 Dubbo 协议,connections 表示该服务对每个提供者建立的长连接数

31. 延迟连接

延迟连接用于减少长连接数。当有调用发起时,再创建长连接。[1]

<dubbo:protocol name="dubbo" lazy="true" />

注意:该配置只对使用长连接的 dubbo 协议生效。

32. 粘滞连接

粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者挂了,再连另一台。

粘滞连接将自动开启延迟连接,以减少长连接数。

<dubbo:reference id="xxxService" interface="com.xxx.XxxService" sticky="true" />

Dubbo 支持方法级别的粘滞连接,如果你想进行更细粒度的控制,还可以这样配置。

<dubbo:reference id="xxxService" interface="com.xxx.XxxService">
    <dubbo:mothod name="sayHello" sticky="true" />
</dubbo:reference>

33. 令牌验证

通过令牌验证在注册中心控制权限,以决定要不要下发令牌给消费者,可以防止消费者绕过注册中心访问提供者,另外通过注册中心可灵活改变授权方式,而不需修改或升级提供者

111

可以全局设置开启令牌验证:

<!--随机token令牌,使用UUID生成-->
<dubbo:provider interface="com.foo.BarService" token="true" />

或者

<!--固定token令牌,相当于密码-->
<dubbo:provider interface="com.foo.BarService" token="123456" />

也可在服务级别设置:

<!--随机token令牌,使用UUID生成-->
<dubbo:service interface="com.foo.BarService" token="true" />

或者

<!--固定token令牌,相当于密码-->
<dubbo:service interface="com.foo.BarService" token="123456" />

34. 路由规则

在此查看老版本路由规则(2.6.x or before)

路由规则在发起一次RPC调用前起到过滤目标服务器地址的作用,过滤后的地址列表,将作为消费端最终发起RPC调用的备选地址。

  • 条件路由。支持以服务或Consumer应用为粒度配置路由规则。
  • 标签路由。以Provider应用为粒度配置路由规则。

后续我们计划在2.6.x版本的基础上继续增强脚本路由功能,老版本脚本路由规则配置方式请参见开篇链接。

条件路由:

​ 您可以随时在服务治理控制台Dubbo-Admin写入路由规则

简介:

  • 应用粒度
# app1的消费者只能消费所有端口为20880的服务实例
# app2的消费者只能消费所有端口为20881的服务实例
---
scope: application
force: true
runtime: true
enabled: true
key: governance-conditionrouter-consumer
conditions:
  - application=app1 => address=*:20880
  - application=app2 => address=*:20881
...
  • 服务粒度
# DemoService的sayHello方法只能消费所有端口为20880的服务实例
# DemoService的sayHi方法只能消费所有端口为20881的服务实例
---
scope: service
force: true
runtime: true
enabled: true
key: org.apache.dubbo.samples.governance.api.DemoService
conditions:
  - method=sayHello => address=*:20880
  - method=sayHi => address=*:20881
...

规则详解:

​ 各字段含义:

1.scope表示路由规则的作用粒度,scope的取值会决定key的取值。必填。
	service 服务粒度
	application 应用粒度
2.Key明确规则体作用在哪个服务或应用。必填。
	scope=service时,key取值为[{group}:]{service}[:{version}]的组合
	scope=application时,key取值为application名称
3.enabled=true 当前路由规则是否生效,可不填,缺省生效。
4.force=false 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 false。
	runtime=false 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如	果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 false。
5.priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0。
6.conditions 定义具体的路由规则内容。必填。

​ Conditions规则体:

`conditions`部分是规则的主体,由1到任意多条规则组成,下面我们就每个规则的配置语法做详细说明:

格式

  1. => 之前的为消费者匹配条件,所有参数和消费者的 URL 进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则。

  2. => 之后为提供者地址列表的过滤条件,所有参数和提供者的 URL 进行对比,消费者最终只拿到过滤后的地址列表。

  3. 如果匹配条件为空,表示对所有消费方应用,如:=> host != 10.20.153.11

  4. 如果过滤条件为空,表示禁止访问,如:host = 10.20.153.10 =>

表达式

参数支持:

服务调用信息,如:method, argument 等,暂不支持参数路由
URL 本身的字段,如:protocol, host, port 等
以及 URL 上的所有参数,如:application, organization 等

条件支持:

等号 = 表示"匹配",如:host = 10.20.153.10
不等号 != 表示"不匹配",如:host != 10.20.153.10

值支持:

以逗号 , 分隔多个值,如:host != 10.20.153.10,10.20.153.11
以星号 * 结尾,表示通配,如:host != 10.20.*
以美元符 $ 开头,表示引用消费者参数,如:host = $host

Condition示例

排除预发布机:

=> host != 172.22.3.91

白名单[1]

host != 10.20.153.10,10.20.153.11 =>

黑名单:

host = 10.20.153.10,10.20.153.11 =>

服务寄宿在应用上,只暴露一部分的机器,防止整个集群挂掉:

=> host = 172.22.3.1*,172.22.3.2*

为重要应用提供额外的机器:

application != kylin => host != 172.22.3.95,172.22.3.96

读写分离:

method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96
method != find*,list*,get*,is* => host = 172.22.3.97,172.22.3.98

前后台分离:

application = bops => host = 172.22.3.91,172.22.3.92,172.22.3.93
application != bops => host = 172.22.3.94,172.22.3.95,172.22.3.96

隔离不同机房网段:

host != 172.22.3.* => host != 172.22.3.*

提供者与消费者部署在同集群内,本机只访问本机的服务:

=> host = $host

标签路由规则

简介:

标签路由通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。

Provider

标签主要是指对Provider端应用实例的分组,目前有两种方式可以完成实例分组,分别是动态规则打标静态规则打标,其中动态规则相较于静态规则优先级更高,而当两种规则同时存在且出现冲突时,将以动态规则为准。

动态规则打标,可随时在服务治理控制台下发标签归组规则

# governance-tagrouter-provider应用增加了两个标签分组tag1和tag2
# tag1包含一个实例 127.0.0.1:20880
# tag2包含一个实例 127.0.0.1:20881
---
  force: false
  runtime: true
  enabled: true
  key: governance-tagrouter-provider
  tags:
    - name: tag1
      addresses: ["127.0.0.1:20880"]
    - name: tag2
      addresses: ["127.0.0.1:20881"]
 ...
静态打标

<dubbo:provider tag="tag1"/>

or

<dubbo:service tag="tag1"/>

or

java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV}

consume

RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"tag1");

请求标签的作用域为每一次 invocation,使用 attachment 来传递请求标签,注意保存在 attachment 中的值将会在一次完整的远程调用中持续传递,得益于这样的特性,我们只需要在起始调用时,通过一行代码的设置,达到标签的持续传递。

目前仅仅支持 hardcoding 的方式设置 requestTag。注意到 RpcContext 是线程绑定的,优雅的使用 TagRouter 特性,建议通过 servlet 过滤器(在 web 环境下),或者定制的 SPI 过滤器设置 requestTag。

规则详解

格式:

1. Key明确规则体作用到哪个应用。必填。
2. enabled=true 当前路由规则是否生效,可不填,缺省生效。
3. force=false 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 false。
4. runtime=false 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如	果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 false。
5. priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0。
6. tags 定义具体的标签分组内容,可定义任意n(n>=1)个标签并为每个标签指定实例列表。必填。
	name, 标签名称
7. addresses, 当前标签包含的实例列表

降级约定:

  1. request.tag=tag1 时优先选择 标记了tag=tag1 的 provider。若集群中不存在与请求标记对应的服务,默认将降级请求 tag为空的provider;如果要改变这种默认行为,即找不到匹配tag1的provider返回异常,需设置request.tag.force=true
  2. request.tag未设置时,只会匹配tag为空的provider。即使集群中存在可用的服务,若 tag 不匹配也就无法调用,这与约定1不同,携带标签的请求可以降级访问到无标签的服务,但不携带标签/携带其他种类标签的请求永远无法访问到其他标签的服务。

【1】注意:一个服务只能有一条白名单规则,否则两条规则交叉,就都被筛选掉了

35. 配置规则

查看老版本配置规则

覆盖规则是Dubbo设计的在无需重启应用的情况下,动态调整RPC调用行为的一种能力。2.7.0版本开始,支持从服务应用两个粒度来调整动态配置。

概览

请在服务治理控制台查看或修改覆盖规则。

  • 应用粒度
# 将应用demo(key:demo)在20880端口上提供(side:provider)的所有服务(scope:application)的权重修改为1000(weight:1000)。
---
configVersion: v2.7
scope: application
key: demo
enabled: true
configs:
- addresses: ["0.0.0.0:20880"]
  side: provider
  parameters:
    weight: 1000
...
  • 服务粒度
# 所有消费(side:consumer)DemoService服务(key:org.apache.dubbo.samples.governance.api.DemoService)的应用实例(addresses:[0.0.0.0]),超时时间修改为6000ms
---
configVersion: v2.7
scope: service
key: org.apache.dubbo.samples.governance.api.DemoService
enabled: true
configs:
- addresses: [0.0.0.0]
  side: consumer
  parameters:
    timeout: 6000
...

规则详解

配置模板

---
configVersion: v2.7
scope: application/service
key: app-name/group+service+version
enabled: true
configs:
- addresses: ["0.0.0.0"]
  providerAddresses: ["1.1.1.1:20880", "2.2.2.2:20881"]
  side: consumer
  applications/services: []
  parameters:
    timeout: 1000
    cluster: failfase
    loadbalance: random
- addresses: ["0.0.0.0:20880"]
  side: provider
  applications/services: []
  parameters:
    threadpool: fixed
    threads: 200
    iothreads: 4
    dispatcher: all
    weight: 200
...

其中:

  • configVersion表示dubbo的版本
  • scope表示配置作用范围,分别是应用(application)或服务(service)粒度。必填
  • key 指定规则体作用在哪个服务或应用。 必填。
    • scope=service时,key取值为[{group}:]{service}[:{version}]的组合
  • scope=application时,key取值为application名称
  • enabled=true 覆盖规则是否生效,可不填,缺省生效。
  • configs 定义具体的覆盖规则内容,可以指定n(n>=1)个规则体。必填。
    • side,
    • applications
    • services
    • parameters
    • addresses
    • providerAddresses

对于绝大多数配置场景,只需要理清楚以下问题基本就知道配置该怎么写了:

  1. 要修改整个应用的配置还是某个服务的配置。
    • 应用:scope: application, key: app-name(还可使用services指定某几个服务)。
    • 服务:scope: service, key:group+service+version
  2. 修改是作用到消费者端还是提供者端。
    • 消费者:side: consumer ,作用到消费端时(你还可以进一步使用providerAddress, applications选定特定的提供者示例或应用)。
    • 提供者:side: provider
  3. 配置是否只对某几个特定实例生效。
    • 所有实例:addresses: ["0.0.0.0"]addresses: ["0.0.0.0:*"]具体由side值决定。
    • 指定实例:addersses[实例地址列表]
  4. 要修改的属性是哪个。

示例:

  1. 禁用提供者:(通常用于临时踢除某台提供者机器,相似的,禁止消费者访问请使用路由规则)
---
configVersion: v2.7
scope: application
key: demo-provider
enabled: true
configs:
- addresses: ["10.20.153.10:20880"]
  side: provider
  parameters:
    disabled: true
...
  1. 调整权重:(通常用于容量评估,缺省权重为 200)
---
configVersion: v2.7
scope: application
key: demo-provider
enabled: true
configs:
- addresses: ["10.20.153.10:20880"]
  side: provider
  parameters:
    weight: 200
...
  1. 调整负载均衡策略:(缺省负载均衡策略为 random)
---
configVersion: v2.7
scope: application
key: demo-consumer
enabled: true
configs:
- side: consumer
  parameters:
    loadbalance: random
...
  1. 服务降级:(通常用于临时屏蔽某个出错的非关键服务)
---
configVersion: v2.7
scope: service
key: org.apache.dubbo.samples.governance.api.DemoService
enabled: true
configs:
- side: consumer
 parameters:
   force: return null
...

36. 服务降级

可以通过服务降级功能 [1] 临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。

向注册中心写入动态配置覆盖规则:

RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));

其中:

  • mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
  • 还可以改为 mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。

【1】2.2.0 以上版本支持

37. 优雅停机

Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果用户使用 kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行。

原理

  • 服务提供方
  1. 停止时,先标记为不接收新请求,新请求过来时直接报错,让客户端重试其它机器。
  2. 然后,检测线程池中的线程是否正在运行,如果有,等待所有线程执行完成,除非超时,则强制关闭。
  • 服务消费方
  1. 停止时,不再发起新的调用请求,所有新的调用在客户端即报错。
  2. 然后,检测有没有请求的响应还没有返回,等待响应返回,除非超时,则强制关闭。
  • 设置方式

设置优雅停机超时时间,缺省超时时间是 10 秒,如果超时则强制关闭。

# dubbo.properties
dubbo.service.shutdown.wait=15000

如果 ShutdownHook 不能生效,可以自行调用,使用tomcat等容器部署的场景,建议通过扩展ContextListener等自行调用以下代码实现优雅停机

DubboShutdownHook.destroyAll();

38. 主机绑定

  • 查找顺序

缺省主机 IP 查找顺序:

 通过 LocalHost.getLocalHost() 获取本机地址。
 如果是 127.* 等 loopback 地址,则扫描各网卡,获取网卡 IP。
  • 主机配置

注册的地址如果获取不正确,比如需要注册公网地址,可以:

  1. 可以在 /etc/hosts 中加入:机器名 公网 IP,比如:
test1 205.182.23.201
  1. dubbo.xml 中加入主机地址的配置:
<dubbo:protocol host="205.182.23.201">
  1. 或在 dubbo.properties 中加入主机地址的配置:
dubbo.protocol.host=205.182.23.201
  • 端口配置

缺省主机端口与协议相关:

协议段口
dubbo20880
rmi1099
http80
hessian80
webservice80
memcached11211
redis6379

可以按照下面的方式配置端口:

  1. dubbo.xml 中加入主机地址的配置:
<dubbo:protocol name="dubbo" port="20880">
  1. 或在 dubbo.properties 中加入主机地址的配置:
dubbo.protocol.dubbo.port=20880

39. 日志适配

2.2.1 开始,dubbo 开始内置 log4j、slf4j、jcl、jdk 这些日志框架的适配[1],也可以通过以下方式显式配置日志输出策略:

  1. 命令行
java -Ddubbo.application.logger=log4j
  1. dubbo.properties 中指定
 dubbo.application.logger=log4j
  1. dubbo.xml 中配置
<dubbo:application logger="log4j" />

[1]: 自定义扩展可以参考 日志适配扩展

40. 访问日志

如果你想记录每一次请求信息,可开启访问日志,类似于apache的访问日志。注意:此日志量比较大,请注意磁盘容量。

将访问日志输出到当前应用的log4j日志:

<dubbo:protocol accesslog="true" />

将访问日志输出到指定文件:

<dubbo:protocol accesslog="http://10.20.160.198/wiki/display/dubbo/foo/bar.log" />

41. 服务容器

服务容器是一个 standalone 的启动程序,因为后台服务不需要 Tomcat 或 JBoss 等 Web 容器的功能,如果硬要用 Web 容器去加载服务提供方,增加复杂性,也浪费资源。

服务容器只是一个简单的 Main 方法,并加载一个简单的 Spring 容器,用于暴露服务。

服务容器的加载内容可以扩展,内置了 spring, jetty, log4j 等加载,可通过容器扩展点进行扩展。配置配在 java 命令的 -D 参数或者 dubbo.properties 中。

  • 容器类型

spring Container

  1. 自动加载 META-INF/spring 目录下的所有 Spring 配置。
  2. 配置 spring 配置加载位置:
dubbo.spring.config=classpath*:META-INF/spring/*.xml

Jetty Container

  1. 启动一个内嵌 Jetty,用于汇报状态。
  2. 配置:
dubbo.jetty.port=8080:配置 jetty 启动端口
dubbo.jetty.directory=/foo/bar:配置可通过 jetty 直接访问的目录,用于存放静态文件
dubbo.jetty.page=log,status,system:配置显示的页面,缺省加载所有页面

Log4j Container

  1. 自动配置 log4j 的配置,在多进程启动时,自动给日志文件按进程分目录。
  2. 配置:
dubbo.log4j.file=/foo/bar.log:配置日志文件路径
dubbo.log4j.level=WARN:配置日志级别
dubbo.log4j.subdirectory=20880:配置日志子目录,用于多进程启动,避免冲突
  • 容器启动

缺省只加载 spring

java org.apache.dubbo.container.Main

通过 main 函数参数传入要加载的容器

java org.apache.dubbo.container.Main spring jetty log4j

通过 JVM 启动参数传入要加载的容器

java org.apache.dubbo.container.Main -Ddubbo.container=spring,jetty,log4j

通过 classpath 下的 dubbo.properties 配置传入要加载的容器

dubbo.container=spring,jetty,log4j

42. ReferenceConfig 缓存

ReferenceConfig 实例很重,封装了与注册中心的连接以及与提供者的连接,需要缓存。否则重复生成 ReferenceConfig 可能造成性能问题并且会有内存和连接泄漏。在 API 方式编程时,容易忽略此问题。

因此,自 2.4.0 版本开始, dubbo 提供了简单的工具类 ReferenceConfigCache用于缓存 ReferenceConfig 实例。

使用方式如下:

ReferenceConfig<XxxService> reference = new ReferenceConfig<XxxService>();
reference.setInterface(XxxService.class);
reference.setVersion("1.0.0");
......
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
// cache.get方法中会缓存 Reference对象,并且调用ReferenceConfig.get方法启动ReferenceConfig
XxxService xxxService = cache.get(reference);
// 注意! Cache会持有ReferenceConfig,不要在外部再调用ReferenceConfig的destroy方法,导致Cache内的ReferenceConfig失效!
// 使用xxxService对象
xxxService.sayHello();

消除 Cache 中的 ReferenceConfig,将销毁 ReferenceConfig 并释放对应的资源。

ReferenceConfigCache cache = ReferenceConfigCache.getCache();
cache.destroy(reference);

缺省 ReferenceConfigCache 把相同服务 Group、接口、版本的 ReferenceConfig 认为是相同,缓存一份。即以服务 Group、接口、版本为缓存的 Key。

可以修改这个策略,在 ReferenceConfigCache.getCache 时,传一个 KeyGenerator。详见 ReferenceConfigCache 类的方法。

KeyGenerator keyGenerator = new ...
ReferenceConfigCache cache = ReferenceConfigCache.getCache(keyGenerator );

43. 分布式事务

分布式事务基于 JTA/XA 规范实现 [1]

两阶段提交:

【1】本功能暂未实现

44. 线程栈自动dump

当业务线程池满时,我们需要知道线程都在等待哪些资源、条件,以找到系统的瓶颈点或异常点。dubbo通过Jstack自动导出线程堆栈来保留现场,方便排查问题.

默认策略:

  • 导出路径,user.home标识的用户主目录
  • 导出间隔,最短间隔允许每隔10分钟导出一次

指定导出路径:

# dubbo.properties
dubbo.application.dump.directory=/tmp
<dubbo:application ...>
    <dubbo:parameter key="dump.directory" value="/tmp" />
</dubbo:application>

45. Netty4

dubbo 2.5.6版本新增了对netty4通信模块的支持,启用方式如下

provider端:

<dubbo:protocol server="netty4" />

或者

<dubbo:provider server="netty4" />

consumer端:

<dubbo:consumer client="netty4" />

注意

  1. provider端如需不同的协议使用不同的通信层框架,请配置多个protocol分别设置
  2. consumer端请使用如下形式:
<dubbo:consumer client="netty">
  <dubbo:reference />
</dubbo:consumer>
<dubbo:consumer client="netty4">
  <dubbo:reference />
</dubbo:consumer>

接下来我们会继续完善:

  1. 性能测试指标及与netty3版本的性能测试对比,我们会提供一份参考数据

46. 在Dubbo中使用高效的Java序列化(Kryo和FST)

  • 启用Kryo和FST

使用Kryo和FST非常简单,只需要在dubbo RPC的XML配置中添加一个属性即可:

<dubbo:protocol name="dubbo" serialization="kryo"/>
<dubbo:protocol name="dubbo" serialization="fst"/>
  • 注册被序列化类

要让Kryo和FST完全发挥出高性能,最好将那些需要被序列化的类注册到dubbo系统中,例如,我们可以实现如下回调接口:

public class SerializationOptimizerImpl implements SerializationOptimizer {

    public Collection<Class> getSerializableClasses() {
        List<Class> classes = new LinkedList<Class>();
        classes.add(BidRequest.class);
        classes.add(BidResponse.class);
        classes.add(Device.class);
        classes.add(Geo.class);
        classes.add(Impression.class);
        classes.add(SeatBid.class);
        return classes;
    }
}

然后在XML配置中添加:

<dubbo:protocol name="dubbo" serialization="kryo" optimizer="org.apache.dubbo.demo.SerializationOptimizerImpl"/>

在注册这些类后,序列化的性能可能被大大提升,特别针对小数量的嵌套对象的时候。

当然,在对一个类做序列化的时候,可能还级联引用到很多类,比如Java集合类。针对这种情况,我们已经自动将JDK中的常用类进行了注册,所以你不需要重复注册它们(当然你重复注册了也没有任何影响),包括:

GregorianCalendar
InvocationHandler
BigDecimal
BigInteger
Pattern
BitSet
URI
UUID
HashMap
ArrayList
LinkedList
HashSet
TreeSet
Hashtable
Date
Calendar
ConcurrentHashMap
SimpleDateFormat
Vector
BitSet
StringBuffer
StringBuilder
Object
Object[]
String[]
byte[]
char[]
int[]
float[]
double[]

由于注册被序列化的类仅仅是出于性能优化的目的,所以即使你忘记注册某些类也没有关系。事实上,即使不注册任何类,Kryo和FST的性能依然普遍优于hessian和dubbo序列化。

47. 简化注册中心URL

背景:

dubbo provider中的服务配置项有接近30个配置项。 排除注册中心服务治理需要之外,很大一部分配置项是provider自己使用,不需要透传给消费者。这部分数据不需要进入注册中心,而只需要以key-value形式持久化存储。

dubbo consumer中的配置项也有20+个配置项。在注册中心之中,服务消费者列表中只需要关注application,version,group,ip,dubbo版本等少量配置,其他配置也可以以key-value形式持久化存储。

这些数据是以服务为维度注册进入注册中心,导致了数据量的膨胀,进而引发注册中心(如zookeeper)的网络开销增大,性能降低。

现有功能Sample

当前现状一个简单展示。通过这个展示,分析下为什么需要做简化配置。

参考sample子工程: dubbo-samples-simplified-registry/dubbo-samples-simplified-registry-nosimple (跑sample前,先跑下ZKClean进行配置项清理)

dubbo-provider.xml配置

<dubbo:application name="simplified-registry-nosimple-provider"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<bean id="demoService" class="org.apache.dubbo.samples.simplified.registry.nosimple.impl.DemoServiceImpl"/>
<dubbo:service async="true" interface="org.apache.dubbo.samples.simplified.registry.nosimple.api.DemoService" 
               version="1.2.3" group="dubbo-simple" ref="demoService" 
               executes="4500" retries="7" owner="vict" timeout="5300"/>

启动provider的main方法之后,查看zookeeper的叶子节点(路径为:/dubbo/org.apache.dubbo.samples.simplified.registry.nosimple.api.DemoService/providers目录下)的内容如下:

dubbo%3A%2F%2F30.5.124.158%3A20880%2Forg.apache.dubbo.samples.simplified.registry.nosimple.api.DemoService%3Fanyhost%3Dtrue%26application%3Dsimplified-registry-xml-provider%26async%3Dtrue%26dubbo%3D2.0.2%26executes%3D4500%26generic%3Dfalse%26group%3Ddubbo-simple%26interface%3Dorg.apache.dubbo.samples.simplified.registry.nosimple.api.DemoService%26methods%3DsayHello%26owner%3Dvict%26pid%3D2767%26retries%3D7%26revision%3D1.2.3%26side%3Dprovider%26timeout%3D5300%26timestamp%3D1542361152795%26valid%3Dtrue%26version%3D1.2.3

从加粗字体中能看到有:executes, retries, owner, timeout. 但是这些字段不是每个都需要传递给dubbo ops或者dubbo consumer。 同样的,consumer也有这个问题,可以在例子中启动Consumer的main方法进行查看。

设计目标和宗旨:

期望简化进入注册中心的provider和consumer配置数量。 期望将部分配置项以其他形式存储。这些配置项需要满足:不在服务调用链路上,同时这些配置项不在注册中心的核心链路上(服务查询,服务列表)。

配置:

简化注册中心的配置,只在2.7之后的版本中进行支持。 开启provider或者consumer简化配置之后,默认保留的配置项如下:

provider:

Constant KeyKeyremark
APPLICATION_KEYapplication
CODEC_KEYcodec
EXCHANGER_KEYexchanger
SERIALIZATION_KEYserialization
CLUSTER_KEYcluster
CONNECTIONS_KEYconnections
DEPRECATED_KEYdeprecated
GROUP_KEYgroup
LOADBALANCE_KEYloadbalance
MOCK_KEYmock
PATH_KEYpath
TIMEOUT_KEYtimeout
TOKEN_KEYtoken
VERSION_KEYversion
WARMUP_KEYwarmup
WEIGHT_KEYweight
TIMESTAMP_KEYtimestamp
DUBBO_VERSION_KEYdubbo
SPECIFICATION_VERSION_KEYspecVersion新增,用于表述dubbo版本,如2.7.0

consumer:

Constant KeyKeyremark
APPLICATION_KEYapplication
VERSION_KEYversion
GROUP_KEYgroup
DUBBO_VERSION_KEYdubbo
SPECIFICATION_VERSION_KEYspecVersion新增,用于表述dubbo版本,如2.7.0

Constant Key表示来自于类org.apache.dubbo.common.Constants的字段。

下面介绍几种常用的使用方式。所有的sample,都可以查看sample-2.7

方式1:配置dubbo.properties

sample在dubbo-samples-simplified-registry/dubbo-samples-simplified-registry-xml 工程下 (跑sample前,先跑下ZKClean进行配置项清理)

dubbo.properties

dubbo.registry.simplified=true
dubbo.registry.extra-keys=retries,owner

怎么去验证呢?

provider端验证:

provider端配置

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
    <!-- optional -->
    <dubbo:application name="simplified-registry-xml-provider"/>
    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
    <bean id="demoService" class="org.apache.dubbo.samples.simplified.registry.nosimple.impl.DemoServiceImpl"/>
    <dubbo:service async="true" interface="org.apache.dubbo.samples.simplified.registry.nosimple.api.DemoService" version="1.2.3" group="dubbo-simple"
                   ref="demoService" executes="4500" retries="7" owner="vict" timeout="5300"/>

</beans>

得到的zookeeper的叶子节点的值如下:

dubbo%3A%2F%2F30.5.124.149%3A20880%2Forg.apache.dubbo.samples.simplified.registry.nosimple.api.DemoService%3Fapplication%3Dsimplified-registry-xml-provider%26dubbo%3D2.0.2%26group%3Ddubbo-simple%26owner%3Dvict%26retries%3D7%26timeout%3D5300%26timestamp%3D1542594503305%26version%3D1.2.3

和上面的现有功能sample 进行对比,上面的sample中,executes, retries, owner, timeout四个配置项都进入了注册中心。但是本实例不是:

1. 配置了:dubbo.registry.simplified=true, 默认情况下,timeout在默认的配置项列表,所以还是会进入注册中心;
2. 配置了:dubbo.registry.extra-keys=retries,owner , 所以retries,owner也会进入注册中心。

总结:timeout,retries,owner进入了注册中心,而executes没有进入。

consumer端配置

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <!-- optional -->
    <dubbo:application name="simplified-registry-xml-consumer"/>

    <dubbo:registry address="zookeeper://127.0.0.1:2181" username="xxx" password="yyy" check="true"/>

    <dubbo:reference id="demoService" interface="org.apache.dubbo.samples.simplified.registry.nosimple.api.DemoService"
                     owner="vvv" retries="4" actives="6" timeout="4500" version="1.2.3" group="dubbo-simple"/>

</beans>

得到的zookeeper的叶子节点的值如下:

consumer%3A%2F%2F30.5.124.149%2Forg.apache.dubbo.samples.simplified.registry.nosimple.api.DemoService%3Factives%3D6%26application%3Dsimplified-registry-xml-consumer%26category%3Dconsumers%26check%3Dfalse%26dubbo%3D2.0.2%26group%3Ddubbo-simple%26owner%3Dvvv%26version%3D1.2.3

  • 配置了:dubbo.registry.simplified=true , 默认情况下,application,version,group,dubbo在默认的配置项列表,所以还是会进入注册中心;

方式2:声明spring bean

sample在dubbo-samples-simplified-registry/dubbo-samples-simplified-registry-annotation 工程下 (跑sample前,先跑下ZKClean进行配置项清理)

Provider配置

privide端bean配置:

// 等同于dubbo.properties配置,用@Bean形式进行配置
        @Bean
        public RegistryConfig registryConfig() {
            RegistryConfig registryConfig = new RegistryConfig();
            registryConfig.setAddress("zookeeper://127.0.0.1:2181");
            registryConfig.setSimplified(true);
            registryConfig.setExtraKeys("retries,owner");
            return registryConfig;
        }
// 暴露服务
@Service(version = "1.1.8", group = "d-test", executes = 4500, retries = 7, owner = "victanno", timeout = 5300)
public class AnnotationServiceImpl implements AnnotationService {
    @Override
    public String sayHello(String name) {
        System.out.println("async provider received: " + name);
        return "annotation: hello, " + name;
    }
}

和上面sample中的dubbo.properties的效果是一致的。结果如下:

  • 默认情况下,timeout在默认的配置项列表,所以还是会进入注册中心;
  • 配置了retries,owner 作为额外的key进入注册中心 , 所以retries,owner也会进入注册中心。

总结:timeout,retries,owner进入了注册中心,而executes没有进入。

Consumer配置

consumer端bean配置:

  @Bean
  public RegistryConfig registryConfig() {
      RegistryConfig registryConfig = new RegistryConfig();
      registryConfig.setAddress("zookeeper://127.0.0.1:2181");
      registryConfig.setSimplified(true);
      return registryConfig;
  }

消费服务:

@Component("annotationAction")
public class AnnotationAction {

    @Reference(version = "1.1.8", group = "d-test", owner = "vvvanno", retries = 4, actives = 6, timeout = 4500)
    private AnnotationService annotationService;
    public String doSayHello(String name) {
        return annotationService.sayHello(name);
    }
}

和上面sample中consumer端的配置是一样的。结果如下:

  • 默认情况下,application,version,group,dubbo在默认的配置项列表,所以还是会进入注册中心.

注意:

如果一个应用中既有provider又有consumer,那么配置需要合并成:

    @Bean
    public RegistryConfig registryConfig() {
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress("zookeeper://127.0.0.1:2181");
        registryConfig.setSimplified(true);
        //只对provider生效
        registryConfig.setExtraKeys("retries,owner");
        return registryConfig;
    }

后续规划

本版本还保留了大量的配置项,接下来的版本中,会逐渐删除所有的配置项。

48. 启动TLS

2.7.5 版本在传输链路的安全性上做了很多工作,对于内置的 Dubbo Netty Server 和新引入的 gRPC 协议都提供了基于 TLS 的安全链路传输机制。

TLS 的配置都有统一的入口,如下所示:

Provider

SslConfig sslConfig = new SslConfig();
sslConfig.setServerKeyCertChainPath("path to cert");
sslConfig.setServerPrivateKeyPath(args[1]);
// 如果开启双向 cert 认证
if (mutualTls) {
  sslConfig.setServerTrustCertCollectionPath(args[2]);
}

ProtocolConfig protocolConfig = new ProtocolConfig("dubbo/grpc");
protocolConfig.setSslEnabled(true);

Consumer 端

if (!mutualTls) {}
    sslConfig.setClientTrustCertCollectionPath(args[0]);
} else {
    sslConfig.setClientTrustCertCollectionPath(args[0]);
    sslConfig.setClientKeyCertChainPath(args[1]);
    sslConfig.setClientPrivateKeyPath(args[2]);
}

为尽可能保证应用启动的灵活性,TLS Cert 的指定还能通过 -D 参数或环境变量等方式来在启动阶段根据部署环境动态指定,具体请参见 Dubbo 配置读取规则与 TLS 示例

Dubbo 配置读取规则:http://dubbo.apache.org/zh-cn/docs/user/configuration/configuration-load-process.html

TLS 示例:https://github.com/apache/dubbo-samples/tree/master/java/dubbo-samples-ssl

如果要使用的是 gRPC 协议,在开启 TLS 时会使用到协议协商机制,因此必须使用支持 ALPN 机制的 Provider,推荐使用的是 netty-tcnative,具体可参见 gRPC Java 社区的总结: https://github.com/grpc/grpc-java/blob/master/SECURITY.md

在服务调用的安全性上,Dubbo 在后续的版本中会持续投入,其中服务发现/调用的鉴权机制预计在接下来的版本中就会和大家见面。

49. 服务鉴权

类似支付之类的对安全性敏感的业务可能会有限制匿名调用的需求。在加固安全性方面,2.7.5 引入了基于AK/SK机制的认证鉴权机制,并且引入了鉴权服务中心,主要原理是消费端在请求需要鉴权的服务时,会通过SK、请求元数据、时间戳、参数等信息来生成对应的请求签名,通过Dubbo的Attahcment机制携带到对端进行验签,验签通过才进行业务逻辑处理。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NdgNKea7-1599302442788)(https://raw.githubusercontent.com/Ooo0oO0o0oO/res/master/auth.png)]

具体的接入方式也并不复杂:

  1. 使用者需要在微服务站点上填写自己的应用信息,并为该应用生成唯一的证书凭证。

  2. 之后在管理站点上提交工单,申请某个敏感业务服务的使用权限,并由对应业务管理者进行审批,审批通过之后,会生成对应的AK/SK到鉴权服务中心。

  3. 导入该证书到对应的应用下,并且进行配置。配置方式也十分简单,以注解方式为例:

    服务提供端,只需要设置service.auth为true,表示该服务的调用需要鉴权认证通过。param.signtrue表示需要对参数也进行校验。

@Service(parameters = {"service.auth","true","param.sign","true"})
public class AuthDemoServiceImpl implements AuthService {
}

​ 服务消费端,只需要配置好对应的证书等信息即可,之后会自动地在对这些需要认证的接口发起调用前进行签 名操作,通过与鉴权服务的交互,用户无需在代码中配置AK/SK这些敏感信息,并且在不重启应用的情况下刷新 AK/SK,达到权限动态下发的目的。

该方案目前已经提交给Dubbo开源社区,并且完成了基本框架的合并,除了AK/SK的鉴权方式之外,通过SPI机制支持用户可定制化的鉴权认证以及适配公司内部基础设施的密钥存储。

50. IDL定义服务

当前 Dubbo 的服务定义和具体的编程语言绑定,没有提供一种语言中立的服务描述格式,比如 Java 就是定义 Interface 接口,到了其他语言又得重新以另外的格式定义一遍。 2.7.5 版本通过支持 Protobuf IDL 实现了语言中立的服务定义。

日后,不论我们使用什么语言版本来开发 Dubbo 服务,都可以直接使用 IDL 定义如下服务,具体请参见示例

syntax = "proto3";

option java_multiple_files = true;
option java_package = "org.apache.dubbo.demo";
option java_outer_classname = "DemoServiceProto";
option objc_class_prefix = "DEMOSRV";

package demoservice;

// The demo service definition.
service DemoService {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

51. 消费端线程池

2.7.5 版本对整个调用链路做了全面的优化,根据压测结果显示,总体 QPS 性能提升将近 30%,同时也减少了调用过程中的内存分配开销。其中一个值得提及的设计点是 2.7.5 引入了 Servicerepository 的概念,在服务注册阶段提前生成 ServiceDescriptor 和 MethodDescriptor,以减少 RPC 调用阶段计算 Service 原信息带来的资源消耗。

  • 消费端线程池模型优化

对 2.7.5 版本之前的 Dubbo 应用,尤其是一些消费端应用,当面临需要消费大量服务且并发数比较大的大流量场景时(典型如网关类场景),经常会出现消费端线程数分配过多的问题,具体问题讨论可参见 Need a limited Threadpool in consumer side #2013

改进后的消费端线程池模型,通过复用业务端被阻塞的线程,很好的解决了这个问题。

老的线程池模型

consumer

我们重点关注 Consumer 部分:

  1. 业务线程发出请求,拿到一个 Future 实例。
  2. 业务线程紧接着调用 future.get 阻塞等待业务结果返回。
  3. 当业务数据返回后,交由独立的 Consumer 端线程池进行反序列化等处理,并调用 future.set 将反序列化后的业务结果置回。
  4. 业务线程拿到结果直接返回

2.7.5 版本引入的线程池模型

  1. 业务线程发出请求,拿到一个 Future 实例。
  2. 在调用 future.get() 之前,先调用 ThreadlessExecutor.wait(),wait 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。
  3. 当业务数据返回后,生成一个 Runnable Task 并放入 ThreadlessExecutor 队列
  4. 业务线程将 Task 取出并在本线程中执行:反序列化业务数据并 set 到 Future。
  5. 业务线程拿到结果直接返回

这样,相比于老的线程池模型,由业务线程自己负责监测并解析返回结果,免去了额外的消费端线程池开销。

关于性能优化,在接下来的版本中将会持续推进,主要从以下两个方面入手:

  1. RPC 调用链路。目前能看到的点包括:进一步减少执行链路的内存分配、在保证协议兼容性的前提下提高协议传输效率、提高 Filter、Router 等计算效率。
  2. 服务治理链路。进一步减少地址推送、服务治理规则推送等造成的内存、cpu 资源消耗。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值