SpringCloud Alibaba系列——4Dubbo简介与应用(下)
作者 | 宇木木兮
来源 |今日头条
承接上文,本文将介绍Dubbo的高级应用
第3章 Dubbo高级用法
3.1 启动时检查
通过spring配置文件
关闭某个服务的启动时检查 (没有提供者时报错):
<dubbo:reference interface="com.foo.BarService" check="false" />
关闭所有服务的启动时检查 (没有提供者时报错):
<dubbo:consumer check="false" />
关闭注册中心启动时检查 (注册订阅失败时报错):
<dubbo:registry check="false" />
通过 dubbo.properties
dubbo.reference.com.foo.BarService.check=false
dubbo.consumer.check=false
dubbo.registry.check=false
通过 -D 参数
java -Ddubbo.reference.com.foo.BarService.check=false
java -Ddubbo.consumer.check=false
java -Ddubbo.registry.check=false
- api
public interface ClusterService {
String getClusterService(String str);
}
- 服务端
@DubboService(timeout = 1000000) public class ClusterServiceImpl implements ClusterService { @Override public String getClusterService(String str) { return "高级应用集群容错:"+str; } }
- 消费端
3.2 直连提供者@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestClusterService { //启动时检查和直连 @DubboReference(check = false) private ClusterService clusterService; @Test public void test(){ System.out.println(clusterService.getClusterService("eclipse2019")); } }
Dubbo 中点对点的直连方式
在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直连方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。
配置:
<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService" url="dubbo://localhost:20890" />
- api和服务提供端代码跟启动是检查一样
- 消费端
3.3 集群容错@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestClusterService { //启动时检查和直连 @DubboReference(check = false,url = "dubbo://localhost:20880") private ClusterService clusterService; @Test public void test(){ System.out.println(clusterService.getClusterService("eclipse2019")); } }
集群调用失败时,Dubbo 提供的容错方案
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。各节点关系:
- 这里的Invoker是Provider的一个可调用Service的抽象,Invoker封装了Provider地址及Service接口信息
- Directory代表多个 Invoker,可以把它看成List<Invoker>,但与List不同的是,它的值可能是动态变化的,比如注册中心推送变更
- Cluster将Directory中的多个Invoker伪装成一个Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
- Router负责从多个Invoker中按路由规则选出子集,比如读写分离,应用隔离等
- LoadBalance负责从多个Invoker中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选
3.3.1 集群容错模式
Failover Cluster
失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过retries="2" 来设置重试次数(不含第一次)。该配置为缺省配置
Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过forks="2"来设置最大并行数。
Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
Available Cluster
调用目前可用的实例(只调用一个),如果当前没有可用的实例,则抛出异常。通常用于不需要负载均衡的场景。
配置:
@reference(cluster = "broadcast", parameters = {"broadcast.fail.percent", "20"})
- api端与启动时检查代码一致
- 服务端
@DubboService(timeout = 1000000) public class ClusterServiceImpl implements ClusterService { @Override public String getClusterService(String str) { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return "高级应用集群容错:"+str; } }
- 消费端
3.4 负载均衡@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestClusterService { @DubboReference(check = false,cluster="broadcast",parameters = {"broadcast.fail.percent", "20"},timeout = 100) private ClusterService clusterService; @Test public void test(){ System.out.println(clusterService.getClusterService("eclipse2019")); } }
Dubbo 提供的集群负载均衡策略
在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用。
具体实现上,Dubbo 提供的是客户端负载均衡,即由 Consumer 通过负载均衡算法得出需要将请求提交到哪个Provider 实例。
负载均衡策略
目前 Dubbo 内置了如下负载均衡算法,用户可直接配置使用:
配置:
<dubbo:service interface="..." loadbalance="roundrobin" />
3.5 服务分组
使用服务分组区分服务接口的不同实现
当一个接口有多种实现时,可以用 group 区分。
- api
public interface GroupService {
String getGroupServiceStr(String str);
}
- 服务端
@DubboService(group = "group01") public class GroupServiceImpl01 implements GroupService { @Override public String getGroupServiceStr(String str) { return "我是group1111,返回:"+str; } }
@DubboService(group = "group02") public class GroupServiceImpl02 implements GroupService { @Override public String getGroupServiceStr(String str) { return "我是group2222,返回:"+str; } }
- 消费端
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestGroupService { @DubboReference(check = false,group = "group01") private GroupService groupService; @Test public void test(){ System.out.println(groupService.getGroupServiceStr("eclipse2019")); } }
3.6 分组聚合
通过分组对结果进行聚合并返回聚合后的结果
通过分组对结果进行聚合并返回聚合后的结果,比如菜单服务,用group区分同一接口的多种实现,现在消费方需从每种group中调用一次并返回结果,对结果进行合并之后返回,这样就可以实现聚合菜单项。
- api和服务端代码跟服务分组的代码一致
- 消费者配置:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestGroupService { @DubboReference(check = false,group = "*",parameters = {"merger","true"}) private GroupService groupService; @Test public void test(){ System.out.println(groupService.getGroupServiceStr("eclipse2019")); } }
SPI文件配置 在resources下创建META-INF文件夹并在其下面创建dubbo文件夹,然后在dubbo文件夹下面创建
org.apache.dubbo.rpc.cluster.Merger文件,在该文件下写好Merger的实现类,如:
string=com.example.merge.StringMerger
StringMerger类:
public class StringMerger implements Merger<String> {
//定义了所有group实现类的返回值的合并规则
@Override
public String merge(String... items) {
if(items.length == 0) {
return null;
}
StringBuilder builder = new StringBuilder();
for (String string : items) {
if(string != null) {
builder.append(string).append("----");
}
}
return builder.toString();
}
}
3.7 多版本
在 Dubbo 中为同一个服务配置多个版本
当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。
可以按照以下的步骤进行版本迁移:
- 在低压力时间段,先升级一半提供者为新版本
- 再将所有消费者升级为新版本
- 然后将剩下的一半提供者升级为新版本
- api
public interface VersionService {
String getVersionServiceStr(String str);
}
- 服务端
@DubboService(version = "1.0.0") public class VersionServiceImpl01 implements VersionService { @Override public String getVersionServiceStr(String str) { return "调用第一个版本,返回值:"+str; } }
@DubboService(version = "1.0.1") public class VersionServiceImpl02 implements VersionService { @Override public String getVersionServiceStr(String str) { return "调用第二个版本,返回值:"+str; } }
- 消费端
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestVersionService { @DubboReference(check = false,version = "1.0.0") private VersionService versionService; @Test public void test(){ System.out.println(versionService.getVersionServiceStr("eclipse2019")); } }
3.8 参数验证
在 Dubbo 中进行参数验证
参数验证功能是基于 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>
- api
参数验证类:
public class ValidationParamter implements Serializable {
private static final long serialVersionUID = 32544321432L;
@NotNull
@Size(
min = 2,
max = 20
)
private String name;
@Min(18L)
@Max(100L)
private int age;
@Past
private Date loginDate;
@Future
private Date expiryDate;
}
public interface ValidationService {
void save(ValidationParamter parameter);
void update(ValidationParamter parameter);
void delete(@Min(1) long id,
@NotNull @Size(min = 2, max = 16) String operation);
}
- 服务端
@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方法"); } }
- 消费端
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestValidationService { @DubboReference(check = false,validation = "true") private ValidationService validationService; @Test public void validation() { ValidationParamter paramter = new ValidationParamter(); paramter.setName("eclipse2019"); paramter.setAge(98); paramter.setLoginDate(new Date(System.currentTimeMillis() - 10000000)); paramter.setExpiryDate(new Date(System.currentTimeMillis() + 10000000)); validationService.save(paramter); } }
3.9 使用泛化调用
做测试的,没有使用场景
实现一个通用的服务测试框架,可通过 GenericService 调用所有服务实现
泛化接口调用方式主要用于客户端没有 API 接口及模型类元的情况,参数及返回值中的所有 POJO 均用 Map 表示,通常用于框架集成,比如:实现一个通用的服务测试框架,可通过 GenericService 调用所有服务实现。
消费者:
@Test
@Test
public void usegeneric() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("dubbo_consumer");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://192.168.67.139:2184");
ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(applicationConfig);
referenceConfig.setInterface("com.eclipse2019.service.UserService");
//这个是使用泛化调用
referenceConfig.setGeneric(true);
GenericService genericService = referenceConfig.get();
Object result = genericService.$invoke("queryUser", new String[]{"java.lang.String"},
new Object[]{"eclipse2019"});
System.out.println(result);
}
3.10 参数回调
通过参数回调从服务器端调用客户端逻辑
参数回调方式与调用本地 callback 或 listener 相同,只需要在 Spring 的配置文件中声明哪个参数是 callback 类型即可。Dubbo 将基于长连接生成反向代理,这样就可以从服务器端调用客户端逻辑。
- api
public interface CallbackService {
void addListener(String key, CallbackListener listener);
}
- 服务端
@DubboService(methods = {@Method(name = "addListener", arguments = {@Argument(index = 1,callback = true)})}) public class CallbackServiceImpl implements CallbackService { @Override public void addListener(String key, CallbackListener listener) { listener.changed(getChanged(key)); } private String getChanged(String key) { return "Changed: " + new SimpleDateFormat("yyyy-MM-dd:mm:ss").format(new Date()); } }
- 消费者:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestCallbaskService { @DubboReference(check = false) private CallbackService callbackService; @Test public void test(){ callbackService.addListener("eclipse2019", new CallbackListener() { @Override public void changed(String msg) { System.out.println("又回调回来了" + msg); } }); } }
3.11 本地存根
在 Dubbo 中利用本地存根在客户端执行部分逻辑
远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,比如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成Proxy 实例,会把 Proxy 通过构造函数传给 Stub 1,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
- api
public interface StubService { String stub(String param); }
- 服务端
@DubboService public class StubServiceImpl implements StubService { @Override public String stub(String param) { return "本地存根就是在消费端干一些事,返回:"+param; } }
- 消费端
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestStubService { @DubboReference(check = false,stub = "com.example.stub.LocalStubProxy") private StubService stubService; @Test public void stub() { System.out.println(stubService.stub("eclipse2019")); } }
代理层:
/*
* 1、必须实现服务端的接口
* 2、要定义构造函数(传递客户端引用的实例)
* 接管了studService的调用
*
* 只有这个类才会决定要不要远程调用
*/
public class LocalStubProxy implements StubService {
private StubService stubService;
//这个必须要,传stubService实例本身
public LocalStubProxy(StubService stubService) {
this.stubService = stubService;
}
@Override
public String stub(String param) {
//代码在客户端执行,你可以在客户端做ThreadLocal本地缓存,或者校验参数之类工作的
try {
//用目标对象掉对应的方法 远程调用
return stubService.stub(param);
}catch (Exception e) {
//用来降级
System.out.println("降级数据");
}
//掉完后又执行代码
return null;
}
}
3.12 本地伪装
mock
只关注调用异常,如果客户端调用服务端出现异常了,那么就会触发服务降级
如何在 Dubbo 中利用本地伪装实现服务降级
本地伪装通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过 Mock 数据返回授权失败。
- api
public interface MockService { String mock(String param); String queryArea(String areaCode); String queryUser(String userCode); }
- 服务端
@DubboService public class MockServiceImpl implements MockService { @Override public String mock(String s) { System.out.println("=======mockservice的业务处理======="); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("=======mockservice的业务处理完成======="); return "本地伪装案例返回:"+s; } @Override public String queryArea(String s) { return s; } @Override public String queryUser(String s) { return s; } }
- 消费端
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestMockService { //这两种方式会走rpc远程调用 fail -- 会走远程服务 @DubboReference(check = false,mock = "true") // @DubboReference(check = false,mock = "com.course.mock.LocalMockService") //不走服务直接降级 force -- 是不会走远程服务的,强制降级. // @DubboReference(check = false,mock = "force:return eclipse2019") // @DubboReference(check = false,mock = "throw java.lang.RuntimeException") private MockService mockService; @Test public void test() { System.out.println(mockService.mock("eclipse2019")); } }
默认mock实现
/*
* MockServiceMock
*
* 1、接口名称 + Mock
* MockService + Mock
* 2、mock逻辑必须定义在接口的包下面
*/
public class MockServiceMock implements MockService {
@Override
public String mock(String s) {
System.out.println(this.getClass().getName() + "--mock");
return s;
}
@Override
public String queryArea(String s) {
System.out.println(this.getClass().getName() + "--queryArea");
return s;
}
@Override
public String queryUser(String s) {
System.out.println(this.getClass().getName() + "--queryUser");
return s;
}
}
自定义mock实现
public class LocalMockService implements MockService {
@Override
public String mock(String s) {
System.out.println(this.getClass().getName() + "--mock,嘿嘿");
return s;
}
@Override
public String queryArea(String s) {
System.out.println(this.getClass().getName() + "--queryArea");
return s;
}
@Override
public String queryUser(String s) {
System.out.println(this.getClass().getName() + "--queryUser");
return s;
}
}
3.13 异步调用
在 Dubbo 中发起异步调用
从 2.7.0 开始,Dubbo 的所有异步编程接口开始以 CompletableFuture 为基础
基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。代码案例
- api
public interface AsyncService { String asynctoDo(String name); }
- 服务端
@DubboService public class AsyncServiceImpl implements AsyncService { @Override public String asynctoDo(String s) { for (int i = 0; i < 10; i++) { System.out.println("===============AsyncServiceImpl.asynctoDo"); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return "hello," + s; } }
- 消费端
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestAsyncService { @DubboReference(check = false,timeout = 1000000000,methods = {@Method(name = "asynctoDo",async = true)}) private AsyncService asyncService; @Test public void async() throws InterruptedException { String aa = asyncService.asynctoDo("aa"); System.out.println("main==" + aa); System.out.println("并行调用其他接口===="); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //需要拿到异步调用的返回结果 CompletableFuture<Object> resultFuture = RpcContext.getContext().getCompletableFuture(); resultFuture.whenComplete((retValue,exception)->{ if(exception == null) { System.out.println("正常返回==" + retValue); } else { exception.printStackTrace(); } }); Thread.currentThread().join(); } }
3.14 异步执行
Dubbo 服务提供方的异步执行
Provider端异步执行将阻塞的业务从Dubbo内部线程池切换到业务自定义线程,避免Dubbo线程池的过度占用,有助于避免不同服务间的互相影响。异步执行无异于节省资源或提升RPC响应性能,因为如果业务执行需要阻塞,则始终还是要有线程来负责执行。
注意:
Provider 端异步执行和 Consumer 端异步调用是相互独立的,你可以任意正交组合两端配置
- Consumer同步 - Provider同步
- Consumer异步 - Provider同步
- Consumer同步 - Provider异步
- Consumer异步 - Provider异步
代码案例 - api
public interface AsyncService { String asynctoDo(String name); CompletableFuture<String> doOne(String var1); }
- 服务端
@DubboService public class AsyncServiceImpl implements AsyncService { @Override public String asynctoDo(String s) { for (int i = 0; i < 10; i++) { System.out.println("===============AsyncServiceImpl.asynctoDo"); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return "hello," + s; } @Override public CompletableFuture<String> doOne(String var1) { return CompletableFuture.supplyAsync(()->{ try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return "doOne -- OK"; }); } }
- 消费端
3.15 本地调用@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ConsumerConfiguration.class) public class TestAsyncDoOne { @DubboReference(check = false,timeout = 1000000000,methods = {@Method(name = "asynctoDo",async = true),@Method(name = "doOne",async = false)}) private AsyncService asyncService; @Test public void asyncDoone() throws ExecutionException, InterruptedException { CompletableFuture<String> eclipse2019 = asyncService.doOne("eclipse2019"); eclipse2019.whenComplete((retValue,exception)->{ if(exception == null) { System.out.println(retValue); } else { exception.printStackTrace(); } }); Thread.currentThread().join(); } }
在 Dubbo 中进行本地调用
本地调用使用了 injvm 协议,是一个伪协议,它不开启端口,不发起远程调用,只在 JVM 内直接关联,但执行Dubbo 的 Filter 链。
本地调用,调用的就是本地工程的接口实例
示例:
@DubboReference(check = false,injvm = true)
StudentService studentService;
@Test
public void inJvm() throws InterruptedException {
System.out.println(studentService.find("xx"));
}
3.16 粘滞连接
为有状态服务配置粘滞连接
粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者挂了,再连另一台。
粘滞连接将自动开启延迟连接,以减少长连接数。
sticky=true
@DubboReference(check = false,sticky = true)
UserService userService;
3.17 小结
Dubbo的高级应用不仅仅只是这些,由于篇幅问题,本文只总结以上的应用,具体的其他应用可以参考官方文档