JavaEE 企业级分布式高级架构师(九)Dubbo(2.7)学习笔记(2)

Dubbo学习笔记

Dubbo高级配置

负载均衡

演示环境

  • 这里为了演示负载均衡的效果,使用 02-provider-zk 这个工程,开启三个提供者,消费者使用 02-consumer-zk 工程进行修改。
  • 三个提供者工程的接口实现和配置分别如下:
// 02-provider-zk-01
public class SomeServiceImpl implements SomeService {
    @Override
    public String hello(String name){
        System.out.println("执行【第一个】提供者的hello() " + name);
        return "【第一个】提供者";
    }
}
// 02-provider-zk-02
public class SomeServiceImpl implements SomeService {
    @Override
    public String hello(String name){
        System.out.println("执行【第二个】提供者的hello() " + name);
        return "【第二个】提供者";
    }
}
// 02-provider-zk-03
public class SomeServiceImpl implements SomeService {
    @Override
    public String hello(String name){
        System.out.println("执行【第三个】提供者的hello() " + name);
        return "【第三个】提供者";
    }
}
<!-- 演示负载均衡 -->
<!-- 02-provider-zk-01 配置 -->
<dubbo:application name="02-provider-zk-01">
  <dubbo:parameter key="qos.port" value="11111" />
</dubbo:application>

<dubbo:protocol name="dubbo" port="20881" />

<!-- 02-provider-zk-02 配置 -->
<dubbo:application name="02-provider-zk-02">
  <dubbo:parameter key="qos.port" value="22222" />
</dubbo:application>

<dubbo:protocol name="dubbo" port="20882" />

<!-- 02-provider-zk-03 配置 -->
<dubbo:application name="02-provider-zk-03">
  <dubbo:parameter key="qos.port" value="33333" />
</dubbo:application>

<dubbo:protocol name="dubbo" port="20883" />

在这里插入图片描述

负载均衡算法

  • 若消费者与提供者均设置了负载均衡策略,则消费者端设置的优先级高。
  • 若消费者端没有显示的设置,但提供者端显示的设置了,且同一个服务(接口名、版本号、分组都相同)的负载均衡策略相同,则消费者调用是会按照提供者设置的策略调用。
  • 若多个提供者端设置的策略不相同,则最后一个注册的会将前面注册的信息覆盖。
Dubbo内置的负载均衡算法

Dubbo内置了四种负载均衡算法:

  • random:随机算法,是 Dubbo 默认的负载均衡算法,存在服务堆积问题。
  • roundrobin:轮询算法,按照设定好的权重(weight属性指定)依次进行调度。
  • leastactive:最少活跃度调度算法,即被调度的次数越少,其优先级越高,被调度到的几率就越高。
  • consistenthash:一致性 hash 算法,对于相同参数的请求,其会被路由到相同的提供者。例如:
<!-- 服务暴露 -->
<dubbo:service interface="com.yw.dubbo.example.service.SomeService" ref="someService">
  <dubbo:method name="hello">
    <!-- 对hello方法的第二个参数进行hash结果查找对应主机 -->
    <dubbo:parameter key="hash.arguments" value="0,1,0" />
    <!-- 指定虚拟主机为320个,源码中默认为160个 -->
    <dubbo:parameter key="hash.nodes" value="320" />
  </dubbo:method>
</dubbo:service>
  • 虚拟主机:在进行一致性 hash 算法负载均衡时,相同参数的请求总是发到同一提供者,若被调用的提供者挂了,这时会通过虚拟主机转发到其它提供者,默认可以产生160个虚拟主机。
指定负载均衡算法-loadbalance
  • 负载均衡算法可以在消费者端指定,也可以在提供者端指定:
  • 消费者端指定:
  <!-- 订阅服务:指定调用服务的所有方法均采用轮询负载均衡算法 -->
  <dubbo:reference id="someService" check="false" loadbalance="roundrobin"
                   interface="com.yw.dubbo.example.service.SomeService" />

<!--  &lt;!&ndash; 订阅服务:指定不同的服务方法采用不同的负载均衡算法 &ndash;&gt;-->
<!--  <dubbo:reference id="someService" check="false" interface="com.yw.dubbo.example.service.SomeService">-->
<!--    <dubbo:method name="hello" loadbalance="roundrobin"/>-->
<!--    <dubbo:method name="doFirst" loadbalance="random"/>-->
<!--    <dubbo:method name="doSecond" loadbalance="leastactive"/>-->
<!--  </dubbo:reference>-->
  • 提供者端指定:
  <!-- 服务暴露:指定调用服务的所有方法均采用轮询负载均衡算法 -->
  <dubbo:service interface="com.yw.dubbo.example.service.SomeService" ref="someService" loadbalance="roundrobin" />

  <!-- 服务暴露:指定不同的服务方法采用不同的负载均衡算法 -->
<!--  <dubbo:service interface="com.yw.dubbo.example.service.SomeService" ref="someService">-->
<!--    <dubbo:method name="hello" loadbalance="roundrobin"/>-->
<!--    <dubbo:method name="doFirst" loadbalance="random"/>-->
<!--    <dubbo:method name="doSecond" loadbalance="leastactive"/>-->
<!--  </dubbo:service>-->

  <!-- 服务暴露:一致性 hash 算法进行负载均衡 -->
<!--  <dubbo:service interface="com.yw.dubbo.example.service.SomeService" ref="someService">-->
<!--    <dubbo:method name="hello">-->
<!--      &lt;!&ndash; 对hello方法的第二个参数进行hash结果查找对应主机 &ndash;&gt;-->
<!--      <dubbo:parameter key="hash.arguments" value="0,1,0" />-->
<!--      &lt;!&ndash; 指定虚拟主机为320个,源码中默认为160个 &ndash;&gt;-->
<!--      <dubbo:parameter key="hash.nodes" value="320" />-->
<!--    </dubbo:method>-->
<!--  </dubbo:service>-->
测试
  • 消费者端:
public class ConsumerRun {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-consumer.xml");
        SomeService service = (SomeService) ac.getBean("someService");
        for (int i = 0; i < 10; i++) {
            String hello = service.hello("Consumer-" + i);
            System.out.println(hello);
        }
    }
}

在这里插入图片描述

集群容错

  • 集群容错指的是,当消费者调用提供者集群时发生异常时的处理方案。

Dubbo内置的容错策略

Dubbo内置了 6 种集群容错策略:

  • Failover——故障转移策略:当消费者调用提供者集群中的某个服务器失败时,其会自动尝试着调用其它服务器。该策略通常用于读操作,例如,消费者要通过提供者从 DB 中读取某数据,但重试会带来服务延迟。
  • Failfast——快速失败策略:消费者端只发起一次调用,若失败则立即报错,通常用于非幂等性的写操作,比如新增记录。
幂等:在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
* GET请求:幂等
* POST请求:非幂等
* PUT请求:幂等(一般情况下),更新数值加减是非幂等的
* DELETE请求:幂等(一般情况下),按范围条件删除时非幂等的
  • Failsafe——失败安全策略:当消费者调用提供者出现异常时,直接忽略本次消费操纵。该操作通常用于执行相对不太重要的服务,例如:写入审计日志等操作。
  • Failback——失败自动恢复策略:消费者调用提供者失败后,Dubbo 会记录下该失败请求,然后定时自动重新发送该请求。该策略通常用于实时性要求不太高的服务,例如消息通知操作。
  • Forking——并行策略:消费者对于同一服务并行调用多个提供者服务器,只要有一个成功即调用结束并返回结果。通常用于实时性要求较高的读操作,但其会浪费较多的服务器资源。
  • Broadcast——广播策略:广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

配置Dubbo容错策略

  • 容错策略可以设置在消费者端,也可以设置在提供者端。若消费者与提供者均做了设置,则消费者端的优先级更高。
设置重试次数-retries
  • Dubbo默认的容错策略是故障转移(Failover)策略,即允许失败后重试。可以通过如下方式来设置重试次数,注意设置的是重试次数,不含第一次正常调用。
  • 提供者端设置:
<!-- 指定当前服务中的任意方法若调用失败,最多让消费者调用3次(一次正常调用,两次重试) -->
<dubbo:service interface="com.yw.dubbo.example.service.SomeService" ref="someService" retries="2" />

<dubbo:service interface="com.yw.dubbo.example.service.SomeService" ref="someService">
  <!-- 指定当前方法若调用失败,最多让消费者调用3次(一次正常调用,两次重试) -->
  <dubbo:method name="hello" retries="2" />
</dubbo:service>  
  • 消费者端设置:
<!-- 订阅服务:设置对指定服务器最多调用3次(一次正常调用,两次重试) -->
<dubbo:reference id="someService" check="false" 
                 interface="com.yw.dubbo.example.service.SomeService" retries="2"/>

<dubbo:reference id="someService" check="false"
               interface="com.yw.dubbo.example.service.SomeService">
  <!-- 指定对hello方法最多调用3次(一次正常调用,两次重试) -->
  <dubbo:method name="hello" retries="2"/>
</dubbo:reference>
容错策略设置-cluster
  • 提供者端:
<!-- 指定当前的服务器集群容错机制采用failfast -->
<dubbo:service interface="com.yw.dubbo.example.service.SomeService" ref="someService" cluster="failfast" />
  • 消费者端:
<!-- 指定要调用的服务器集群容错机制采用failfast -->
<dubbo:reference id="someService" check="false" cluster="failfast" 
                 interface="com.yw.dubbo.example.service.SomeService" />

服务降级

  • 解决高并发的三把利器:降级、限流、缓存。

服务降级基础(面试题)

什么是服务降级
  • 服务降级:当服务器压力剧增的情况下,根据当前业务情况及流量,对一些服务有策略的降低服务级别,以释放服务器资源,保证核心任务的正常运行。例如:双十一零点到两点期间淘宝用户不能修改收获地址,不能查看历史订单,就是典型的服务降级。
服务降级方式

能够实现服务降级的方式很多,常见的有如下几种情况:

  • 部分服务暂停:页面能够访问,但是部分服务暂停服务,不能访问。
  • 部分服务延迟:页面可以访问,当用户提交某些请求时,系统会提示该操作已成功提交给了服务器,由于当前服务器繁忙,此操作随后会执行。在等待了若干时间后最终用户可以看到正确的执行结果。
  • 全部服务暂停:系统入口页面就不能访问,提示由于服务繁忙此服务暂停。跳转到一个预先设定好的静态页面。(12306,每天晚上0点 到 6点不能买票)
  • 随机拒绝服务:服务器会按照预先设定好的比例,随机挑选用户,对其拒绝服务。作为用户,其看到的就是请重试,可能再重试就可获得服务。(12306,春节抢票,服务器忙)
整个系统的服务降级埋点

在这里插入图片描述

服务降级埋点可以在以下几个地方做:

  • 路由网关:例如nginx,可以预设静态页面返回。
  • 消费者:指Dubbo、Spring Cloud中的服务降级,当调用提供者失败,会返回一个相对友好的提示信息。
  • 数据缓存层:当QPS很高时,可以在数据缓存层预设一些值,直接返回。
  • 消息中间件:部分服务延迟返回,它与其他服务降级埋点有本质区别,其返回的是真实的数据,只是在时间上降级了
  • 提供者:预设一个值直接返回,而不向数据查询了,但是一般不建议在提供者端做降级。因为其只是减轻了数据库的压力,整个系统的压力并没有减轻。
服务降级与Mock机制
  • Dubbo的服务降级采用的是 mock 机制,其具有两种降级处理方式:Mock Null降级处理Class Mock 降级处理

Mock Null服务降级处理 06-consumer-mock

  • 创建消费者工程:直接复制 02-consumer-zk 工程,并命令为 06-consumer-mock。修改pom.xml文件,由于这里不再需要 00-api 工程了,所以在pom文件中将对 00-api 工程的依赖删除即可。
  • 定义接口:
public interface UserService {
    String getUsernameById(int id);
    void addUser(String username);
}
  • 修改spring-consumer.xml文件:
<!-- 订阅服务:Mock Null降级处理 -->
<dubbo:reference id="userService" check="false" mock="return null"
                 interface="com.yw.dubbo.example.service.UserService" />
  • 修改消费者启动类:
public class ConsumerRun {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-consumer.xml");
        UserService service = (UserService) ac.getBean("userService");

        // 有返回值的方法降级结果为null
        String username = service.getUsernameById(3);
        System.out.println("username = " + username);

        // 无返回值的方法降级结果是无任何显示
        service.addUser("China");
    }
}

在这里插入图片描述

Mock class服务降级处理

  • 定义Mock Class:在业务接口所在的包中,定义一个类,该类的命名需要满足以下规则:业务接口简单类名+Mock
public class UserServiceMock implements UserService{
    @Override
    public String getUsernameById(int id) {
        return "没有该用户:" + id;
    }

    @Override
    public void addUser(String username) {
        System.out.println("添加该用户失败:" + username);
    }
}
  • 修改spring-consumer.xml文件:
<dubbo:reference id="userService" check="false" mock="true"
                 interface="com.yw.dubbo.example.service.UserService" />

在这里插入图片描述

服务调用超时

  • 前面的服务降级的发生,其实是由于消费者调用服务超时引起的,即从发出调用请求到获取到提供者的响应结果这个时间超出了设定的时限。默认服务器调用超时时限为1秒,可以在消费者端与提供者端设置超时时限。
  • 00-api 工程中新增接口:
public interface UserService {
    String getUsernameById(int id);
    void addUser(String username);
}

创建提供者工程 06-provider-timeout

  • 创建工程:复制 02-provider-zk 工程,并重命名为 06-provider-timeout。
  • 定义接口实现类:业务方法添加一个 2 秒 Sleep,以延长向消费者返回结果的时间。
public class UserServiceImpl implements UserService {
    @Override
    public String getUsernameById(int id) {
        try{
            TimeUnit.SECONDS.sleep(2);
        }catch(Exception e){
            e.printStackTrace();
        }
        // 这里暂且返回一个指定的值
        return "张三";
    }

    @Override
    public void addUser(String username) {
        // 这里暂且返回一个指定的成功提示
        System.out.println("添加用户成功");
    }
}
  • 修改配置文件:
<!-- 注册服务执行对象 -->
<bean id="userService" class="com.yw.dubbo.example.provider.UserServiceImpl" />
<!-- 服务暴露 -->
<dubbo:service interface="com.yw.dubbo.example.service.UserService" ref="userService" />

创建消费者工程 06-consumer-timeout

  • 创建工程:复制 06-consumer-mock 工程,并重命名为 06-consumer-timeout。
  • 修改启动类:
public class ConsumerRun {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-consumer.xml");
        UserService service = (UserService) ac.getBean("userService");

        String username = service.getUsernameById(3);
        System.out.println(username);
    }
}
  • 先启动提供者,再启动消费者,后台报超时错误如下:

在这里插入图片描述

  • 可以在消费者端设置超时时限,假设设置为 3 秒,这样就不会报错了,timeout也可以设置在提供者端,但是设置在消费者端的 timeout 优先级更高。

在这里插入图片描述

服务限流

  • 为了防止某个消费者的 QPS 或是所有消费者的 QPS 总和突然飙升而导致的重要服务的失效,系统可以对访问流量进行控制,这种对集群的保护措施称为服务限流。Dubbo中能够实现服务限流的方式较多,可以划分为两类:直接限流与间接限流。
  • 直接限流:通过对连接的数量直接限制来达到限流的目的。超过限制则会让再来的请求等待,直到等待超时,或获取到相应服务(官方方案)。
  • 间接限流:通过一些非连接数量设置的间接手段来达到限流的目的(个人经验)。

直接限流

executes限流——仅提供者端
  • 该属性仅能设置在提供者端。可以设置为接口级别,也可以设置为方法级别。限制的是服务(方法)并发执行数量。
<!-- 接口级别:显示当前接口中每个方法的并发执行个数不能超过10个 -->
<dubbo:service interface="com.yw.dubbo.example.service.UserService" 
               ref="userService" executes="10">
  <!-- 方法级别:显示当前接口的addUser方法的并发执行个数不能超过5个 -->
  <dubbo:method name="addUser" executes="5" />
</dubbo:service>
accepts限流——仅提供者端
  • 该属性仅可设置在提供者端的<dubbo:provider/>与<dubbo:protocol>,是针对指定协议的连接数量进行限制。
<!-- 限制当前提供者在使用dubbo协议时,最多接收20个消费者连接 -->
<dubbo:provider protocol="dubbo" accepts="20" />

<!-- 限制当前提供者在使用dubbo协议时,最多接收10个消费者连接 -->
<dubbo:protocol name="dubbo" port="20880" accepts="10" />
actives限流——两端均可
  • 该限流方式与前两种不同的是,其可以设置在提供者端,也可以设置在消费者端。可以设置为接口级别,也可以设置为方法级别。
提供者端限流
  • 根据客户端与服务端建立的连接是长连接还是短连接,其意义不同:
  • 长连接:当前这个服务上的一个长连接最多能够处理的请求个数,对长连接数量没有限制。
  • 短连接:当前这个服务上可以同时处理的短连接数量。
<dubbo:service interface="com.yw.dubbo.example.service.UserService"
               ref="userService" actives="10">
  <dubbo:method name="addUser" actives="5" />
</dubbo:service>
消费者端限流
  • 根据客户端与服务端建立的连接是长连接还是短连接,其意义不同:
  • 长连接:当前这个消费者的一个长连接最多能够提交的请求个数,对长连接数量没有限制。
  • 短连接:当前这个消费者可以同时提交的短连接数量。
<dubbo:reference id="userService" check="false" actives="10"
                 interface="com.yw.dubbo.example.service.UserService">
  <dubbo:method name="addUser" actives="5" />
</dubbo:reference>
connections限流——两端均可
  • 可以设置在提供者端,也可以设置在消费者端,限定连接的个数。一般情况下,我们使用的都是默认的服务暴露协议Dubbo,所以,一般会让 connections 与 actives 联用。connections 限制长连接的数量,而 actives 限制每个长连接上的请求数量。
提供者端限流
<!-- 限制当前接口中每个方法的并发连接数不能超过10个 -->
<dubbo:service interface="com.yw.dubbo.example.service.UserService"
               ref="userService" connections="10">
  <!-- 限制当前接口中addUser方法的并发连接数不能超过5个 -->
  <dubbo:method name="addUser" connections="5" />
</dubbo:service>
消费者端限流
<dubbo:reference id="userService" check="false" connections="10"
                interface="com.yw.dubbo.example.service.UserService">
  <dubbo:method name="addUser" connections="5" />
</dubbo:reference>

间接限流

延迟连接
  • 只要消费者真正调用提供者方法时才创建长连接。仅可设置在消费者端,且不能设置为方法级别。仅作用于 Dubbo 服务暴露协议,用于减少长连接数量。
<!-- 延迟连接: 仅可设置在消费者端,且不能设置为方法级别 -->
<!-- 设置当前消费者对指定接口的每一个方法发出的连接均采用延迟连接 -->
<dubbo:reference id="userService" lazy="true"
                 interface="com.yw.dubbo.example.service.UserService" />
<!-- 设置当前消费者对所有接口的所有方法发出的连接均采用延迟连接 -->
<dubbo:consumer lazy="true" />
粘连连接
  • 系统会尽量让同一个消费者调用同一个提供者,其可以限定流向。仅能设置在消费者端,其可以设置为接口级别,也可以设置为方法级别。仅作用于 Dubbo 服务暴露协议,用于减少长连接数量。粘连连接的开启将自动开启延迟连接。
<!-- 设置当前消费者对指定接口的每一个方法发出的连接均采用粘连连接 -->
<dubbo:reference id="userService" sticky="true"
                 interface="com.yw.dubbo.example.service.UserService" />
                 
<dubbo:reference id="userService"
                 interface="com.yw.dubbo.example.service.UserService" >
  <!-- 设置当前消费者对指定接口的addUser方法发出的连接均采用粘连连接 -->
  <dubbo:method name="addUser" sticky="true" />
</dubbo:reference>
负载均衡
  • 可以设置在消费者端,亦可设置在提供者端;可以设置在接口级别,亦可设置在方法级别。其可以限定流向,但其没有限制流量。
<dubbo:reference id="userService" loadbalance="leastactive"
                 interface="com.yw.dubbo.example.service.UserService" />

声明式缓存

  • 为了进一步提高消费者对用户的响应速度,减轻提供者的压力,Dubbo提供了基于结果的声明式缓存。该缓存是基于消费者端的,所以使用很简单,只需要修改消费者配置文件,与提供者无关。该缓存是缓存在消费者端内存中的,一旦缓存创建,即使提供者宕机也不会影响消费者端的缓存。

缓存设置 07-consumer-cache

  • 创建工程:复制 02-consumer-zk 工程,并命名为 07-consumer-cache。
  • 修改消费者配置文件:仅需在<dubbo:reference/>中添加 cache=“true” 属性即可,当然,若一个接口中只有部分方法需要缓存,则可使用方法级别的缓存

在这里插入图片描述

  • ConsumerRun启动测试类:
public class ConsumerRun {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-consumer.xml");
        SomeService service = (SomeService) ac.getBean("someService");

        System.out.println(service.hello("Tom"));
        System.out.println(service.hello("Jerry"));
        System.out.println(service.hello("Tom"));
        System.out.println(service.hello("Jerry"));
    }
}

在这里插入图片描述

默认缓存1000个结果

  • 声明式缓存中可以缓存多少个结果呢?默认可以缓存 1000 个结果。若超出1000,将采用 LRU 策略来删除缓存,以保证最热的数据被缓存。注意,该删除缓存的策略不能修改
  • 测试代码:
public class ConsumerRun {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-consumer.xml");
        SomeService service = (SomeService) ac.getBean("someService");

        // 1000次不同的消费结果,将占满1000个缓存空间
        for (int i = 1; i <= 1000; i++) {
            service.hello("i==" + i);
        }
        // 第1001次不同的消费结果,会将第一个缓存内容挤出去
        System.out.println(service.hello("Tom"));
        // 本次消费会重新调用提供者,说明原理的第一个缓存的确被挤出去了
        // 本次消费结果会将(i==2)的缓存结果挤出去
        System.out.println(service.hello("i==1"));
        // 本次消费会直接从缓存中获取
        System.out.println(service.hello("i==3"));
    }
}

在这里插入图片描述

  • 应用于查询结果不会发生改变的情况,例如,查询某产品的序列号、订单、身份证号等。

多注册中心

创建提供者 08-provider-registers

  • 创建工程:直接复制 05-provider-group 工程,并命名为 08-provider-registers。
  • 修改配置文件:同一个服务注册到不同的中心,使用逗号进行分隔。

在这里插入图片描述

创建消费者 08-consumer-registers

  • 创建工程:直接复制 05-consumer-group 工程,并命名为 08-consumer-registers。
  • 修改配置文:对于消费者工程,用到哪个注册中心了,就声明哪个注册中心,无需将全部注册中心进行声明。

在这里插入图片描述

单功能注册中心

仅订阅

概念
  • 对于某服务来说,其可以发现可调用注册中心中的其他服务,但不能被其他服务发现和调用,这种情形称为仅订阅。
  • 仅可去发现,但不能被发现。
  • 即可以从注册中心下载服务注册表,但其不会将当前配置文件中的服务写入到注册中心的服务注册表。
设置方式
  • 对于“仅订阅”注册中心的实现,只需修改提供者配置文件,在<dubbo:registry/>标签中添加 register=“false” 属性。即对于当前服务来说,注册中心不再接受其注册,但该服务可以通过注册中心去发现和调用其它服务。

在这里插入图片描述

应用场景
  • 本地dev开发分支未开发完的功能需要设置为仅订阅

仅注册

概念
  • 对于某服务来说,其可以被注册中心的其它服务发现和调用,但不能发现和调用注册中心中的其它服务,这种情形称为仅注册。
  • 仅可被发现,但不能去发现。
设置方式
  • 对于“仅注册”注册中心的实现,只需要修改提供者配置文件,在 <dubbo:registry/>标签中添加 subscribe=“false” 的属性。即对于当前服务来说,注册中心中的其它服务可以发现和调用当前服务,但其不能发现和调用其它服务。

在这里插入图片描述

应用场景
  • 服务注册中心B是服务注册中心A的镜像,但是由于某种原因,镜像复制过来的S挂掉了,这个时候调用其的 b和c 就会报错,因此需要把 b和c 设置为仅注册。

在这里插入图片描述

服务暴露延迟

  • 如果我们的服务启动过程需要 warmup 事件(预热),就可以使用 delay 进行服务延迟暴露。只需在服务提供者的 <dubbo:service/> 标签中添加 delay 属性。其值可以有三类:
  • 正数:单位为毫秒,表示在提供者对象创建完毕后的指定时间后,再发布服务。
  • 0:默认值,表示当前提供者创建完毕后,马上向注册中心暴露服务。
  • -1:表示在 Spring 容器初始化完毕后,再向注册中心暴露服务。

在这里插入图片描述

消费者的异步调用

应用场景

在这里插入图片描述

  • 在Dubbo简介时,我们分析了 Dubbo 的四大组件工作原理图,其中消费者调用提供者采用的是同步调用方式。其实,消费者对于提供者的调用,也可以采用异步方式进行调用。异步调用一般应用于提供者的是耗时性 IO 服务。

Future异步执行原理

  • 异步方法调用执行原理如下图所示,其中实线为同步调用,而虚线为异步调用。
    • UserThread:消费者线程
    • IOThread:提供者线程
    • Server:对IO型操作的真正执行者

在这里插入图片描述

Future异步调用

  • 在 00-api 中新增一个接口类:
public interface OtherService {
    String doFirst();
    String doSecond();
    String doThird();
    String doFourth();
}
创建提供者 09-provider-async
  • 定义实现类:
public class OtherServiceImpl implements OtherService {
    @Override
    public String doFirst() {
        sleep("doFirst");
        return "doFirst()";
    }

    @Override
    public String doSecond() {
        sleep("doSecond");
        return "doSecond()";
    }

    @Override
    public String doThird() {
        sleep("doThird");
        return "doThird()";
    }

    @Override
    public String doFourth() {
        sleep("doFourth");
        return "doFourth()";
    }
    // 模拟耗时操作
    private void sleep(String method) {
        long startTime = System.currentTimeMillis();
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (Exception ignored) {
        }
        System.out.println(method + "方法执行用时:" + (System.currentTimeMillis() - startTime));
    }
}
  • 配置文件spring-provider.xml:

在这里插入图片描述

创建消费者 10-consumer-async
  • 配置文件spring-consumer.xml:

在这里插入图片描述

  • 测试代码:
public class ConsumerRun {
    public static void main(String[] args) throws Exception {
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-consumer.xml");
        OtherService service = (OtherService) ac.getBean("otherService");

        // 测试一:同步消费者
        // 记录同步调用开始时间
        long syncStart = System.currentTimeMillis();
        // 同步调用:
        System.out.println("同步,doFirst()直接获取到返回值:" + service.doFirst());
        System.out.println("同步,doSecond()直接获取到返回值:" + service.doSecond());
        System.out.println("两个同步操作共计用时(毫秒):" + (System.currentTimeMillis() - syncStart));

        // 测试二:异步消费者,无返回值
        // 记录异步调用开始时间
        long asyncStart = System.currentTimeMillis();
        // 异步调用:
        service.doThird();
        service.doFourth();
        System.out.println("两个异步操作共计用时(毫秒):" + (System.currentTimeMillis() - asyncStart));

        // 测试三:异步消费者,有返回值
        // 记录异步调用开始时间
        long asyncStart2 = System.currentTimeMillis();
        // 异步调用
        System.out.println("调用结果1 = " + service.doThird());
        Future<String> future = RpcContext.getContext().getFuture();
        System.out.println("调用结果2 = " + future.get());
        System.out.println("获取到异步调用结果共计用时(毫秒):" + (System.currentTimeMillis() - asyncStart2));
    }
}
  • 测试输出:

在这里插入图片描述

CompletableFuture异步调用

  • 使用 Future 实现异步调用,对于无需获取返回值的操作来说不存在问题,但消费者若需要获取到最终的异步执行结果,则会出现问题:消费者在使用 Future 的 get() 方法获取返回值时被阻塞(轮询获取返回结果)。为了解决这个问题,Dubbo 又引入了 CompletableFuture 来实现对提供者的异步调用。
  • 改造 OtherService接口:

在这里插入图片描述

修改提供者

在这里插入图片描述

消费者测试
  • 修改消费者类:直接删除同步消费者类,修改异步消费者类

在这里插入图片描述

  • 运行看测试结果:

在这里插入图片描述

总结

  • Future 与 CompletableFuture 的对比:
    • 对于消费者不用获取提供者所调用的耗时操作结果的情况,使用 Future 与 CompletableFuture 效果是区别不大的。但对于需要获取返回值的情况,它们的区别是很大的。
    • Future:Dubbo 2.7.0版本之前消费者异步调用提供者的实现方式,源自JDK5,对异步结果的获取采用了阻塞与轮询方式。
    • CompletableFuture:Dubbo 2.7.0版本之后消费者调用提供者的实现方式,源自JDK8,对异步结果的获取采用了回调的方式。

提供者的异步执行

  • 从前面“对提供者的异步调用”例子可以看出,消费者对提供者实现了异步调用,消费者线程的执行过程不再发生阻塞,但提供者对 IO耗时操作仍采用的是同步调用,即 IO 操作仍会阻塞 Dubbo 的提供者线程。
  • 但是需要注意,提供者对 IO 操作的异步调用,并不会提升 RPC响应速度,因为耗时操作终归是需要消耗那么多时间后才能给出结果的。

在这里插入图片描述

Dubbo与Spring Boot整合

新增接口和依赖

  • 在 00-api 工程中新增接口类和实体类:
@Data
public class Employee implements Serializable{
    private Integer id;
    private String name;
    private int age;
}
public interface EmployeeService {
    void addEmployee(Employee employee);
    Employee findEmployeeById(int id);
    Integer findEmployeeCount();
}
  • 主要新增的一些依赖:
<!-- dubbo与spring boot整合依赖 -->
<dependency>
  <groupId>org.apache.dubbo</groupId>
  <artifactId>dubbo-spring-boot-starter</artifactId>
  <version>2.7.0</version>
</dependency>

<!-- Spring Boot与Redis整合依赖 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- mybatis与Spring Boot整合依赖 -->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.3.2</version>
</dependency>

<!-- 数据源Druid依赖 -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.1.10</version>
</dependency>

<!-- MySQL驱动依赖 -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.47</version>
</dependency>

创建提供者 10-provider-springboot

  • 定义Dao接口:
// 自动Mapper的动态代理
@Mapper
public interface EmployeeDao {
    void insertEmployee(Employee employee);
    Integer selectEmployeeCount();
    Employee selectEmployeeById(int id);
}
  • 定义映射文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yw.dubbo.example.dao.EmployeeDao">
  <insert id="insertEmployee">
    insert into employee(`name`, age) values(#{name}, #{age})
  </insert>
  <select id="selectEmployeeCount" resultType="int">
    select count(id) from employee
  </select>
  <select id="selectEmployeeById" resultType="com.yw.dubbo.example.model.Employee">
    select id, `name`, age from employee where id = #{id}
  </select>
</mapper>
  • 定义Service实现类:
@Service(version = "1.0.0")        // Dubbo的注解
@Component
public class EmployeeServiceImpl implements EmployeeService {
    @Autowired
    private EmployeeDao employeeDao;

    // 当有对象插入时会清空 realTimeCache 缓存空间
    @CacheEvict(value = "realTimeCache", allEntries = true)
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void addEmployee(Employee employee) {
        employeeDao.insertEmployee(employee);
    }

    // 一旦有了大量查询结果,则会将此结果写入到realTimeCache缓存
    // key是 employee_ 加上方法参数
    @CacheEvict(value = "realTimeCache", key = "'employee_'+#id")
    @Override
    public Employee findEmployeeById(int id) {
        return employeeDao.selectEmployeeById(id);
    }

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    // 双重检测锁机制解决 redis 的热点缓存问题(存在线程安全问题)
    @Override
    public Integer findEmployeeCount() {
        // 获取Redis操作对象
        BoundValueOperations<Object, Object> ops = redisTemplate.boundValueOps("count");
        // 从缓存获取数据
        Object count = ops.get();
        if (count == null) {
            synchronized (this) {
                count = ops.get();
                if (count == null) {
                    System.out.println("============");
                    // 从DB中查询
                    count = employeeDao.selectEmployeeCount();
                    // 将查询结果存放到Redis
                    ops.set(count, 10, TimeUnit.SECONDS);
                }
            }
        }
        return (Integer) count;
    }
}
  • 启动类:
@EnableTransactionManagement    // 开启事务
@EnableCaching                  // 开启缓存
@DubboComponentScan(basePackages = "com.yw.dubbo.example.service") // 扫描提供者服务
@SpringBootApplication
public class ProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }
}
  • 主配置文件 application.yml:
server:
  port: 8888

mybatis:
#  # 注册mybatis中实体类的别名
#  type-aliases-package: com.yw.dubbo.example.model
  # 注册映射文件
  mapper-locations: classpath:com/yw/dubbo/example/dao/*.xml

spring:
  # 注册数据源
  datasource:
    # 指定数据源类型为Druid
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.254.128:3306/test?useUnicode=true&amp;characterEncoding=UTF-8
    username: root
    password: 123456

  # 连接Redis服务器
  redis:
    host: 192.168.254.128
    port: 6379
    password:

  # 配置缓存
  cache:
    # 指定缓存类型
    type: redis
    # 指定缓存区域名称
    cache-names: realTimeCache

  # 功能等价于 spring-dubbo 配置文件中的<dubbo:application>
  application:
    name: 10-provider-springboot
  # 指定zk注册中心
  dubbo:
    registry: zookeeper://192.168.254.120:2181
  • 启动提供者:

在这里插入图片描述

  • 查看控制台:

在这里插入图片描述

创建消费者10-consumer-springboot

  • pom.xml 与工程 10-provider-springboot 类似,主配置文件 application.yml:
server:
  port: 9999

# dubbo相关配置
dubbo:
  application:
    name: 10-consumer-springboot
  registry: # 指定zk注册中心
    address: zookeeper://192.168.254.120:2181
  • 启动类和处理器:
@SpringBootApplication
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}
@RestController
@RequestMapping("/consumer/employee")
public class EmployeeController {
//    @Autowired
    @Reference(version = "1.0.0") // Dubbo注解,<dubbo:reference />
    private EmployeeService employeeService;

    @PostMapping("/register")
    public String registerHandle(@RequestBody Employee employee){
        employeeService.addEmployee(employee);
        return "OK";
    }

    @GetMapping("/find/{id}")
    @ResponseBody
    public Employee findHandle(@PathVariable("id") int id){
        return employeeService.findEmployeeById(id);
    }

    @GetMapping("/count")
    @ResponseBody
    public Integer countHandle(){
        return employeeService.findEmployeeCount();
    }
}
  • 启动消费者,并用 postman 调试接口:

在这里插入图片描述

属性配置优先级

  • Dubbo 配置文件中各个标签属性配置的优先级总原则是:
    • 方法级优先,接口级(服务级)次之,全局配置再次之;
    • 如果级别一样,则消费者优先,提供者次之。

在这里插入图片描述

  • 另外,还有两个标签需要说明一下:
    • <dubbo:consumer/>设置在消费者端,用于设置消费者端的默认配置,即消费者端的全局设置。
    • <dubbo:provider/>设置在提供者端,用于设置提供者端的默认配置,即提供者端的默认配置。

配置建议

  • Provider 端要配置合理的 Provider 端属性
  • Provider 端上尽量多配置 Consumer 端属性
开课吧-javaEE企业级分布式高级架构师是一门专注于培养企业级应用开发的高级技术课程。该课程旨在帮助学员全面掌握Java EE企业级开发的技能和知识,培养他们成为具备分布式应用系统设计和架构能力的高级架构师。 在这门课程中,学员将学习Java EE的核心概念和技术,包括Servlet、JSP、JDBC、EJB、JNDI等。同时,学员还将深入学习分布式应用开发的相关技术,如Web服务、消息队列、分布式缓存负载均衡等。除此之外,课程还将涉及如何使用流行的Java EE开发框架(如Spring、Hibernate等)进行企业应用开发,并介绍分布式系统的设计原则和最佳实践。 通过学习这门课程,学员将能够了解分布式应用架构的基本原理,并具备设计和构建分布式应用系统的能力。他们将熟练掌握Java EE平台的各种技术和工具,能够灵活运用它们开发高性能、可扩展性强的企业级应用系统。此外,通过课程中的实战项目,学员还将锻炼解决实际问题和项目管理的能力。 作为一门高级架构师的课程,它将帮助学员进一步提升自己的职业发展。毕业后,学员可以在企业中担任分布式应用的架构师、系统设计师、技术经理等角色,负责企业级应用系统的设计和开发。此外,他们还可以选择独立开发,提供技术咨询和解决方案。 总之,开课吧-javaEE企业级分布式高级架构师是一门非常有价值的课程,它将帮助学员掌握Java EE企业级开发的核心技术和分布式应用架构的设计原理,培养他们成为具备高级架构师能力的软件开发专业人士。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值