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

承接上文,本文将介绍Dubbo的高级应用

第3章 Dubbo高级用法

3.1 启动时检查

通过spring配置文件

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

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

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

通过 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 接口从注册中心获取列表。

配置:

  • 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简介与应用(下)

各节点关系:

  • 这里的InvokerProvider的一个可调用Service的抽象,Invoker封装了Provider地址及Service接口信息
  • Directory代表多个 Invoker,可以把它看成List,但与List不同的是,它的值可能是动态变化的,比如注册中心推送变更
  • ClusterDirectory中的多个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 内置了如下负载均衡算法,用户可直接配置使用:

算法

特性

备注

RandomLoadBalance

加权随机

默认算法,默认权重相同

RoundRobinLoadBalance

加权轮询

借鉴于 Nginx 的平滑加权轮询算法,默认权重相同

LeastActiveLoadBalance

最少活跃优先 + 加权随机

背后是能者多劳的思想,最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差;使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。

ShortestResponseLoadBalance

最短响应优先 + 加权随机

更加关注响应速度

ConsistentHashLoadBalance

一致性 Hash

一致性Hash,相同参数的请求总是发到同一提供者。当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

配置:

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 {
    //定义了所有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 依赖


    javax.validation
    validation-api
    1.0.0.GA


    org.hibernate
    hibernate-validator
    4.2.0.Final
  • 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
public void usegeneric() {
    ApplicationConfig applicationConfig = new ApplicationConfig();
    applicationConfig.setName("dubbo_consumer");
    RegistryConfig registryConfig = new RegistryConfig();
    registryConfig.setAddress("zookeeper://192.168.67.139:2184");
    ReferenceConfig 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);
}
public interface CallbackListener {
    void changed(String msg);
}
  • 服务端
@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 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 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 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 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的高级应用不仅仅只是这些,由于篇幅问题,本文只总结以上的应用,具体的其他应用可以参考官方文档

下文预告

  1. Dubbo整合spring的xml配置流程
  2. Dubbo整合spring的注解配置流程

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章