Dubbo 基础知识以及使用方法 (持续更新中)

什么是Dubbo

Dubbo一款高性能的RPC远程调用框架,采用TCP长连接的方式进行远程调用,相比较HTTP来说效率要高很多

Dubbo架构

在这里插入图片描述

角色说明

  • Provider:服务的提供者
  • Consumer:服务的消费者
  • Registry:服务的注册中心,可以没有(采用消费者直连服务者的模式)
  • Monitor:提供者/消费者的监控中心,监控状态和数据,这个也可以没有且不会影响到Provider或Consumer
  • Contrainer:服务运行的容器,可以理解为Spring

调用关系说明:

  1. 服务提供者启动并创建服务在容器内
  2. 服务提供者在启动后将服务注册到Registry注册中心
  3. 服务消费者在启动后将向Registry服务注册中心订阅自己所需要的服务
  4. Registry服务注册中心将把订阅的服务提供者地址列表返回给消费者,如有变更时,将基于长连接的推送变更信息给消费者
  5. 服务消费者从服务提供者地址中根据负载均衡算法选取一台服务器调用服务,如果服务调用失败会再选取另一台
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

负载均衡

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

线程模型

Dubbo的服务用两个线程来进行服务的调用,一个是IO线程,一个是线程池的线程,通常我们在进行服务调用时,长耗时的操作,例如数据库操作,我们会放入线程池中,这样IO线程才不会被阻塞,而短耗时的操作可以在IO线程种直接执行,从而减少线程的调度提高处理速度

我们可以在<protocol>dispatchthreadpool属性来配置服务调用的派发和线程池的配置

多协议

Dubbo支持将一个服务通过多个协议暴露出来,也可以使某个服务使用dubb协议另一服务使用rmi协议这样的方式

单服务配置多协议案例:

<!-- 多协议配置 -->
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="rmi" port="1099" />
<!-- 使用dubbo协议暴露服务 -->
<dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" protocol="dubbo" />
<!-- 使用rmi协议暴露服务 -->
<dubbo:service interface="com.alibaba.hello.api.DemoService" version="1.0.0" ref="demoService" protocol="rmi" /> 

多服务配置多协议案例:

<!-- 多协议配置 -->
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="hessian" port="8080" />
<!-- 使用多个协议暴露服务 -->
<dubbo:service id="helloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" protocol="dubbo,hessian" />

多注册中心

Dubbo支持统一服务向不同注册中心同时注册,或者不同服务分别注册到不同服务上去,甚至可以同时引用注册在不同注册中心上的同名服务

多中心注册案例:

 <!-- 多注册中心配置 -->
<dubbo:registry id="hangzhouRegistry" address="10.20.141.150:9090" />
<dubbo:registry id="qingdaoRegistry" address="10.20.141.151:9010" default="false" />
<!-- 向多个注册中心注册 -->
<dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" registry="hangzhouRegistry,qingdaoRegistry" />

不同服务注册不同中心案例:

<!-- 多注册中心配置 -->
<dubbo:registry id="chinaRegistry" address="10.20.141.150:9090" />
<dubbo:registry id="intlRegistry" address="10.20.154.177:9010" default="false" />
<!-- 向中文站注册中心注册 -->
<dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" registry="chinaRegistry" />
<!-- 向国际站注册中心注册 -->
<dubbo:service interface="com.alibaba.hello.api.DemoService" version="1.0.0" ref="demoService" registry="intlRegistry" />

多中心服务引用案例:

<!-- 多注册中心配置 -->
<dubbo:registry id="chinaRegistry" address="10.20.141.150:9090" />
<dubbo:registry id="intlRegistry" address="10.20.154.177:9010" />
<!-- 引用中文站服务 -->
<dubbo:reference id="chinaHelloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" registry="chinaRegistry" />
<!-- 引用国际站站服务 -->
<dubbo:reference id="intlHelloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" registry="intlRegistry" />

参数验证

Dubbo支持提供者参数验证以及消费者参数验证

添加依赖:validation-apihibernate-validator,如果是SpringbootWeb可以不需要,因为已经有了

  1. 使用需要的注解在bean的属性上或者接口的参数上
// bean 属性
@NotNull
private String name;

// 接口方法
Object validation(@NotNull ValidationParameter parameter);
Object validation(@Min(1) int id);

// 分组验证
// 默认可按服务接口区分验证场景,等价于参数中用到的验证注解groups = ValidationService.class
public interface ValidationService { 
    // 使用方法分组,首字母大写,等价于该方法用到的验证注解groups = ValidationService.Save.class
    @interface Save{}
    void save(ValidationParameter parameter);
    // 使用默认的接口分组
    void update(ValidationParameter parameter);
}

// 关联验证
public interface ValidationService {   
    @GroupSequence(Update.class) // 同时验证Update组规则
    @interface Save{}
    void save(ValidationParameter parameter);
 
    @interface Update{} 
    void update(ValidationParameter parameter);
}
  1. 配置
// 在客户端验证参数,消费者在调用接口服务方法时进行参数的验证
<dubbo:reference validation="true" />

// 在服务器端验证参数,服务提供方法被调用时进行参数的验证
<dubbo:service validation="true" />
  1. 验证异常信息
try{
    // .... 
}catch (RpcException e) { // 抛出的是RpcException
    // 里面嵌了一个ConstraintViolationException
    ConstraintViolationException ve = (ConstraintViolationException) e.getCause();
    // 可以拿到一个验证错误详细信息的集合
    Set<ConstraintViolation<?>> violations = ve.getConstraintViolations(); 
    System.out.println(violations);
}

结果缓存

在某些场景下我们需要把结果缓存起来,以减少服务的压力,例如一个资源在一段时间内不会变化的资源,我们可以把它缓存起来,Dubbo为我们提供了这一功能,这个功能我们可以在消费者端实现

Dubbo已提供的缓存方式

<reference><service>标签指定cache属性,以此来指定缓存的类型,在调用这个服务时会检查缓存没有则会发起一次调用

<service>标签指定cache属性会应用在所有调用这个服务的客户端上,如果客户端上没有这个缓存类型的实现则会报错

案例:

<!--服务级别缓存-->
<dubbo:reference interface="com.foo.BarService" cache="lru" />

<!--方法级别缓存-->
<dubbo:reference interface="com.foo.BarService">
    <dubbo:method name="findBar" cache="lru" />
</dubbo:reference>

自定义缓存

有时候Dubbo的缓存机制并不能满足我们的需求,这是我们可以自定义扩展一个缓存

我们扩展一个缓存首先要知道,Dubbo缓存的时会将请求服务的参数作为键,结果作为值来缓存

1. 实现接口或集成类

我们可以实现org.apache.dubbo.cache.CacheFactory接口或者继承AbstractCacheFactory

继承AbstractCacheFactory类案例:
public class TimeCacheFactory extends AbstractCacheFactory {
  private static class TimeCache implements Cache {
    // 我们用于管理服务缓存的数据的map
    private HashMap<Object, Object> cacheMap = new HashMap<>();

    @Override
    public void put(Object key, Object value) {
      // 当这个服务的没有已缓存的数据(get时为null)时被调用
      // 参数key是调用的服务的参数字符串,value是服务返回的值
      cacheMap.put(key, value);
    }

    @Override
    public Object get(Object key) {
      // 每次请求服务时都会从Cache中get这个缓存值,参数key是请求这个服务参数的字符串
      // 多个参数用","分割,如果有非基本类型会将其转为JSON序列换的字符串
      Object obj = cacheMap.get(key);
      return obj;
      // 如果返回的值为null代表服务方法暂时没有缓存并且AbstractCacheFactory会去请求一次服务
      // 并把服务的结果调用put(key, value)希望将其服务返回的值放入缓存
    }
  }

  @Override
  protected Cache createCache(URL url) {
    // 每次请求这个缓存类型的服务或方法时都会被调用来请求一个缓存数据,如果在AbstractCacheFactory没有被保留维护
    // 这里必须要返回一个Cache对象否则会报错,返回的Cache对象将会自动被AbstractCacheFactory保留维护
    return new TimeCache();
  }
}
实现CacheFactory接口案例:
public class TimeCacheFactory implements CacheFactory {
  private static class TimeCache implements Cache {
    private HashMap<Object, Object> cacheMap = new HashMap<>();
    
    @Override
    public void put(Object key, Object value) {
      //与上面案例的put意义相同
      cacheMap.put(key, value);
    }

    @Override
    public Object get(Object key) {
      //与上面案例的get意义相同
      return cacheMap.get(key);
    }
  }

  @Override
  public Cache getCache(URL url, Invocation invocation) {
    // 每次调用服务时都会调用该方法,需要获取一个Cache,注意的是这里不能返回null
    return cache new TimeCache();
  }
}

2.声明扩展工厂类

我们还需要在META-INF/dubbo/org.apache.dubbo.cache.CacheFactory文件下配置xxx=com.xxx.XxxCacheFactory这样的一句话代表我们扩展了xxx缓存

3.设置cache属性

设置<reference><service>cache属性为我们扩展的xxx即可

泛化服务

泛化服务可以理解为一个通用的服务,它可以提供一些基础服务,消费者也可以在不知道具体服务的情况下引用一个不具体的服务

引用泛化的服务

在消费者端需要调用一个服务时,但是此时消费者不拥有服务接口类,此时消费者可以将要调用的服务泛化为通用的服务
官方案例

注意的是调用的服务参数及返回值中的所有 POJO 均用 Map 表示

通过 Spring 使用泛化调用

配置引用服务的generic属性为true,代表我们引用的是一个泛化的服务

<dubbo:reference interface="com.pr.testService" generic="true"/>
// 在使用注解时使用interfaceName属性指定完全限定名
@Reference(interfaceName="com.pr.testService", generic="true")

在代码中进行调用

// 我们泛化了testService这个服务,并获取这个泛化的服务类代理对象
@Reference(interfaceName="com.pr.testService", generic="true")
GenericService testService; 

// 希望调用testService的testMethod方法并返回值
Object result = testService.$invoke("testMethod", new String[] {java.lang.String}, new String[] {"方法参数"});

GenericService 解释

GenericService是代表了一个泛化了的服务,这个服务可以是任意一个服务,消费端通过这个类调用服务

$invoke()方法

这个方法是一个同步方法,代表我们将要去调用泛化服务的某个方法,传入参数并获取返回值的方法

它有3个参数

  • method(参数1):将要调用的泛化服务的方法的方法名称,必须存在
  • parameterTypes(参数2):泛化服务调用的方法参数类型字符串数组,这是一个数组,如果有多个参数那么就对应的有多个参数类型,这个是按方法的顺序填充的,值为参数类型的完全限定名
  • args(参数3):泛化服务调用的方法参数实参对象,这个顺序必须对应parameterTypes参数的顺序,如果对应parameterTypes位置的参数类型是POJO类型或抽象的POJO类型(接口、抽象类等)
    • 前者:必须是一个Map并指定属性的键值对
    • 后者:在前者的基础上再指定一个class属性,其值为这个POJO的实现类,代表我们实参使用的类型

i n v o k e ( ) 还 有 一 个 invoke()还有一个 invoke()invokeAsync()的异步调用方法

实现泛化服务

实现泛化服务时服务提供者需要做的事情,实现一个泛化服务很简单

  1. 创建一个服务,使其实现GenericService接口并暴露即可

回声测试

回声测试通常用于测试服务是否可用,回声测试按照正常请求流程执行,能够测试整个调用是否通畅

所有服务自动实现 EchoService 接口,只需将任意服务引用强制转型为 EchoService,即可使用

案例:

// 强制转为回声服务
EchoService echoService = (EchoService) this.testService;
// 调用回声服务测试请求是否完整,返回值类型是序列化的字符串
Object retObj = echoService.$echo(text);

上下文信息

上下文中存放的是当前调用过程中所需的环境信息。所有配置信息都将转换为 URL 的参数

RpcContext 是一个 ThreadLocal 的临时状态记录器,当接收到 RPC 请求,或发起 RPC 请求时,RpcContext 的状态都会变化。比如:A 调 B,B 再调 C,则 B 机器上,在 B 调 C 之前,RpcContext 记录的是 A 调 B 的信息,在 B 调 C 之后,RpcContext 记录的是 B 调 C 的信息

如果还没有调用任意服务的话是不会产生RpcContext上下文数据的

获取上下文信息

我们可以通过RpcContext.getContext()的方式获取

隐式参数

我们可以通过RpcContext上的setAttachmentgetAttachment在服务消费方和提供方之间进行参数的隐式传递

setAttachment设置的 KV 对,在完成下面一次远程调用会被清空,即多次远程调用要多次设置

异步服务的调用/执行

在一些场景下我们可能需要一个异步的调用,可能是提供者耗时,可能是我们不需要结果等等

例如以下场景:

  • 提供者长耗时操作导致高并发下提供者线程池被大量占用导致线程池崩溃不可用
  • 消费者不关心提供者服务的返回结果,只关心调用
  • 消费者需要调用服务后立即处理其他业务

使用CompletableFuture签名的接口进行异步

这种异步的好处是我们可以在提供端将异步分配给不同业务线程池使其减轻dubbo的线程池,消费端可灵活的选择是否异步

  1. 定义返回CompletableFuture的接口
public interface AsyncService {
    CompletableFuture<String> sayHello(String name);
}
  1. 提供者实现接口
@Override
    public CompletableFuture<String> sayHello(String name) {
        // 建议为supplyAsync提供自定义线程池,避免使用JDK公用线程池
        return CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "异步响应";
        });
    }
  1. 暴露和引用服务
<!--正常暴露服务-->
<bean id="asyncService" class="com.xxx.impl.AsyncServiceImpl"/>
<dubbo:service interface="com.xxx.api.AsyncService" ref="asyncService"/>

<!--这里如果客户端不异步要注意一个属性timeout的时间-->
<dubbo:reference id="asyncService" timeout="10000" interface="com.xxx.api.AsyncService"/>
  1. 消费者调用这个异步服务
// 调用直接返回CompletableFuture
CompletableFuture<String> future = asyncService.sayHello("hello");
// 增加回调函数
future.whenComplete((v, t) -> {
    if (t != null) {
        t.printStackTrace();
    } else {
        System.out.println("Response: " + v);
    }
});
System.out.println("消费者流程结束");

使用RpcContext进行异步

通过RpcContext时接口的返回值可以不是CompletableFuture接口,通过RpcContext来处理异步数据

  1. 服务接口定义与暴露
public interface AsyncService {
    String sayHello(String name);
}
<!--正常暴露服务-->
<bean id="asyncService" class="com.xxx.impl.AsyncServiceImpl"/>
<dubbo:service interface="com.xxx.api.AsyncService" ref="asyncService"/>
  1. 服务实现
public String sayHello(String name) {
    // 开始异步Rpc上下文
    final AsyncContext asyncContext = RpcContext.startAsync();
    // 这里可以使用一个线程池
    new Thread(() -> {
        // 对于线程如果要使用上下文,则必须要放在第一句执行
        asyncContext.signalContextSwitch();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
        // 写回响应给Rpc上下文
        asyncContext.write("Hello " + name + ", 这是Rpc异步调用");
    }).start();
    // 返回什么无所谓,真正的内容在异步线程中
    return null;
}
  1. 消费者引用服务
<dubbo:reference id="asyncService" interface="com.xxx.api.AsyncService">
    <!--注意的是async属性是需要的,这会忽略sayHello的方法返回值且不阻塞-->
    <dubbo:method name="sayHello" async="true" />
</dubbo:reference>
  1. 消费者调用(两种方式)
// 此调用会立即返回结果
asyncService.sayHello("world");
// 拿到调用的Future引用,当结果返回后,会被通知和设置到此Future
CompletableFuture<String> helloFuture = RpcContext.getContext().getCompletableFuture();
// 为Future添加回调
helloFuture.whenComplete((retValue, exception) -> {
    // retValue是RpcContext写回的异步结果,exception这是异常信息
});
CompletableFuture<String> future = RpcContext.getContext().asyncCall(() -> {asyncService.sayHello("请求参数");});
future.get(); // 等待结果

参数回调

Dubbo 将基于长连接生成反向代理,这样就可以从服务器端调用客户端逻辑,使其参数回调,通常用于处理事件、通知、等等

1. 定义回调接口

public interface CallbackListener {
    void changed(String msg);
}

2. 服务提供者方法参数中添加回调接口参数

public void addChangedListener(CallbackListener callback) {
    callback.changed();
}
<!--服务提供者端配置-->
<bean id="callbackService" class="com.callback.impl.ChangedListenerServiceImpl" />
<dubbo:service interface="com.callback.ChangedListenerService" ref="callbackService">
    <dubbo:method name="addListener">
        <dubbo:argument index="1" callback="true" />
        <!--也可以通过指定类型的方式-->
        <!--<dubbo:argument type="com.demo.CallbackListener" callback="true" />-->
    </dubbo:method>
</dubbo:service>

3. 服务消费者请求服务

changedListenerService.addChangedListener(new CallbackListener(){
    void changed(String msg){ /*处理回调 */ }
})
<!--消费端配置-->
<dubbo:reference id="changedListenerService" interface="com.callback.ChangedListenerService" />

事件通知

在调用之前、调用之后、出现异常时,会触发 oninvokeonreturnonthrow 三个事件,可以配置当事件发生时,通知哪个类的哪个方法

1. 自定义通知类

public class Notify {
    // 这些通知方法可以自定义的
    public void onreturn(Person msg, Integer id){}
    public void onthrow(Throwable ex, Integer id) {}
}

2. 服务消费者回调配置

<bean id ="demoCallback" class = "com.lg.Notify" />
<dubbo:reference id="demoService" interface="com.lg.DemoService">
    <dubbo:method name="get" async="true" onreturn="demoCallback.onreturn" onthrow="demoCallback.onthrow" />
    <!--async属性不是必要的-->
</dubbo:reference>

本地伪装

本地伪装通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过本地模拟数据返回

基本使用方式

1. 消费端配置服务mock

<dubbo:reference interface="com.service.TestService" mock="com.service.TestServiceMock" />

2. 实现服务接口完成mock类

public class TestServiceMock implements TestService {
    public string test1() {
        // 在消费者调用TestService服务失败时将使用mock这个方法的数据
        return "失败的调用";
    }
}

方法级别的mock

Mock 可以在方法级别上指定,假定com.service.TestService上有好几个方法,我们可以单独为某方法指定 Mock 行为

<dubbo:reference id="testService" check="false" interface="com.service.TestService">
    <dubbo:parameter key="someMethod.mock" value="return empty"/>
</dubbo:reference>

令牌验证

通过令牌验证在注册中心控制权限,以决定要不要下发令牌给消费者,可以防止消费者绕过注册中心访问提供者,另外通过注册中心可灵活改变授权方式,而不需修改或升级提供者

令牌可以设置为

  • 全局令牌:provider.token
  • 服务令牌:service.token

优雅停机

Dubbo提供了一套优雅的停机机制,可以通过kill PID的方式触发

服务提供方优雅停机

停止时,先标记为不接收新请求,新请求过来时直接报错,让客户端重试其它机器,然后,检测线程池中的线程是否正在运行,如果有,等待所有线程执行完成,除非超时,则强制关闭

服务消费方优雅停机

停止时,不再发起新的调用请求,所有新的调用在客户端即报错,然后,检测有没有请求的响应还没有返回,等待响应返回,除非超时,则强制关闭

设置方式

// 设置优雅停机超时时长 单位毫秒
dubbo.service.shutdown.wait=15000

如果通过tomcat等容器启动Dubbo这可以调用DubboShutdownHook.destroyAll()方法来进行优雅停机

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值