SpringCloud Alibaba系列——4Dubbo简介与应用(下)

老老老JR老北
发布于 2022-7-25 17:27
浏览
0收藏

作者 | 宇木木兮
来源 |今日头条

承接上文,本文将介绍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;
        }
    }​
  • 消费端
    @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"));
        }
    }​
    3.2 直连提供者
    Dubbo 中点对点的直连方式

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

配置:

<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService" url="dubbo://localhost:20890" />
  • api和服务提供端代码跟启动是检查一样
  • 消费端
    @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"));
        }
    }​
    3.3 集群容错
    集群调用失败时,Dubbo 提供的容错方案

在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。SpringCloud Alibaba系列——4Dubbo简介与应用(下)-鸿蒙开发者社区各节点关系:

  • 这里的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;
        }
    }​
  • 消费端
    @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"));
        }
    }​
    3.4 负载均衡
    Dubbo 提供的集群负载均衡策略

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

具体实现上,Dubbo 提供的是客户端负载均衡,即由 Consumer 通过负载均衡算法得出需要将请求提交到哪个Provider 实例。

负载均衡策略

目前 Dubbo 内置了如下负载均衡算法,用户可直接配置使用:

   SpringCloud Alibaba系列——4Dubbo简介与应用(下)-鸿蒙开发者社区配置:

<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

SpringCloud Alibaba系列——4Dubbo简介与应用(下)-鸿蒙开发者社区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 中为同一个服务配置多个版本

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

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

  1. 在低压力时间段,先升级一半提供者为新版本
  2. 再将所有消费者升级为新版本
  3. 然后将剩下的一半提供者升级为新版本
  • 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。SpringCloud Alibaba系列——4Dubbo简介与应用(下)-鸿蒙开发者社区

  • 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 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。SpringCloud Alibaba系列——4Dubbo简介与应用(下)-鸿蒙开发者社区代码案例

  • 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";
            });
        }
    }​
  • 消费端
    @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();
        }
    }​
    3.15 本地调用
    在 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的高级应用不仅仅只是这些,由于篇幅问题,本文只总结以上的应用,具体的其他应用可以参考官方文档

分类
已于2022-7-25 17:27:47修改
收藏
回复
举报
回复
    相关推荐