dubbo学习

dubbo学习

1. dubbo注解整合基础案例

参考下列博客:
手把手教你使用Springboot整合dubbo,搭建一个微服务

2. dubbo负载均衡

负载均衡:负载均衡是指在集群中,将多个数据请求分散在不同单元上进行执行,主要为了提高系统容错能力和加强系统对数据的处理能力。
dubbo负载均衡机制是决定一次服务调用哪个提供者的服务
在dubbo运行的过程中,有两个位置会用到负载均衡:

  1. 有多个注册中心时,客户端使用负载均衡选择其中一个注册中心上注册的服务
  2. 客户端使用负载均衡选择一个注册中心上注册的多个服务
    dubbo的负载均衡算法有下列五种:(默认选择RandomLoadBalance随机算法)
    在这里插入图片描述
    RandomLoadBalance:随机负载均衡。随机的选择一个。是Dubbo的默认负载均衡策略。
    RoundRobinLoadBalance:轮询负载均衡。轮询选择一个。
    LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机。活跃数指调用前后计数差。使慢的 Provider 收到更少请求,因为越慢的 Provider 的调用前后计数差会越大。
    ConsistentHashLoadBalance:一致性哈希负载均衡。相同参数的请求总是落在同一台机器上。

我们修改dubbo-provider的代码
UserServiceImpl.java

@DubboService
@Service
@Slf4j
public class UserServiceImpl implements IUserService {
    @Autowired
    private UserMapper userMapper;
    @Value("${server.port}")
    private Integer port;
    @Override
    public User selectUserById(Long id){
        log.info("当前访问的提供者的port是:{}",port);
        User user=userMapper.selectUserById(id);
        return user;
    }
}

修改dubbo-consumer的代码
ConsumerUserController.java

@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController {
    @DubboReference(protocol = "dubbo",loadbalance = "random")
    private IUserService userService;
    @RequestMapping("/selectUserById/{id}")
    public User getUser(@PathVariable("id")Long id){
        User user=userService.selectUserById(id);
        log.info("response from provider:{}",user);
        return user;
    }
}

修改provider的application.yml

server:
  port: 8091
spring:
  application:
    name: dubbo-samples-provider-springCloud
  datasource:
    username: root
    password: 3fa4d180
    url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  mapper-locations: /mapper/*.xml
dubbo:
  registry:
    address: zookeeper://127.0.0.1:2181
  protocol:
    name: dubbo
    port: 20811

修改配置,设置多实例
在这里插入图片描述
运行一份ProviderApp后,修改application.yml的server.port和protocol.port为8182和20812,8183和20813,总共创建三份实例,然后运行ConsumerApp,多次访问consumer提供的接口
控制台结果如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
说明确实为随机访问。
接下来我们修改dubbo-consumer,修改负载均衡的方式

@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController {
    @DubboReference(protocol = "dubbo",loadbalance = "roundrobin")
    private IUserService userService;
    @RequestMapping("/selectUserById/{id}")
    public User getUser(@PathVariable("id")Long id){
        User user=userService.selectUserById(id);
        log.info("response from provider:{}",user);
        return user;
    }
}

重启consumer,多次访问selectuserById,一共访问9次,每个ProviderApp实例出现此时都为3次
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Dubbo 的配置,如果存在多个的话,是会有覆盖关系的:

  1. 方法级优先,接口级次之,全局配置次之
  2. 如果级别一样,则消费方优先,提供方次之

所以,上面4中配置的优先级是:

  1. 客户端方法级别配置
  2. 客户端接口级别配置
  3. 服务端方法级别配置
  4. 服务端接口级别配置

我们修改ProviderApp的配置,在UserServiceImpl修改负载均衡策略为轮询

@DubboService(loadbalance = "roundrobin")
@Service
@Slf4j
public class UserServiceImpl implements IUserService {
    @Autowired
    private UserMapper userMapper;
    @Value("${server.port}")
    private Integer port;
    @Override
    public User selectUserById(Long id){
        log.info("当前访问的提供者的port是:{}",port);
        User user=userMapper.selectUserById(id);
        return user;
    }
}

修改Consumer,将负载均衡策略改为random

@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController {
    @DubboReference(protocol = "dubbo",loadbalance = "random")
    private IUserService userService;
    @RequestMapping("/selectUserById/{id}")
    public User getUser(@PathVariable("id")Long id){
        User user=userService.selectUserById(id);
        log.info("response from provider:{}",user);
        return user;
    }
}

重启实例,多次访问selectUserById,虽然服务提供方的策略是轮询,但消费方的策略是随机,结果如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
参考连接:Dubbo的负载均衡

3. dubbo调用机制

远程调用是dubbo框架的核心,基本过程是,向服务端发送参数,并等待获取结果。如果调⽤过程出错则需要对异常进⾏处理。Dubbo调用类别有四种,分别是同步,异步,并行以及广播调用。默认情况下Dubbo是采⽤同步⽅式进⾏调⽤,即发起调⽤后线程将会阻塞,直到结果返回。同时为了提⾼性能,也可采用异⽅式调⽤。另外⼀些特殊的业务场景下,则可以使⽤⼴播⽅式调⽤、以及并⾏调⽤。
在这里插入图片描述#### 3.1 同步调用:向远程服务器发送请求,该请求是阻塞的,直到服务器返回结果。在这里插入图片描述
客户端线程发送给服务端时,其实有专门的io线程完成,它是异步发送的,但dubbo会构建一个CompletableFuture,通过它阻塞当前线程去等待结果返回,但服务端结果返回之后completableFuture填充结果,并释放阻塞的调用线程

3.2 异步调用

在特殊场景下,异步调用可提高性能,比如要远程调用A、B、C,分别用时1s,2s,3s,如果是同步的话,用时6秒,如果异步调用,同时去执行这三个接口,理想情况下用时3秒,不过前提是ABC这三个方法没有参数依赖并且没有顺序依赖。
修改dubbo-api模块,修改接口

public interface IUserService {
    User selectUserById(Long id);
    User asyncSelectUserById(Long id);
    User syncSelectUserById(long id);
}

修改dubbo-provider,修改UserServiceImpl

@DubboService(loadbalance = "roundrobin")
@Service
@Slf4j
public class UserServiceImpl implements IUserService {
    @Autowired
    private UserMapper userMapper;
    @Value("${server.port}")
    private Integer port;
    @Override
    public User selectUserById(Long id){
        log.info("当前访问的提供者的port是:{}",port);
        User user=userMapper.selectUserById(id);
        return user;
    }

    @Override
    public User asyncSelectUserById(Long id) {
        log.info("异步访问获取用户,port:{}",port);
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return userMapper.selectUserById(id);
    }
     @Override
    public User syncSelectUserById(Long id) {
        log.info("同步访问获取用户,port:{}",port);
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return userMapper.selectUserById(id);
    }
}

修改dubbo-consumer,修改consumerUserController

@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController {
	//设置asyncSelectUserById为异步调用
    @DubboReference(protocol = "dubbo",loadbalance = "random",
    methods = {@Method(name = "asyncSelectUserById",async = true)})
    private IUserService userService;
    @RequestMapping("/selectUserById/{id}")
    public User getUser(@PathVariable("id")Long id){
        User user=userService.selectUserById(id);
        log.info("response from provider:{}",user);
        return user;
    }
    @RequestMapping("/syncSelectUserById/{id}")
    public User asyncGetUser(@PathVariable("id")Long id){
        long start = System.currentTimeMillis();
        User user=null;
        user=userService.syncSelectUserById(id);
        user=userService.syncSelectUserById(id);
        user=userService.syncSelectUserById(id);
        long end=System.currentTimeMillis();
        log.info("用时:{}",end-start);
        return user;
    }
    @RequestMapping("/asyncSelectUserById/{id}")
    public User asyncGetUser2(@PathVariable("id")Long id){
        long start = System.currentTimeMillis();
        User user=null;
        userService.asyncSelectUserById(id);
        Future<User> future1 = RpcContext.getServletContext().getFuture();
        userService.asyncSelectUserById(id);
        Future<User> future2 = RpcContext.getServletContext().getFuture();
        userService.asyncSelectUserById(id);
        Future<User> future3 = RpcContext.getServletContext().getFuture();
        try{
            user=future1.get();
            user=future2.get();
            user=future3.get();
        }catch (InterruptedException e){
            e.printStackTrace();
        }catch (ExecutionException e){
            e.printStackTrace();
        }
        long end=System.currentTimeMillis();
        log.info("用时:{}",end-start);
        return user;
    }
}

重启服务,分别访问syncSelectUserById和asyncSelectUserById
syncSelectUserById
在这里插入图片描述
asyncSelectUserById
在这里插入图片描述
实现原理:事实上Dubbo的调用本身就是异步的,其常规的调用是通过AsyncToSyncInvoker组件,由异步转成了同步,所以异步的实现就是让该组件不去执行阻塞逻辑即可,此外,为了顺利拿到结果回执(Future),在调用发起之后其回执会被填充到RpcContext当中
在这里插入图片描述

3.3 并行调用

为了尽可能获得更⾼的性能,以及最⾼级别的保证服务的可⽤性。⾯对多个服务,并不知道哪个处理更快。这时客户端可并⾏发起多个调⽤,只要其中⼀个成功即返回,某个服务异常直接勿略,只有所有服务多出现异常情况下才会判定调⽤出错。
由于并行调用特殊性,生产环境不建议使用,如果一定要用,请配置在method级别,这里不做案例演示
并⾏调⽤原理是通过线程池异步发送远程请求。其流程如下:

  1. 根据forks数量挑选出服务节点
  2. 基于线程池(ExecutorService)并⾏发起远程调⽤
  3. 基于阻塞队列(BlockingQueue)等待结果返回
  4. 第⼀个结果返回,填充阻塞列,并释放线程
    注:并行调用时,不能同时设置为异步调用,即async=true
3.4 广播调用

在分布式环境,⼀个服务通常有多个提供⽅。原则上调⽤任意⼀个服务,结果都应该是⼀样的。但⼀些特殊场景除外,⽐如:“通知缓存更新”,需要通知到每个服务器都要更新各⾃节点缓存。要确保每个服务都被调⽤到。⼴播调⽤⼀次调⽤,会遍历所有提供者并发起调⽤,确保所有节点都被调⽤到。任意⼀台报错就算失败

参考文章:Dubbo调用及容错机制详解

4. 超时、重试机制

Dubbo主要内置了如下几种策略:
Failover(失败自动切换):高可用系统中的一个常用概念,服务器通常拥有主备两套机器配置,如果主服务器出现故障,则自动切换到备服务器中,从而保证了整体的高可用性。这是Dubbo对的默认容错策略,当调用出现失败时,根据配置的重试次数,会自动从其他可用地址中重新选择一个可用的地址进行调用,直到调用成功,或者达到重试的上限位置。
Failsafe(失败安全):失败安全策略的核心是即使失败了也不会影响整个调用流程。通常情况下用于旁路系统或流程中,它的失败不影响核心业务的正确性。在实现上,当出现调用失败时,会忽略此错误,并记录一条日志,同时返回一个空结果,在上游看来调用是成功的。应用场景,可以用于写入审计日志等操作。
Failfast(快速失败):某些业务场景中,某些操作可能是非幂等的,如果重复发起调用,可能会导致出现脏数据等。例如调用某个服务,其中包含一个数据库的写操作,如果写操作完成,但是在发送结果给调用方的过程中出错了,那么在调用发看来这次调用失败了,但其实数据写入已经完成。这种情况下,重试可能并不是一个好策略,这时候就需要使用到Failfast策略,调用失败立即报错。让调用方来决定下一步的操作并保证业务的幂等性。
Failback(失败自动恢复):Failback通常和Failover两个概念联系在一起。在高可用系统中,当主机发生故障,通过Failover进行主备切换后,待故障恢复后,系统应该具备自动恢复原始配置的能力。
Dubbo中的Failback策略中,如果调用失败,则此次失败相当于Failsafe,将返回一个空结果。而与Failsafe不同的是,Failback策略会将这次调用加入内存中的失败列表中,对于这个列表中的失败调用,会在另一个线程中进行异步重试,重试如果再发生失败,则会忽略,即使重试调用成功,原来的调用方也感知不到了。因此它通常适合于,对于实时性要求不高,且不需要返回值的一些异步操作。
在这里插入图片描述
我们以failover为例,编写案例
修改Dubbo-api的接口
在IUserService接口中添加retrySelectUserById方法

public interface IUserService {
    User selectUserById(Long id);
    User asyncSelectUserById(Long id);
    User syncSelectUserById(Long id);
    User retrySelectUserById(Long id);
}

在dubbo-provider模块中,修改UserServiceImpl,添加retrySelectUserById方法

 @Override
    public User retrySelectUserById(Long id) {
        log.info("超时重传获取用户id,port:{}",port);
        try{
            Thread.sleep(4000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        User user = userMapper.selectUserById(id);
        log.info("user:{}",user);
        return user;
    }

在dubbo-consumer中,修改ConsumerUserController.java

  @DubboReference(protocol = "dubbo",loadbalance = "random",
    methods = {@Method(name = "asyncSelectUserById",async = true),
    @Method(name = "retrySelectUserById",retries = 3,timeout = 2000)})
    private IUserService userService;
    @RequestMapping("/retrySelectUserById/{id}")
    public User retrySelectUserById(@PathVariable("id")Long id){
        User user = userService.retrySelectUserById(id);
        log.info("retryUser:{}",user);
        return user;
    }

重启,访问retrySelectUserById一次,因为我们设置超时时间为2000ms,而服务端Thread.sleep(4000),所以会超时,同时因为在消费端设置retry为3,所以会重试3次,也就是说一共会访问4次方法
在这里插入图片描述
在这里插入图片描述
超时重试存在一些问题,如果我们执行的是一个插入操作,假设插入业务用时3秒,而我们超时时间设置为2秒,那么因为超时,可能会导致重复插入数据,这时就需要考虑到分布式事务以及幂等性方面的问题了,这里我们不做详细探讨。

5. 本地存根

远程服务后,客户端通常只剩下接口,而实现全在服务端,但提供方有时想在客户端也执行部分逻辑
在这里插入图片描述
使用场景:做ThreadLocal缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在API中带上Stub,客户端生成Proxy实例,会把Proxy通过构造函数传给Stub,然后把Stub暴露给用户,Stub可以决定要不要去调Proxy
在dubbo-api模块中,添加IUserService2接口

public interface IUserService2 {
    User getUserById(Long id);
}

在dubbo-provider模块中,添加IUserService2的实现

@Service
@DubboService
@Slf4j
public class UserService2Impl implements IUserService2 {
    @Autowired
    private UserMapper userMapper;
    @Value("${server.port}")
    private Integer port;
    @Override
    public User getUserById(Long id) {
        log.info("超时重传获取用户id,port:{}",port);
        try{
            Thread.sleep(4000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        User user = userMapper.selectUserById(id);
        log.info("user:{}",user);
        return user;
    }
}

修改dubbo-consumer模块,添加上IUserService2的本地存根

public class UserServiceStub implements IUserService2 {
    private IUserService2 userService2;
    public UserServiceStub(IUserService2 userService2){
        this.userService2=userService2;
    }
    @Override
    public User getUserById(Long id) {
        try{
            //检查参数是否有误
            if (id<=0){
                System.out.println("参数有误");
                return null;
            }
            return userService2.getUserById(id);
        }catch (Exception e){
//            e.printStackTrace();
            System.out.println("启动容错===========");
            User user=new User();
            user.setId(0L);
            user.setUsername("容错");
            user.setPassword("本地存根");
            return user;
        }
    }
}

修改ConsumerUserController,添加下述方法

@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController { 
    @DubboReference(stub = "com.young.customer.stub.UserServiceStub")
    private IUserService2 userService2;
    @RequestMapping("/stubSelectUserById/{id}")
    public User stubSelectUserById(@PathVariable("id")Long id){
        return userService2.getUserById(id);
    }
 }

重启,访问
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
参考文章:Dubbo(十二)dubbo的服务版本配置以及本地存根使用介绍

6. 灰度发布

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。灰度发布开始到结束期间的这一段时间,称为灰度期。
下面简单演示一下dubbo通过version版本号进行灰度发布
创建一个项目,假设叫dubbo02,引入下列依赖:
pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.young</groupId>
    <artifactId>dubbo02</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>dubbo-api</module>
        <module>dubbo-provider</module>
        <module>dubbo-consumer</module>
    </modules>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <spring-boot-dependencies.version>2.7.0</spring-boot-dependencies.version>
        <spring-cloud-dependencies.version>2021.0.1</spring-cloud-dependencies.version>
        <spring-dubbo.version>2.0.0</spring-dubbo.version>
        <lombok.version>1.18.22</lombok.version>
        <dubbo.version>3.0.2.1</dubbo.version>
        <apache.commons-fileupload.version>1.4</apache.commons-fileupload.version>
        <apache.commons-text.version>1.9</apache.commons-text.version>
        <apache.commons-configuration.version>1.10</apache.commons-configuration.version>
        <httpclient.version>4.5.12</httpclient.version>
        <hutool.version>5.7.7</hutool.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot-dependencies.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-bom</artifactId>
                <version>${dubbo.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-spring-boot-starter</artifactId>
                <version>${dubbo.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <scope>compile</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

创建dubbo-api模块,引入依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>dubbo02</artifactId>
        <groupId>com.young</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>dubbo-api</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    </dependencies>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

</project>

User.java实体类

package com.young.entity;

import lombok.Data;

import java.io.Serializable;

@Data
public class User implements Serializable {
    private Integer id;
    private String username;
    private String password;
    private String school;
}

UserService.java接口

package service;

import com.young.entity.User;

public interface UserService {
    User getUserById();
}

创建dubbo-provider模块,提供服务
引入依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>dubbo02</artifactId>
        <groupId>com.young</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>dubbo-consumer</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-registry-zookeeper</artifactId>
        </dependency>
        <dependency>
            <groupId>com.young</groupId>
            <artifactId>dubbo-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

UserServiceImpl.java实现类

package com.young.service.impl;

import com.young.entity.User;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import service.UserService;

@Service
@DubboService(version = "1.0.0")
public class UserServiceImpl implements UserService {
    @Override
    public User getUserById() {
        User user=new User();
        user.setUsername("cxy");
        user.setPassword("123456");
        user.setId(1);
        user.setSchool("华南理工大学");
        return user;
    }
}

UserServiceImpl2.java实现类

package com.young.service.impl;

import com.young.entity.User;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import service.UserService;

@Service
@DubboService(version = "2.0.0")
public class UserServiceImpl2 implements UserService {
    @Override
    public User getUserById() {
        User user=new User();
        user.setUsername("dhi");
        user.setPassword("123456");
        user.setId(1);
        user.setSchool("潮阳一中");
        return user;
    }
}

ProviderApp.java

package com.young;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableDubbo
public class ProviderApp {
    public static void main(String[] args) {
        SpringApplication.run(ProviderApp.class,args);
    }
}

application.yml

server:
  port: 8089
spring:
  application:
    name: provider-service
dubbo:
  registry:
    address: zookeeper://127.0.0.1:2181
  protocol:
    name: dubbo
    port: 29089

创建dubbo-consumer模块,依赖和刚才provider模块一样
DemoController.java

package com.young.controller;

import com.young.entity.User;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import service.UserService;

@RestController
@RequestMapping("/demo")
public class DemoController {
    @DubboReference(version = "1.0.0")
    private UserService userService1;
    @DubboReference(version = "2.0.0")
    private UserService userService2;

    @GetMapping("/v1")
    public User getUser1(){
        return userService1.getUserById();
    }
    
    @GetMapping("/v2")
    public User getUser2(){
        return userService2.getUserById();
    }
}

application.yml

server:
  port: 8099
spring:
  application:
    name: consumer-service
dubbo:
  protocol:
    name: dubbo
    port: 29099
  registry:
    address: zookeeper://localhost:2181

分别访问/demo/v1 和/demo/v2
在这里插入图片描述
在这里插入图片描述
刚才的项目,只是演示了如何区分不同版本的服务,我们一般使用version或group来对新旧服务进行区分,至于灰度发布的实现,除了刚才提到的新旧版本区分之外,还需要通过使用nginx方向代理、dubbo路由规则等,来实现灰度发布。
除此之外,在灰度发布的实践过程中,我们还会遇到项目的问题:

  1. 如何设置流量比例
  2. 如何确保新旧版本共存

dubbo灰度发布带来的好处:

  1. 减小对用户的影响,提高用户体验
  2. 降低系统升级风险,提高系统可靠性
  3. 更好地支持持续集成和持续交付,提高开发效率

7. dubbo服务鉴权

在dubbo中,我们有时候要对调用者进行鉴权,保证我们的服务只提供给某些服务调用。因此这里就设计到Dubbo服务鉴权,关于dubbo服务鉴权,有两种方式:

  1. 将token作为参数发送到服务提供者端,获取RpcContext中的token参数,并进行鉴权验证
  2. 基于IP白名单的服务鉴权:将允许访问服务的IP地址添加到白名单中,服务提供者在接收到请求后比对请求后比对请求IP地址与白名单是否匹配,从而判断请求是否合法

我们分别对上述两种方式进行案例实现
首先是token,我们要将token从消费者服务传递到提供者服务,这里便涉及到一个隐式传参的问题,而隐式传参,可以使用上下文信息,即RpcContext实现,下面是Dubbo3中RpcContext的四大模块

ServiceContext:在 Dubbo 内部使用,用于传递调用链路上的参数信息,如 invoker 对象等
ClientAttachment:在 Client 端使用,往 ClientAttachment 中写入的参数将被传递到 Server 端
ServerAttachment:在 Server 端使用,从 ServerAttachment 中读取的参数是从 Client 中传递过来的
ServerContext:在 Client 端和 Server 端使用,用于从 Server 端回传 Client 端使用,Server 端写入到 ServerContext 的参数在调用结束后可以在 Client 端的 ServerContext 获取到

我们修改刚才灰度发布相关的代码,在dubbo-consumer模块和dubbo-provider模块加上spring-boot-starter-data-redis的依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

先修改消费者模块的DemoController.java,这里的hasToken中,我们将token的值隐式传递给消费者

package com.young.controller;

import com.young.entity.User;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import service.UserService;

import java.util.UUID;

@RestController
@RequestMapping("/demo")
public class DemoController {
    @DubboReference(version = "1.0.0")
    private UserService userService1;
    @DubboReference(version = "2.0.0")
    private UserService userService2;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/v1")
    public User getUser1(){
        return userService1.getUserById();
    }

    @GetMapping("/v2")
    public User getUser2(){
        return userService2.getUserById();
    }

    @GetMapping("/token/yes")
    public User hasToken(){
        String token= UUID.randomUUID().toString();
        stringRedisTemplate.opsForValue().set("authorization",token);
        System.out.println("token:"+token);
        RpcContext.getClientAttachment().setAttachment("authorization",token);
        RpcContext.getClientAttachment().setAttachment("name","cxy");
        User user = userService1.getUserById();
        return user;
    }

    @GetMapping("/token/no")
    public User notToken(){
        User user = userService1.getUserById();
        return user;
    }

    @GetMapping("/name")
    public String name(){
        String interfaceName = RpcContext.getServerContext().getInterfaceName();
        String methodName = RpcContext.getServerContext().getMethodName();
        String localHostName = RpcContext.getServerContext().getLocalHostName();
        String remoteApplicationName = RpcContext.getServerContext().getRemoteApplicationName();
        String remoteHostName = RpcContext.getServerContext().getRemoteHostName();
        System.out.println("interface:"+interfaceName);
        System.out.println("method:"+methodName);
        System.out.println("localHost"+localHostName);
        System.out.println("remoteHost:"+remoteHostName);
        System.out.println("remoteApplication:"+remoteApplicationName);
        return remoteApplicationName;
    }
}

修改application.yml

server:
  port: 8099
spring:
  application:
    name: consumer-service
  redis:
    jedis:
      pool:
        max-wait: 1000
        max-idle: 8
        min-idle: 0
    timeout: 5000
    host: 127.0.0.1
    port: 6379
dubbo:
  protocol:
    name: dubbo
    port: 29099
  registry:
    address: zookeeper://localhost:2181

修改dubbo-provider模块,首先,先添加TokenAuthFilter类

package com.young.filter;

import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Activate
@Component
public class TokenAuthFilter implements Filter {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    private static final String TOKEN_KEY="authorization";
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String token = RpcContext.getServerAttachment().getAttachment(TOKEN_KEY);
        String name = RpcContext.getServerAttachment().getAttachment("name");
        System.out.println("token:"+token+",name:"+name);
        if (token == null || !validateToken(token)) {
            throw new RpcException("UnValid Token");
        }
        return invoker.invoke(invocation);
    }

    private boolean validateToken(String token) {
        String resToken = stringRedisTemplate.opsForValue().get(TOKEN_KEY);
        if (token.equals(resToken)) {
            return true;
        }
        return false;
    }
}

接着,在resource目录下,新建META-INF目录,在META-INF目录下创建dubbo目录,然后在dubbo目录下创建org.apache.dubbo.rpc.Filter文件,文件内容如下:

tokenAuthFilter=com.young.filter.TokenAuthFilter
# 格式为: 过滤器名称=过滤器对应的包路径

然后最重要的一点,就是修改application.yml

server:
  port: 8089
spring:
  application:
    name: provider-service
  redis:
    jedis:
      pool:
        max-wait: 1000
        max-idle: 8
        min-idle: 0
    timeout: 5000
    host: 127.0.0.1
    port: 6379
  dubbo:
    provider:
      filter: tokenAuthFilter
dubbo:
  provider:
    filter: tokenAuthFilter #这个必须配置,不然filter不生效
  registry:
    address: zookeeper://127.0.0.1:2181
  protocol:
    name: dubbo
    port: 29089

项目的完整结构如下:
在这里插入图片描述
启动项目,进行测试
访问/demo/token/yes
在这里插入图片描述
在这里插入图片描述
访问其他接口
在这里插入图片描述在这里插入图片描述
第二种方式,是使用IP白名单来进行权限校验,我这里修改一下,改为使用服务名进行权限校验,我们先创建一个表,表中包含服务名称,以及调用者的服务名称,用来表示有哪些调用者可以使用当前服务,结构如下:
在这里插入图片描述
数据内容如下:
在这里插入图片描述

在dubbo-provider模块中,引入mysql和mybatis-plus的依赖
修改application.yml

server:
  port: 8089
spring:
  application:
    name: provider-service
  datasource:
    username: root
    password: 3fa4d180
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: mysql:jdbc://localhost:3306/young?useSSL=false&serverTimezone
  redis:
    jedis:
      pool:
        max-wait: 1000
        max-idle: 8
        min-idle: 0
    timeout: 5000
    host: 127.0.0.1
    port: 6379
  dubbo:
    provider:
      filter: tokenAuthFilter
dubbo:
  provider:
    filter: tokenAuthFilter
  registry:
    address: zookeeper://127.0.0.1:2181
  protocol:
    name: dubbo
    port: 29089
mybatis-plus:
  global-config:
    db-config:
      logic-not-delete-value: 0
      logic-delete-value: 1

添加ServiceAuth实体类

package com.young.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

@Data
@TableName(value = "m_service_auth")
public class ServiceAuth implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String providerService;
    private String consumerService;
    private LocalDateTime createTime;
    @TableLogic
    private Integer isDelete;
}

ServiceAuthMapper.java

package com.young.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.young.entity.ServiceAuth;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ServiceAuthMapper extends BaseMapper<ServiceAuth> {
}

ProviderConfigProperty.java

package com.young.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@Data
public class ProviderConfigProperty {
    @Value("${spring.application.name}")
    private String name;
}

ServiceAuthServiceImpl.java

package com.young.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.young.entity.ServiceAuth;
import com.young.mapper.ServiceAuthMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ServiceAuthServiceImpl {
    @Autowired
    private ServiceAuthMapper serviceAuthMapper;
    public Boolean canVisit(String providerName,String consumerName){
        LambdaQueryWrapper<ServiceAuth>queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(ServiceAuth::getProviderService,providerName)
                .eq(ServiceAuth::getConsumerService,consumerName);
        List<ServiceAuth> serviceAuths = serviceAuthMapper.selectList(queryWrapper);
        if (serviceAuths!=null&&serviceAuths.size()>0){
            return true;
        }
        return false;
    }
}

修改TokenAuthFilter.java,这里有个坑,在dubbo的拦截器中,采用@Autowired自动注入是无效的,可以采取setter方式来注入其他的bean,且不用标注注解,dubbo自己会对这些bean进行注入

package com.young.filter;

import com.young.config.ProviderConfigProperty;
import com.young.service.ServiceAuthService;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.config.ProviderConfig;
import org.apache.dubbo.rpc.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Activate
@Component
public class TokenAuthFilter implements Filter {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private ServiceAuthService serviceAuthService;

    private ProviderConfigProperty providerConfigProperty;

    public void setProviderConfigProperty(ProviderConfigProperty providerConfigProperty){
        this.providerConfigProperty=providerConfigProperty;
    }

    public void setServiceAuthService(ServiceAuthService serviceAuthService){
        this.serviceAuthService=serviceAuthService;
    }


    private static final String TOKEN_KEY="authorization";
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
//        String token = RpcContext.getServerAttachment().getAttachment(TOKEN_KEY);
//        String name = RpcContext.getServerAttachment().getAttachment("name");
//        System.out.println("token:"+token+",name:"+name);
//        if (token == null || !validateToken(token)) {
//            throw new RpcException("UnValid Token");
//        }
        String consumerName = RpcContext.getServerContext().getRemoteApplicationName();
        if (consumerName!=null){
            if (!serviceAuthService.canVisit(providerConfigProperty.getName(),consumerName)){
                throw new RpcException("UnValid Token");
            }
        }
        return invoker.invoke(invocation);
    }

    private boolean validateToken(String token) {
        String resToken = stringRedisTemplate.opsForValue().get(TOKEN_KEY);
        if (token.equals(resToken)) {
            return true;
        }
        return false;
    }
}


dubbo-provider的完整结构如下:
在这里插入图片描述
dubbo-consumer不需要修改,然后我们运行项目
在这里插入图片描述
修改dubbo-consumer的服务名,然后重新运行项目,访问
在这里插入图片描述
在这里插入图片描述
参考文章:
pringboot之dubbo实现过滤器-yellowcong
调用链路传递隐式参数

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值