官网 : https://dubbo.apache.org/zh/
1. 背景
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。
-
单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
-
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,提升效率的方法之一是将应用拆成互不相干的几个应用,以提升效率。
但是缺点则是不懂动态扩展,每次增加机器都需要修改软负载的相关配置。同时由于每一个war包都是相同的,也会造成比较大的机器浪费,比如有些简单的模块其实访问量不是太大,没必要那么多的服务。
-
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
分布式的优点很多,但是也具有一些缺点,由于多个服务之间的调用需要使用RPC进行调用,所以单次请求的速度会降低;同时由于模块较多,调用关系更加复杂。
-
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。
在服务的演进过程中,当企业开始大规模的服务化以后,远程通信带来的弊端就越来越明显了。比如说
-
服务链路变长了,如何实现对服务链路的跟踪和监控呢?
-
服务的大规模集群使得服务之间需要依赖第三方注册中心来解决服务的发现和服务的感知问题
-
服务通信之间的异常,需要有一种保护机制防止一个节点故障引发大规模的系统故障,所以要有容错机制
-
服务大规模集群会是的客户端需要引入负载均衡机制实现请求分发
而这些对于服务治理的要求,传统的RPC技术在这样的场景中显得有点力不从心,因此很多企业开始研发自己的RPC框架,比如阿里的HSF、Dubbo;京东的JSF框架、当当的dubbox、新浪的motan、蚂蚁金服的sofa等等。
有技术输出能力的公司,都会研发适合自己场景的rpc框架,要么是从0到1开发,要么是基于现有的思想结合公司业务特色进行改造。而没有技术输出能力的公司,遇到服务治理的需求时,会优先选择那些比较成熟的开源框架,而Dubbo就是其中一个。
dubbo主要是一个分布式服务治理解决方案,那么什么是服务治理?服务治理主要是针对大规模服务化以后,服务之间的路由、负载均衡、容错机制、服务降级这些问题的解决方案,而Dubbo实现的不仅仅是远程服务通信,并且还解决了服务路由、负载、降级、容错等功能。
2. 快速了解Dubbo
Dubbo是阿里巴巴内部使用的一个分布式服务治理框架,2012年开源,因为Dubbo在公司内部经过了很多的验证相对来说比较成熟,所以在很短的的时间就被很多互联网公司使用,再加上阿里出来的很多技术大牛进入各个创业公司担任技术架构以后,都以Dubbo作为主推的RPC框架使得dubbo很快成为了很多互联网公司的首要选择。并且很多公司在应用dubbo时,会基于自身业务特性进行优化和改进,所以也衍生了很多版本,比如京东的JSF、比如新浪的Motan、比如当当的dubbox.
在2014年10月份,Dubbo停止了维护。后来在2017年的9月份,阿里宣布重启Dubbo,并且对于Dubbo做好了长期投入的准备,并且在这段时间Dubbo进行了非常多的更新,目前的版本已经到了2.7。
2018年1月8日,Dubbo创始人之一梁飞在Dubbo交流群里透露了Dubbo 3.0正在动工的消息。Dubbo 3.0内核与Dubbo2.0完全不同,但兼容Dubbo 2.0。Dubbo 3.0将支持可选Service Mesh。
2018年2月份, Dubbo捐给了Apache。另外,阿里巴巴对于Spring Cloud Alibaba生态的完善,以及Spring Cloud团队对于alibaba整个服务治理生态的支持,所以Dubbo未来依然是国内绝大部分公司的首要选择。
2.1 Dubbo3 ?
如开篇所述,Dubbo 提供了构建云原生微服务业务的一站式解决方案,可以使用 Dubbo 快速定义并发布微服务组件,同时基于 Dubbo 开箱即用的丰富特性及超强的扩展能力,构建运维整个微服务体系所需的各项服务治理能力,如 Tracing、Transaction 等,Dubbo 提供的基础能力包括:
- 服务发现
- 流式通信
- 负载均衡
- 流量治理
- …
Dubbo 计划提供丰富的多语言客户端实现,其中 Java、Golang 版本是当前稳定性、活跃度最好的版本,其他多语言客户端正在持续建设中。
自开源以来,Dubbo 就被一众大规模互联网、IT公司选型,经过多年企业实践积累了大量经验。Dubbo3 是站在巨人肩膀上的下一代产品,它汲取了上一代的优点并针对已知问题做了大量优化,因此,Dubbo 在解决业务落地与规模化实践方面有着无可比拟的优势:
-
开箱即用
- 易用性高,如 Java 版本的面向接口代理特性能实现本地透明调用
- 功能丰富,基于原生库或轻量扩展即可实现绝大多数的微服务治理能力
-
超大规模微服务集群实践
- 高性能的跨进程通信协议
- 地址发现、流量治理层面,轻松支持百万规模集群实例
-
企业级微服务治理能力
- 服务测试
- 服务Mock
Dubbo3 是在云原生背景下诞生的,使用 Dubbo 构建的微服务遵循云原生思想,能更好的复用底层云原生基础设施、贴合云原生微服务架构。这体现在:
-
服务支持部署在容器、Kubernetes平台,服务生命周期可实现与平台调度周期对齐;
-
支持经典 Service Mesh 微服务架构,引入了 Proxyless Mesh 架构,进一步简化 Mesh 的落地与迁移成本,提供更灵活的选择;
-
作为桥接层,支持与 SpringCloud、gRPC 等异构微服务体系的互调互通
2.2 Dubbo的整体架构
节点角色说明
节点 | 角色说明 |
---|---|
Provider | 暴露服务的服务提供方 |
Consumer | 调用远程服务的服务消费方 |
Registry | 服务注册与发现的注册中心 |
Monitor | 统计服务的调用次数和调用时间的监控中心 |
Container | 服务运行容器 |
调用关系说明
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
3. Dubbo的简单使用
首先我们构建出三个maven项目。
首先,构建两个maven项目
-
dubbo-provider
-
dubbo-consumer
-
dubbo-api
3.1 dubbo-api
dubbo-api主要提供服务的公共契约,里面提供了对外的服务。
编写之后需要install一下,方便其他两个项目调用。
public interface UserService {
public String queryUser(String userId);
void doKill(String killid);
}
3.2 dubbo-provider
3.2.1 相关依赖
实现服务提供者项目我们首先需要引入dubbo
的相关依赖。包括如下几种:
- dubbo
- dubbo集成zookeeper
- dubbo-api,从而能实现相关接口。
- 测试相关
- 日志相关
由于我们的注册中心使用zookeeper
来实现,所以也需要引入相关依赖。
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<source.level>1.8</source.level>
<target.level>1.8</target.level>
<dubbo.version>3.0.2.1</dubbo.version>
<spring.version>5.2.8.RELEASE</spring.version>
<junit.version>4.12</junit.version>
<maven-compiler-plugin.version>3.7.0</maven-compiler-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>${spring.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-metadata-report-zookeeper</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>dubbo-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.23</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>8.5.16</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
3.2.2 dubbo配置信息
引入相关依赖后,我们需要配置好dubbo的相关配置信息,包括:
- 服务名
- 注册中心地址
- 底层通信协议等
dubbo.application.name=dubbo_provider
dubbo.registry.address=zookeeper://${zookeeper.address:127.0.0.1}:2181
#指定dubbo底层通信协议
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
创建dubbo-provider.properties文件用于取代xml中的配置属性,然后用@PropertySource加载该properties配置文件即可 ,同时开启dubbo的扫描,并且指定路径。
@Configuration
@EnableDubbo(scanBasePackages = "com.demo") //作用:扫描@DubboService注解 @DubboRef
@PropertySource("classpath:/dubbo-provider.properties")
public class ProviderConfiguration {
}
3.2.3 接口实现类
在接口的实现类上需要加上@DubboService
注解,这样才能被扫描到,想要暴露什么服务就在类上加上该注解。
@DubboService
public class UserServiceImpl implements UserService {
@Override
public String queryUser(String s) {
System.out.println(s);
System.out.println("==========provider===========" + s);
return "OK--" + s;
}
@Override
public void doKill(String s) {
System.out.println("==========provider===========" + s);
}
}
3.2.4 启动
最后开启本地zookeeper,运行即可。
public class AnnotationProvider {
public static void main(String[] args) throws InterruptedException {
new AnnotationConfigApplicationContext(ProviderConfiguration.class);
System.out.println("dubbo service started.");
//阻塞 防止项目关闭
new CountDownLatch(1).await();
}
}
3.3 dubbo-consumer
消费者项目的相关依赖和生产者项目相同即可。
3.3.1 配置信息
消费者项目引入依赖后,同样的我们先来配置相关信息。
dubbo.application.name=dubbo_consumer
dubbo.registry.address=zookeeper://${zookeeper.address:127.0.0.1}:2181
dubbo.protocol.port=29015
通过配置类将其交给Spring容器管理。
@Configuration
@EnableDubbo(scanBasePackages = "com.demo")
@PropertySource("classpath:/dubbo-consumer.properties")
public class ConsumerConfiguration {
}
3.3.2 测试
这里通过@DubboReference
注解实现相关代理,与服务端建立连接,具体在后期讲解。也是由@EnableDubbo
注解扫描到的。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ConsumerConfiguration.class)
public class AnnotationTest {
@DubboReference(check = false)
private UserService userService;
@Test
public void test(){
System.out.println(userService.queryUser("帅哥"));
}
}
以上便是dubbo的简单使用。
3.4 总结
简单总结一下上面的整个过程,其实不难发现,Dubbo这个中间件为我们提供了服务远程通信的解决方案。通过dubbo这个框架,可以开发者快速高效的构建微服务架构下的远程通信实现。
不知道大家是否发现,我们在使用dubbo发布服务,或者消费服务的时候,全程都是采用spring的配置来完成的,这样的好处是我们在学习或者使用dubbo时,如果你用过spring这个框架,那么对于它的学习难度会大大的降低。而且我们也可以看到,dubbo是完全集成Spring 的,因此后续我们去分析dubbo的源码时,还是会有一些和spring有关的内容。
4. 配置管理
上面可以看到我们的相关配置都是写在properties文件中的,但是dubbo的配置管理并不局限于此。
-
配置组件
Dubbo框架的配置项比较繁多,为了更好地管理各种配置,将其按照用途划分为不同的组件,最终所有配置项都会汇聚到URL中,传递给后续处理模块。
常用配置组件如下:
- application: Dubbo应用配置
- registry: 注册中心
- protocol: 服务提供者RPC协议
- confifig-center: 配置中心
- metadata-report: 元数据中心
- service: 服务提供者配置
- reference: 远程服务引用配置
- provider: service的默认配置或分组配置
- consumer: reference的默认配置或分组配置
- module: 模块配置
- monitor: 监控配置
- metrics: 指标配置
- ssl: SSL/TLS配置
-
配置来源
从Dubbo支持的配置来源说起,默认有6种配置来源:
- JVM System Properties,JVM -D 参数
- System environment,JVM进程的环境变量
- Externalized Confifiguration,外部化配置,从配置中心读取
- Application Confifiguration,应用的属性配置,从Spring应用的Environment中提取"dubbo"打头的属性集
- API / XML /注解等编程接口采集的配置可以被理解成配置来源的一种,是直接面向用户编程的配置采集方式
- 从classpath读取配置文件 dubbo.properties
-
覆盖关系
下图展示了配置覆盖关系的优先级,从上到下优先级依次降低:
5. Dubbo的高级用法
5.1 API的方式发布和引用服务
除了通过注解的方式发布和引用服务,我们也可以通过API 配置的方式来配置你的 Dubbo 应用。
通过API编码方式组装配置,启动Dubbo,发布及订阅服务。此方式可以支持动态创建ReferenceConfig/ServiceConfig,结合泛化调用可以满足API Gateway或测试平台的需要。
-
服务提供者
服务提供者API的方式很简单,首先我们按照之前在
properties
文件中配置的相关信息依次进行配置。首先是应用信息,然后注册地址以及协议信息,最后则是调用API进行服务的发布,需要注意的是如果我们设置了版本号,那么消费端也一定要设置版本号,否则会启动出错。
public class ProviderApi { public static void main(String[] args) throws IOException { UserServiceImpl userService = new UserServiceImpl(); //1.应用信息 ApplicationConfig applicationConfig = new ApplicationConfig(); applicationConfig.setName("dubbo_provider"); //2.注册信息 RegistryConfig registryConfig = new RegistryConfig(); registryConfig.setAddress("zookeeper://127.0.0.1:2181"); //3.协议信息 ProtocolConfig protocolConfig =new ProtocolConfig(); protocolConfig.setName("dubbo"); protocolConfig.setPort(20880); protocolConfig.setThreads(200);//服务隔离 //服务发布 ServiceConfig<UserService> serviceConfig =new ServiceConfig<>(); serviceConfig.setApplication(applicationConfig); serviceConfig.setRegistry(registryConfig); serviceConfig.setProtocol(protocolConfig); //发布那个接口 serviceConfig.setInterface(UserService.class); serviceConfig.setRef(userService); //设置版本号,消费端也需要设置版本号!!! serviceConfig.setVersion("1.0.0"); //服务发布 serviceConfig.export(); System.in.read(); } }
-
服务消费者
服务消费端和生产端类似,唯一不同的则是生产端使用的是
ServiceConfig
,而消费端使用的是ReferenceConfig
,其他的都是相同的步骤即可。@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class AnnotationTest { @Test public void refService(){ //1.应用信息 ApplicationConfig applicationConfig = new ApplicationConfig(); applicationConfig.setName("dubbo_consumer"); //2.注册信息 RegistryConfig registryConfig = new RegistryConfig(); registryConfig.setAddress("zookeeper://127.0.0.1:2181"); //引用API ReferenceConfig<UserService> referenceConfig = new ReferenceConfig<>(); referenceConfig.setApplication(applicationConfig); referenceConfig.setRegistry(registryConfig); referenceConfig.setInterface(UserService.class); referenceConfig.setVersion("1.0.0"); //服务引用 引用过程非常的重,如果想要API方式去引用服务,这个对象需要缓存 UserService userService = referenceConfig.get(); System.out.println(userService.queryUser("帅哥")); } }
5.2 启动时检查
在启动时检查依赖的服务是否可用。
Dubbo缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,组织Spring初始化完成,以便上线时,能及早发现问题,默认check=true
。
我们可以通过check=flase
来关闭检查,比如测试的时候有些服务不关心,或者出现了循环依赖,必须有一方先启动的时候,我们就可以关闭检查。
另外,如果你的spring容器时懒加载的,或者通过API编程延迟引用服务,请关闭check,否则服务临时不可用时会抛出异常,拿到null,如果check=false
,那么总会返回引用,当服务恢复的时候能自动连上。
使用注解的时候我们一般都是直接设置属性即可:
@DubboReference(check = false,version = "1.0")
private UserService userService;
我们还可以通过下方的其他方式设置:
关闭某个服务的启动时检查 (没有提供者时报错):
<dubbo:reference interface="com.demo.UserService" check="false" />
关闭所有服务的启动时检查 (没有提供者时报错):
<dubbo:consumer check="false" />
关闭注册中心启动时检查 (注册订阅失败时报错):
<dubbo:registry check="false" />
通过 dubbo.properties
dubbo.reference.com.demo.UserService.check=false
dubbo.consumer.check=false
dubbo.registry.check=false
通过 -D 参数
java -Ddubbo.reference.com.demo.UserService.check=false
java -Ddubbo.consumer.check=false
java -Ddubbo.registry.check=false
5.3 直连提供者
在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连。点对点直连方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。
可以通过如下方式进行配置:
@DubboReference(check = false,url = "dubbo://localhost:20880")
直连方式的连接速度肯定更快,毕竟没有走负载均衡去选择的过程。但是几乎使用不到,可能也就我们单元测试的时候使用下。
5.4 重试次数配置
Dubbo服务在尝试调用一次后,如果出现非业务异常(超时,服务不可用等。)Dubbo默认会进行额外的最多2次重试。
重试次数支持自定义配置:
-
通过注解进行配置
@DubboReference(check = false,version = "1.0",retries = 3) private UserService userService;
5.5 集群容错
在集群调用失败时,Dubbo 提供了多种容错方案,默认为 failover 重试。
各节点关系:
-
这里的 Invoker 是 Provider 的一个可调用 Service 的抽象, Invoker 封装了 Provider 地址及Service 接口信息
-
Directory 代表多个 Invoker ,可以把它看成 List ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更
-
Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker ,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
-
Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等
-
LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选
集群容错模式:
Failover Cluster
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=“2” 来设置重试次数(不含第一次)。该配置为默认配置。
Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过forks=“2” 来设置最大并行数。
Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
Available Cluster
调用目前可用的实例(只调用一个),如果当前没有可用的实例,则抛出异常。通常用于不需要负载均衡的场景。
配置:
@DubboReference(check = false,version = "1.0",retries = 3,cluster = "failover")
private UserService userService;
5.6 负载均衡
在集群负载均衡时,Dubbo 提供了多种均衡策略,默认为 random 随机调用。
具体实现上,Dubbo 提供的是客户端负载均衡,即由 Consumer 通过负载均衡算法得出需要将请求提交到哪个Provider 实例。
负载均衡策略
目前 Dubbo 内置了如下负载均衡算法,用户可直接配置使用:
算法 | 特性 | 备注 |
---|---|---|
RandomLoadBalance | 加权随机 | 默认算法,默认权重相同 |
RoundRobinLoadBalance | 加权轮询 | 借鉴于 Nginx 的平滑加权轮询算法,默认权重相同 |
LeastActiveLoadBalance | 最少活跃优先 + 加权随机 | 背后是能者多劳的思想 |
ShortestResponseLoadBalance | 最短响应优先 + 加权随机 | 更加关注响应速度 |
ConsistentHashLoadBalance | 一致性 Hash | 确定的入参,确定的提供者,适用于有状态请求;(可以理解为hash环) |
配置:
<dubbo:service interface="..." loadbalance="roundrobin" />
5.7 服务分组
当一个接口有多种实现时,可以用 group 区分。
-
服务提供者
我们在服务提供者中给接口创建两个实现类,通过
group
属性区分。@DubboService(group = "groupImpl1") public class GroupImpl1 implements Group { @Override public String doSomething(String param) { return "GroupImpl1"; } }
@DubboService(group = "groupImpl2") public class GroupImpl2 implements Group { @Override public String doSomething(String param) { return "GroupImpl2"; } }
-
服务消费者
在消费端我们也需要通过
group
来指定调用那个实现类。@DubboReference(group = "groupImpl1") private Group group;
5.8 分组聚合
通过分组对结果进行聚合并返回聚合后的结果,比如菜单服务,用group区分同一接口的多种实现,现在消费方需从每种group中调用一次并返回结果,对结果进行合并之后返回,这样就可以实现聚合菜单项。
使用分组聚合,我们需要使用到Dubbo中的SPI机制,我们可以先查看dubbo中的相关配置文件,可以看到dubbo并不支持我们的string类型的分组聚合,所以这里需要我们自己去实现。
map=org.apache.dubbo.rpc.cluster.merger.MapMerger
set=org.apache.dubbo.rpc.cluster.merger.SetMerger
list=org.apache.dubbo.rpc.cluster.merger.ListMerger
byte=org.apache.dubbo.rpc.cluster.merger.ByteArrayMerger
char=org.apache.dubbo.rpc.cluster.merger.CharArrayMerger
short=org.apache.dubbo.rpc.cluster.merger.ShortArrayMerger
int=org.apache.dubbo.rpc.cluster.merger.IntArrayMerger
long=org.apache.dubbo.rpc.cluster.merger.LongArrayMerger
float=org.apache.dubbo.rpc.cluster.merger.FloatArrayMerger
double=org.apache.dubbo.rpc.cluster.merger.DoubleArrayMerger
boolean=org.apache.dubbo.rpc.cluster.merger.BooleanArrayMerger
SPI文件配置
首先我们在消费端的resources下创建META-INF文件夹并在其下面创建dubbo文件夹,然后在dubbo文件夹下面创建org.apache.dubbo.rpc.cluster.Merger文件,这些都必须和我们刚刚看到的配置文件名相同,在该文件下写好Merger的实现类,如:
string=com.demo.merger.StringMerger
StringMerger类
该类需要实现Merger
接口,然后对多实例返回的结果进行处理即可。
public class StringMerger implements Merger<String> {
//items 代表多个group实例的返回结果
@Override
public String merge(String... items) {
if (items.length == 0) {
return null;
}
StringBuilder builder = new StringBuilder();
for (String item : items) {
if (item != null) {
builder.append(item).append("-");
}
}
return builder.toString();
}
}
消费端配置修改
SPI相关处理好之后,我们就可以修改消费端的配置信息了。
这个时候不需要指定某个实例了,而是可以直接使用*
代表所有,当然也可以选择某几个来合并。
//分组聚合
// * 代表所有接口的实例 也可以选择某几个实例合并 group1,group2
@DubboReference(check = false, group = "*", parameters = {"merger", "true"})
private Group group;
5.9 多版本
在 Dubbo 中为同一个服务配置多个版本。
当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。
可以按照以下的步骤进行版本迁移:
-
在低压力时间段,先升级一半提供者为新版本
-
再将所有消费者升级为新版本
-
然后将剩下的一半提供者升级为新版本
生产者配置
创建两个实现类,给他们设置不同的版本号。
@DubboService(version = "1.0.0")
public class VersionServiceImpl1 implements VersionService {
@Override
public String version(String param) {
return "VersionServiceImpl1";
}
}
@DubboService(version = "2.0.0")
public class VersionServiceImpl2 implements VersionService {
@Override
public String version(String param) {
return "VersionServiceImpl2";
}
}
消费者配置
消费端我们只需要在引用的时候指定好版本号即可,类似上文的分组。
//多版本
@DubboReference(check = false,version = "2.0.0")
private VersionService versionService;
5.10 参数验证
参数验证功能是基于 JSR303 实现的,用户只需标识 JSR303 标准的验证 annotation,并通过声明 filter 来实现验证。
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>
参数验证类
public class ValidationParamter implements Serializable {
/**
* @Fields serialVersionUID TODO
*/
private static final long serialVersionUID = 32544321432L;
@NotNull
@Size(min = 2, max = 20)
private String name;
@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 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;
}
}
生产者
@DubboService
public class ValidationServiceImpl implements ValidationService {
@Override
public void save(ValidationParamter parameter) {
System.out.println("save");
}
@Override
public void update(ValidationParamter parameter) {
System.out.println("update");
}
@Override
public void delete(long id, String operation) {
System.out.println("delete");
}
}
消费者
//参数验证
@DubboReference(check = false, validation = "true")
private ValidationService validationService;
/**
* 测试参数验证
*/
@Test
public void validationTest() {
ValidationParamter paramter = new ValidationParamter();
paramter.setName("帅哥");
paramter.setAge(111);
paramter.setLoginDate(new Date(System.currentTimeMillis() - 10000000));
paramter.setExpiryDate(new Date(System.currentTimeMillis() + 10000000));
validationService.save(paramter);
}
5.11 泛化调用
实现一个通用的服务测试框架,可通过 GenericService
调用所有服务实现。
泛化接口调用方式主要用于客户端没有 API 接口及模型类元的情况,参数及返回值中的所有 POJO
均用 Map 表示,通常用于框架集成,比如:实现一个通用的服务测试框架,可通过 GenericService
调用所有服务实现。
@Test
public void genericTest() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("dubbo_consumer");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2184");
ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(applicationConfig);
referenceConfig.setInterface("com.example.dubboapi.service.UserService"); //这个是使用泛化调用
referenceConfig.setGeneric(true);
referenceConfig.setVersion("1.0");
GenericService genericService = referenceConfig.get();
Object result = genericService.$invoke("queryUser", new String[]{"java.lang.String"}, new Object[]{"Jack"});
System.out.println(result);
}
5.12 参数回调
例如,一个支付业务,消费者A调用生产者B的场景,B是进行支付操作,当B执行完支付后需要回调A的接口来修改订单状态,这时候就可以使用参数回调的功能。
参数回调方式与调用本地 callback 或 listener 相同,只需要在 Spring 的配置文件中声明哪个参数是callback 类型即可。Dubbo 将基于长连接生成反向代理,这样就可以从服务器端调用客户端逻辑。
生产者
@DubboService(methods = {@Method(name = "addListener", arguments = {@Argument(index = 1, callback = true)})})
public class CallbackServiceImpl implements CallbackService {
@Override
public void addListener(String key, CallbackListener listener) {
//1.支付操作
System.out.println("支付操作");
//2.回调客户端操作
listener.changed(getChanged(key));
}
private String getChanged(String key) {
return "changed:" + new SimpleDateFormat("yyyy-MM-dd").format(new Date());
}
}
服务发布这里的配置信息是什么意思呢?@Method
是设置方法,通过@Argument
来设置那个参数是进行回调的,我们这里就是设置第二个参数CallbackListener
回调。
消费者
//参数回调
@DubboReference(check = false)
private CallbackService callbackService;
/**
* 测试参数回调
*/
@Test
public void callbackTest() {
callbackService.addListener("测试啦", new CallbackListener() {
@Override
public void changed(String msg) {
//处理回调逻辑
//修改订单状态
System.out.println("回调逻辑,修改状态。" + msg);
}
});
}
消费端调用的时候直接在在回调参数里面处理回调逻辑即可。
5.13 本地存根
有时候调用后端接口需要有前置条件,例如参数满足条件,例如权限验证成功的时候才允许调用后端接口,这时候就可以用本地存根的功能。
在 Dubbo 中利用本地存根在客户端执行部分逻辑,远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,比如:做 ThreadLocal
缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把Proxy 通过构造函数传给 Stub ,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
生产者
生产者的代码相对来说比较简单,直接接口发布就可以了。
@DubboService
public class StubServiceImpl implements StubService {
@Override
public String stub(String param) {
System.out.println("本地存根业务逻辑处理");
return param;
}
}
消费者
消费端我们首先需要创建代理对象,这里就可以理解为一个简单的静态代理模式。
这里我们需要注意两个步骤,首先是必须实现接口;然后是定义构造函数,从而传递客户端的引用;这是不是就是一个很简单的静态代理模式。
public class LocalStubProxy implements StubService {
private StubService stubService;
public LocalStubProxy(StubService stubService) {
this.stubService = stubService;
}
//在这里对远程调用进行包装
@Override
public String stub(String param) {
try {
//1、校验
System.out.println("校验");
//2、如何校验通过 调用接口
return stubService.stub("帅哥");
} catch (Exception e) {
System.out.println("异常处理 === 服务降级");
return "ERROE";
}
}
}
在进行服务引用的时候,我们需要添加stub属性并指定我们刚刚创建的代理类,这个时候debug测试,可以发现会进入到我们的代理类中。
//本地存根 指定代理类
@DubboReference(check = false ,stub = "com.demo.stud.LocalStubProxy")
private StubService stubService;
/**
* 测试本地存根
*/
@Test
public void stubTest() {
System.out.println(stubService.stub("测试"));
}
5.14 本地伪装
该功能通常用于服务降级,当调用服务端失败时,我们不希望返回一个exception给调用方,而是把错误包装一下,实际上返回的是包装后的信息,是一个降级的信息,是一个对用户友好的信息。
本地伪装通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过 Mock 数据返回授权失败。
虽然本地存根也可以,但是本地存根需要我们在代码中通过try-catch
来进行异常的捕获,而我们使用本地伪装,则是框架层面替我们来捕获异常,并进行服务降级。
生产者
生产端还是很简单的代码,我们在mock方法里面认为的制造了超时异常,方便我们调用的时候进行测试。
@DubboService
public class MockServiceImpl implements MockService {
@Override
public String mock(String param) throws InterruptedException {
System.out.println("mock的业务处理");
Thread.sleep(100000);
System.out.println("mock的业务处理完成");
return "mock";
}
@Override
public String queryArea(String areaCode) {
return areaCode;
}
@Override
public String queryUser(String userCode) {
return userCode;
}
}
在消费端有三种方式可以进行本地伪装:
方式一
在服务引用的时候使用mock=“true”
属性,这是采用的约定俗成的方式,那么定义降级逻辑是必须满足两点:
1、类名必须是,接口名+“Mock”,例如MockServiceMock
2、类必须定义在包的同路径下
调用RPC接口出现异常的时候,会进行服务降级处理。
//本地伪装
@DubboReference(check = false ,mock = "true") //约定大于配置
private MockService mockService;
public class MockServiceMock implements MockService {
@Override
public String mock(String param) throws InterruptedException {
System.out.println(this.getClass().getName() + "--mock");
return param;
}
@Override
public String queryArea(String areaCode) {
System.out.println(this.getClass().getName() + "--queryArea");
return areaCode;
}
@Override
public String queryUser(String userCode) {
System.out.println(this.getClass().getName() + "--queryUser");
return userCode;
}
}
方式二
如果不想被束缚,我就是不想按照你的约定来,那我们可以使用mock=“com.demo.mock.LockMockService”
,配置一个具体实现类的方式,这样就是自定义的降级逻辑。
@DubboReference(check = false ,mock = "com.demo.mock.LocalMockService") //直接指定自定义接口
private MockService mockService;
public class LocalMockService implements MockService {
@Override
public String mock(String param) throws InterruptedException {
System.out.println(this.getClass().getName() + "--mock");
return param;
}
@Override
public String queryArea(String areaCode) {
System.out.println(this.getClass().getName() + "--queryArea");
return areaCode;
}
@Override
public String queryUser(String userCode) {
System.out.println(this.getClass().getName() + "--queryUser");
return userCode;
}
}
方式三
最后一种方式就是使用mock=“force:xxx”
属性,以force作为前缀的方式就表示是强制降级,强制降级就表示客户端调用是不会到达接口提供方的,这种方式也不会出现在代码中,一般都是通过dubbo-admin服务治理的方式动态修改。
@DubboReference(check = false ,mock = "force:return 帅哥") //强制降级
private MockService mockService;
5.15 异步调用
有时候接口的业务逻辑比较复杂,执行时间比较长,客户端调用的时候不希望一直等待结果的响应,而是能并行的执行其他接口的调用,这时候就可以使用异步调用。异步调用接口提供方是非异步的。
从 2.7.0 开始,Dubbo 的所有异步编程接口开始以 CompletableFuture
为基础。基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
生产者
我们在asynctoDo
方法中模拟异步调用,为了使客户端等待时间久一点,我们在这里循环等待下。
@DubboService
public class AsyncServiceImpl implements AsyncService {
//模拟异步调用
@Override
public String asynctoDo(String name) {
for (int i = 0; i < 10; i++) {
System.out.println("异步调用:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "asynctoDo " + name;
}
}
消费者
想要使用异步调用,我们在引用服务的时候需要进行相关配置,首先是设置好超时时间,因为我们在业务代码中耗时太久,防止接口调用超时;然后就是指定好具体那个方法需要异步调用。
在测试的时候我们需要获得CompletableFuture
从而获取异步调用的返回结果。
//异步调用
@DubboReference(check = false, timeout = 1000000000, methods = {@Method(name = "asynctoDo", async = true)})
private AsyncService asyncService;
/**
* 测试异步调用
*/
@Test
public void asyncTest() throws InterruptedException {
String cc = asyncService.asynctoDo("cc");
System.out.println("main:" + cc);
System.out.println("并行调用其他接口");
Thread.sleep(2000);
//需要拿到异步调用的返回结果
CompletableFuture<Object> future = RpcContext.getContext().getCompletableFuture();
future.whenComplete((value, exception) -> {
//说明没有异常
if (exception == null) {
System.out.println("正常返回值:" + value);
} else {
exception.printStackTrace();
}
});
//这里是主线程阻塞等待返回结果
Thread.currentThread().join();
}
5.16 异步执行
有时候业务代码比较复杂,执行时间很长,这就会导致占用框架的线程池资源,这样就会导致大量的请求得不到处理,这时候就可以采用异步执行,可以把业务请求的逻辑交给自己定义的业务线程池,这样就可以释放框架线程池,让框架线程池去处理其他的业务请求逻辑。
Provider端异步执行将阻塞的业务从Dubbo内部线程池切换到业务自定义线程,避免Dubbo线程池的过度占用,有助于避免不同服务间的互相影响。异步执行无异于节省资源或提升RPC响应性能,因为如果业务执行需要阻塞,则始终还是要有线程来负责执行。
注意:
Provider 端异步执行和 Consumer 端异步调用是相互独立的,你可以任意正交组合两端配置
- Consumer同步 - Provider同步
- Consumer异步 - Provider同步
- Consumer同步 - Provider异步
- Consumer异步 - Provider异步
生产者
在生产端通过CompletableFuture
进行异步执行。
@DubboService
public class AsyncServiceImpl implements AsyncService {
@Override
public CompletableFuture<String> doOne(String name) {
return CompletableFuture.supplyAsync(() -> {
System.out.println("doOne的执行业务处理");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "doOne--OK";
});
}
}
消费者
测试异步执行的时候,我们可以讲async属性设置为false;然后通过CompletableFuture
获取返回值即可。
@DubboReference(check = false, timeout = 1000000000, methods = {@Method(name = "doOne", async = false)})
private AsyncService asyncService;
/**
* 测试异步执行
*/
@Test
public void asyncDoOneTest() throws InterruptedException {
CompletableFuture<String> future = asyncService.doOne("cc");
future.whenComplete((value, exception) -> {
//说明没有异常
if (exception == null) {
System.out.println("正常返回值:" + value);
} else {
exception.printStackTrace();
}
});
//这里是主线程阻塞等待返回结果
Thread.currentThread().join();
}
5.17 配置中心
在演示测试案例的时候,我们都是使用properties文件配置相关信息的,但是存在一些问题,为了解决以下问题,我们可以使用配置中心来处理。
-
外部化配置:启动配置的集中式存储 (简单理解为 dubbo.properties 的外部化存储)。
-
服务治理:服务治理规则的存储与通知。
-
动态配置:控制动态开关或者动态变更属性值
启用动态配置,以 Zookeeper 为例,在dubbo.properties里面添加配置:
#配置中心
dubbo.config-center.address=zookeeper://127.0.0.1:2181
配置中心默认的存储结构:
-
namespace,用于不同配置的环境隔离。
-
config,Dubbo约定的固定节点,不可更改,所有配置和服务治理规则都存储在此节点下。
-
dubbo/application,分别用来隔离全局配置、应用级别配置:dubbo是默认group值,application对应应用名
-
dubbo.properties,此节点的node value存储具体配置内容
当然了,我们不是通过如上配置就可以开启配置中心的,我们还需要将相关配置写入zk中。
public class ZKTools {
private static String zookeeperHost = System.getProperty("zookeeper.address", "127.0.0.1");
private static CuratorFramework client;
public static void main(String[] args) throws Exception {
generateDubboProperties();
}
public static void generateDubboProperties() {
client = CuratorFrameworkFactory.newClient(zookeeperHost + ":2181", 60 * 1000, 60 * 1000,
new ExponentialBackoffRetry(1000, 3));
client.start();
generateDubboPropertiesForGlobal();
generateDubboPropertiesForProvider();
generateDubboPropertiesForConsumer();
}
public static void generateDubboPropertiesForGlobal() {
String str = "dubbo.registry.address=zookeeper://" + zookeeperHost + ":2181\n" +
"#global config for consumer\n" +
"dubbo.consumer.timeout=6000\n" +
"#global config for provider\n" +
"dubbo.protocol.port=20990\n" +
"#dubbo.protocol.serialization=protobuf\n" +
"dubbo.provider.timeout=5000\n" +
"dubbo.application.qos.accept.foreign.ip=true\n" +
"#dubbo.metadata-report.address=zookeeper://127.0.0.1:2181\n";
System.out.println(str);
try {
String path = "/dubbo/config/dubbo/dubbo.properties";
if (client.checkExists().forPath(path) == null) {
client.create().creatingParentsIfNeeded().forPath(path);
}
setData(path, str);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void generateDubboPropertiesForConsumer() {
String str = "dubbo.consumer.timeout=6666\n" + "dubbo.protocol.port=20991\n";
System.out.println(str);
try {
String path = "/dubbo/config/dubbo_consumer/dubbo.properties";
if (client.checkExists().forPath(path) == null) {
client.create().creatingParentsIfNeeded().forPath(path);
}
setData(path, str);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void generateDubboPropertiesForProvider() {
String str = "dubbo.protocol.threadpool=fixed\n" +
"dubbo.protocol.threads=100";
System.out.println(str);
try {
String path = "/dubbo/config/dubbo_provider/dubbo.properties";
if (client.checkExists().forPath(path) == null) {
client.create().creatingParentsIfNeeded().forPath(path);
}
setData(path, str);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void setData(String path, String data) throws Exception {
client.setData().forPath(path, data.getBytes());
}
private static String pathToKey(String path) {
if (StringUtils.isEmpty(path)) {
return path;
}
return path.replace("/dubbo/config/", "").replaceAll("/", ".");
}
}
然后在启动的时候调用即可。
public class AnnotationProvider {
public static void main(String[] args) throws InterruptedException {
//将配置信息写入zk
ZKTools.generateDubboProperties();
new AnnotationConfigApplicationContext(ProviderConfiguration.class);
System.out.println("dubbo service started.");
//阻塞 防止项目关闭
new CountDownLatch(1).await();
}
}
启动成功后,可以发现相关配置信息已经写入ZK中了。
Dubbo中的高级用法远远不止于此,还有些用法后续会在进行补充。