Spring Cloud 入门 ---- Stream 消息驱动【随笔】

Spring Cloud 入门 ---- Stream 消息驱动
简介
为什么引入 cloud Stream

官网:https://docs.spring.io/spring-cloud-stream/docs/3.0.8.RELEASE/reference/html/

现在我们使用的消息中间件有很多,如:ActiveMQ、RabbitMQ、RocketMQ、Kafka等。比方说我们用到了RabbitMQ 和 Kafka,由于这两个消息中间件的架构上的不同,像 RabbitMQ 有 exchange,kafka 有 Topic 和 Partitions 分区。这些中间件的差异性在项目实际开发中会给我们造成了一定的困扰,我们如果用到了两个消息队列的其中一种,后面的业务需求变动,我们想往另一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西需要重新推倒重做;因为它跟我们的系统耦合了,这时候 Spring Cloud Stream 给我们提供了一种解耦合的方式。

一句话:Spring Cloud Stream 可以屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。

20201115181910136

什么是 Spring Cloud Stream

官方定义 Spring Cloud Stream 是一个构建消息驱动微服务的框架。

应用程序通过 inputs 或者 outputs 来与Spring Cloud Stream 中 binder 对象交互。通过我们配置来 binding(绑定),而 Spring Cloud Stream 的 binder 对象负责与消息中间件交互。所以,我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。

通过使用 Spring Integration 来连接消息代理中间件以实现消息事件驱动。Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了 发布-订阅、消费组、分区的三个核心概念。

目前仅支持 RabbitMQ Kafka。

Stream 设计思想

在没有绑定器这个概念的情况下,我们的 SpringBoot 应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性。通过定义 Binder(绑定器)作为中间层,完美的实现了应用程序与消息中间件细节之间的隔离。通过应用程序暴露统一的 Channel 通道,使得应用程序不需要再考虑各种不同的消息中间件实现。Stream 对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq 切换为 kafka),使得微服务开发的高度解耦,服务可以更多关注于自己的业务流程。

Stream 中的消息通信方式遵循了发布-订阅模式,Topic 主题进行广播,在 RabbitMQ 中就是 Exchange,在 Kafka 中就是 Topic。

Binder

INPUT 对应于消费者,OUTPUT 对应于生产者。

20201115182220752

概念与常用注解

20201115185702380

  • Binder :绑定,用于连接中间件,屏蔽差异
  • Channel:通道,是队列 Queue 的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过 Channel 对队列进行配置。
  • Source/Sink:简单的可以理解为参照对象是 Spring Cloud Stream 自身,从 Stream 发布消息就是输出,接收消息就是输入。

20201115185909656

Stream 消息驱动之生产者

我们使用 RabbitMQ作为演示的中间件,由于前面的章节已经介绍了 RabbitMQ 的安装,这里就不做赘述了。官网:https://docs.spring.io/spring-cloud-stream-binder-rabbit/docs/3.0.8.RELEASE/reference/html/spring-cloud-stream-binder-rabbit.html

创建消息生产者

导入 pom 依赖

<!--stream-rabbitMQ-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

添加 yml 配置

server:
  port: 8801

spring:
  application:
    name: stream-provider-service
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
  cloud:
    stream:
      binders:  #在此处配置要绑定的 rabbitmq的服务信息;
        defaultRabbit:  #表示定义的名称,用于与 binding 整合
          type: rabbit  #消息组件类型
      bindings:  #服务的整合处理
        output:  #这个名字是一个通道的名称
          destination: studyExchange  #表示要使用 Exchange 名称定义
          content-tpe: application/json  #设置消息类型,本次为json,文本则设置为 text/plain

eureka-connection:
  name: akieay
  password: 1qaz2wsx
eureka:
  client:
    #表示是否将自己注册进 Eureka Server服务 默认为true
    register-with-eureka: true
    #f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
      #      defaultZone: http://localhost:7001/eureka
      defaultZone: http://${eureka-connection.name}:${eureka-connection.password}@eureka7001.com:7001/eureka,http://${eureka-connection.name}:${eureka-connection.password}@eureka7002.com:7002/eureka
  instance:
    instance-id: stream-provider-8801
    ## 当调用getHostname获取实例的hostname时,返回ip而不是host名
    prefer-ip-address: true
    # Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
    lease-renewal-interval-in-seconds: 10
    # Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
    lease-expiration-duration-in-seconds: 30

主启动

@SpringBootApplication
@EnableEurekaClient
public class StreamProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(StreamProviderApplication.class, args);
    }
}

业务类

//@EnableBinding 定义消息的推送管道
@EnableBinding(Source.class)
public class MessageProviderImpl implements MessageProvider {

    /**
     * 消息发送管道
     */
    @Resource
    private MessageChannel output;

    @Override
    public void send(String message) {
        //构建消息
        Message<String> build = MessageBuilder.withPayload(message).build();
        //发送消息
        output.send(build);
    }
}
@RestController
@Slf4j
public class MessageProviderController {

    @Resource
    private MessageProvider messageProvider;

    @GetMapping(value = "/sendMessage")
    public String sendMessage() {
        String serial = UUID.randomUUID().toString();
        messageProvider.send(serial);
        log.info("******* send message: " + serial);
        return serial;
    }
}
启动测试

启动 RabbitMQ,启动服务,进入 Eureka 注册中心查看服务是否注册成功;

20201115210429531

访问:http://localhost:15672/#/exchanges 查看交换器列表,可以看到我们自定义的交换器 studyExchange

20201115210610709

多次访问:http://localhost:8801/sendMessage,点击 studyExchange 即可看到我们发送消息的折线图。

20201115210700863

Stream 消息驱动之消费者
创建消息消费者

导入 pom 依赖

<!--stream-rabbitMQ-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

添加 yml 配置文件

server:
  port: 8803

spring:
  application:
    name: stream-consumer-service
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
  cloud:
    stream:
      binders:  #在此处配置要绑定的 rabbitmq的服务信息;
        defaultRabbit:  #表示定义的名称,用于与 binding 整合
          type: rabbit  #消息组件类型
      bindings:  #服务的整合处理
        input:  #这个名字是一个通道的名称
          destination: studyExchange  #表示要使用 Exchange 名称定义
          content-type: application/json  #设置消息类型,本次为json,文本则设置为 text/plain

eureka-connection:
  name: akieay
  password: 1qaz2wsx
eureka:
  client:
    #表示是否将自己注册进 Eureka Server服务 默认为true
    register-with-eureka: true
    #f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
      #      defaultZone: http://localhost:7001/eureka
      defaultZone: http://${eureka-connection.name}:${eureka-connection.password}@eureka7001.com:7001/eureka,http://${eureka-connection.name}:${eureka-connection.password}@eureka7002.com:7002/eureka
  instance:
    instance-id: stream-consumer-8803
    ## 当调用getHostname获取实例的hostname时,返回ip而不是host名
    prefer-ip-address: true
    # Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
    lease-renewal-interval-in-seconds: 10
    # Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
    lease-expiration-duration-in-seconds: 30

主启动

@SpringBootApplication
@EnableEurekaClient
public class StreamConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(StreamConsumerApplication.class, args);
    }
}

业务类

//@EnableBinding 定义消息的接收管道
@Component
@Slf4j
@EnableBinding(Sink.class)
public class ReceiveMessageListener {

    @Value("${server.port}")
    private String serverPort;

    @StreamListener(Sink.INPUT)
    public void input(Message<String> message) {
        log.info("消息消费者----->接收到消息: " + message.getPayload() + "\t serverPort: " + serverPort);
    }
}
启动测试

启动服务,打开 Eureka 注册中心查看服务是否注册成功,打卡 rabbitmq 控制台 studyExchange 交换器,查看绑定列表。

20201115215126804

20201115215151417

多次访问:http://localhost:8801/sendMessage 发送消息,点击 studyExchange.anonymous.1xwWv_V5QKGkQpd5fjRPRg 进入队列 即可看到消息的折线图和统计信息。

20201115215344470

进入控制台,即可看到消息消费者接收打印出来的消息信息。

20201115215529458

至此,简单的消息发送与接收的演示完毕,在接收消息后,可根据实际情况添加自己的业务出来逻辑。

分组消费与持久化
消费者集群准备

修改 消费服务 的配置文件名称为 application-one.yml ,并复制一份为 application-two.yml 并修改端口号为 8804。

20201115222829726

server:
  port: 8804

eureka:
  client:
    instance-id: stream-consumer-8804

修改原来服务的启动配置,并 copy 该服务创建第二个服务节点。

20201115222922616

20201115223011627

20201115223045160

20201115223126499

启动消息消费者集群节点 8803 8804,注册中心查看是否注册成功。

20201115223303717

访问:http://localhost:8801/sendMessage ,查看控制台,可以看到同一份消息 8803 与 8804 都有消费,这就产生了 重复消费 的问题。

20201115223432894

20201115223502209

Stream 之消息重复消费

比如在如下场景中,订单系统我们做了集群部署,都会从 RabbitMQ 中获取订单信息,如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免出现这种情况。这时我们就可以使用Stream 中的消息分组来解决这个问题。

20201115224622042

注意:在 Stream 中处于同一个 group 中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。不同的组是可以全面消费的(重复消费),比如我们上面测试 8803 与 8804 服务产生的重复消费问题,这是由于我们没有指定分组时,系统默认会为每一个服务单独创建一个分组,如下图:

20201115225309997

Stream 分组解决重复消费问题

从上面的分析我们可以看出,导致重复消费的原因是由于默认分组 group 是不同的,所以我们可以通过自定义配置分组解决重复消费的问题。修改消费者配置文件,两个配置文件都添加分组,并且分组都为:studyGroup

spring:
  cloud:
    stream:
      bindings:  #服务的整合处理
        input:  #这个名字是一个通道的名称
          destination: studyExchange  #表示要使用 Exchange 名称定义
          content-type: application/json  #设置消息类型,本次为json,文本则设置为 text/plain
          group:  studyGroup  #设置分组

重启服务,多次访问:http://localhost:8801/sendMessage 并查看控制台,可以看到同一条消息将只会在 8803 或 8804 其中一个服务中消费一次,不再会出现重复消费的现象。

20201115230954925

20201115231049493

Stream 之消息持久化

通过上述,我们解决了重复消费的问题,再看看持久化。消息持久化:当我们的消息消费者服务在运行期间突然宕机时,重启消费者服务后,消费者会不会从 rabbitMQ 中读取我们在宕机期间发送的消息并消费。

运行演示

首先我们需要关闭消费者 8803 与 8804,并删除 8803 的分组配置,如下:

20201115231824938

8801 先发送4条消息到 rabbitmq,即访问四次:http://localhost:8801/sendMessage

20201115232006625

重启 消息消费者 8803 并查看控制台,从日志中我们可以看到 8803 重启后,并没有重 rabbitmq 中获取我们之前发送的消息。

20201115232202299

重启 消费者 8804 并查看控制台,从日志中我们可以看到 8804 重启后,从 rabbitmq 中获取到了我们之前发送的消息并消费。

20201115232340734

总结:由此我们可以看出,在创建服务消费者时是有必要指定 group 的,它不仅可以解决重复消费问题,还可以解决消息的持久化问题

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值