SpringCloud之Stream千里传音

1,952 阅读7分钟

SpringCloud之Stream千里传音

一、简介

  • Spring Cloud Stream 是消息中间件组件,它集成了 kafka 和 rabbitmq 。本篇文章以 Rabbit MQ 为消息中间件系统为基础,介绍 Spring Cloud Stream 的使用。如果你没有用过消息中间件,rabbitMq

  • Spring Cloud Stream重要概念

    1. Destination Binders:目标绑定器,目标指的是 kafka 还是 RabbitMQ,绑定器就是封装了目标中间件的包。如果操作的是 kafka 就使用 kafka binder ,如果操作的是 RabbitMQ 就使用 rabbitmq binder。
    2. Destination Bindings:外部消息传递系统和应用程序之间的桥梁,提供消息的“生产者”和“消费者”(由目标绑定器创建)
    3. Message:一种规范化的数据结构,生产者和消费者基于这个数据结构通过外部消息系统与目标绑定器和其他应用程序通信。

    img

二、发布-订阅

  • pom配置

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream</artifactId>
    </dependency><dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    
  • 配置文件

    server:
      port: 8130
    
    spring:
      application:
        name: bike-stream
      rabbitmq:
        host: localhost
        port: 5672
        username: username
        password: password
      cloud:
        stream:
          binders:
            #自定义命名
            defaultRabbit:
              #指定类型,如果是kafka就写kafka
              type: rabbit
          bindings:
    #        通道名称,可指定多个通道名称,与配置中要一一对应
            input_one:
    #          相当于交换机exchange的名称,默认是topic交换机,输入和输出配置要一样
              destination: bike.stream.one
              binder: defaultRabbit
              contentType: application/json
            output_one:
              destination: bike.stream.one
              binder: defaultRabbit
              contentType: application/json
    
    
    1. spring.cloud.stream.binders,上面提到了 stream 的 3 个重要概念的第一个 「Destination binders」。上面的配置文件中就配置了一个 binder,命名为 defaultRabbit,指定 type 为 rabbit ,表示使用的是 rabbitmq 消息中间件,如果用的是 kafka ,则 type 设置为 kafka。还可以设置environment 就是设置使用的消息中间件的配置信息,包括 host、port、用户名、密码等。可以设置多了个 binder,适配不同的场景。

    2. spring.cloud.stream.bindings ,对应上面提到到 「Destination Bindings」。这里面可以配置多个 input 或者 output,分别表示消息的接收通道和发送通道,对应到 rabbitmq 上就是不同的 exchange。

    3. 每个通道下的 destination 属性指 exchange 的名称,binder 指定在 binders 里设置的 binder,上面配置中指定了 defaultRabbit。可以看到 input、output 对应的 destination 是相同的, 也就是对应相同的 exchange。一个表示消息来源,一个表示消息去向。另外还可以设置 group 。因为服务很可能不止一个实例,如果启动多个实例,那么没必要每个实例都消费同一个消息,只要把功能相同的实例的 group 设置为同一个,那么就会只有一个实例来消费消息,避免重复消费的情况。如果设置了 group,那么 group 名称就会成为 queue 的名称,如果没有设置 group ,那么 queue 就会根据 destination + 随机字符串的方式命名。

    4. stream 内置的简单消息通道(消息通道也就是指消息的来源和去向)接口定义,一个 Source 和 一个 Sink

      //消息接收通道定义,定义了一个 SubscribableChannel 类型的 input() 方法,表示订阅一个消息的方法,并用 @Input 注解标
      //识,并且指定了 binging 的名称为 input 。
      public interface Sink {
          String INPUT = "input";
      ​
          @Input("input")
          SubscribableChannel input();
      }
      
      //消息发送通道定义,定义了一个 MessageChannel 类型的 output() 方法,用 @Output 注解标示,并指定了 binding 的名称为 //output。
      public interface Source {
          String OUTPUT = "output";
      ​
          @Output("output")
          MessageChannel output();
      }
      
      //Processor继承了上面两个接口,在绑定通道时可指定该接口类
      public interface Processor extends Source, Sink {
      }
      
    5. 通常我们需要自定义一个消息通道来满足自身复杂的业务需求

      public interface MessageProcess {
          //命名规则要与配置文件中的通道名称保持一致
          String INPUT_ONE="input_one";
      ​
          @Input(MessageProcess.INPUT_ONE)
          MessageChannel inputOne();
          //命名规则要与配置文件中的通道名称保持一致
          String OUTPUT_ONE = "output_one";
      ​
          @Output(MessageProcess.OUTPUT_ONE)
          MessageChannel outputOne();
      ​
      }
      
  • 模拟测试

    1. 启动项目,查看mq控制台信息,发现了我们自定义的exchange,且默认类型为topic

    image-20210831170136240.png

    1. 我们通过测试类模拟生产者发送消息

      @SpringBootTest
      class BikeStreamApplicationTests {
      ​
          @Autowired
          MessageProcess messageProcess;
      ​
          //发送消息
          @Test
          void contextLoads() {
              messageProcess.outputOne().send(MessageBuilder.withPayload("hello spring cloud stream 4 ...").build());
          }
      ​
          @Test
          void partitionLoad() {
              messageProcess.outputOne().send(MessageBuilder.withPayload("hello spring cloud stream 4 ...").setHeader("partitionkey",1).build());
          }
      ​
      }
      
    2. 编写消费者接受mq消息

      @Service
      @Slf4j
      //@EnableBinding 注解用来指定一个或多个定义了 @Input 或 @Output 注解的接口,以此实现对消息通道(Channel)的绑定
      @EnableBinding(value = {MessageProcess.class})
      public class ConsumerService {
      ​
          /**
           * 主要定义在方法上,作用是将被修饰的方法注册为消息中间件上数据流的事件监听器,注解中的属性值对应了监听的消息通道名
           * @param message
           */
          @StreamListener(MessageProcess.INPUT_ONE)
          public void consumer(String message){
              System.out.printf(message);
          }
      }
      
    3. 执行测试类,看到控制台打印出信息即可,也可观察mq控制台信息queue会根据 destination + 随机字符串的方式命名。

      hello spring cloud stream 4 ...

三、消息分组

  • 需求痛点

    Spring Cloud Stream 中的消息通信方式遵循了发布-订阅模式,当一条消息被投递到消息中间件后,它会通过共享的 Topic 主题进行广播,在微服务架构中,一般会有多节点部署来做负载均衡,同样的消息我们只需要一个节点消费即可,否则会出现重复消费的情况,所以stream为我们提供了分组的功能来解决这个问题

  • 如何使用

    1. 创建两个消费端,配置两个不同端口号来模拟集群配置

    2. 配置文件修改

      server:
        port: 8130
      
      spring:
        application:
          name: bike-stream
        rabbitmq:
          host: localhost
          port: 5672
          username: username
          password: password
        cloud:
          stream:
            binders:
              #自定义命名
              defaultRabbit:
                #指定类型,如果是kafka就写kafka
                type: rabbit
            bindings:
      #        通道名称,可指定多个通道名称,与配置中要一一对应
              input_one:
      #          相当于交换机exchange的名称,默认是topic交换机,输入和输出配置要一样
                destination: bike.stream.one
                binder: defaultRabbit
                contentType: application/json
      #         在应用集群分布中,分组只会产生一个队列,轮训消费消息,队列名称为 destination名字+group名字
                group: bike.group
              output_one:
                destination: bike.stream.one
                binder: defaultRabbit
                contentType: application/json
      

      配置group参数

    3. 分别启动两个消费端,观察mq控制台,出现了queue名称 destination名字+group名字消息队列

    image-20210831172102119.png

    1. 通过上一步的测试类,循环发送消息,观察两个消费端的日志打印,我们发现两个消费端会轮训的处理消息,这个时候我们就完美的解决了消息重复消费的问题

四、消息分区

  • 需求痛点

    消息分组解决了我们在负载均衡条件下消息重复消费的问题,但是对于更特殊的业务情况需要保证单一实例消费的情况,比如某一种类型的订单我们只需要实例1单独消费的情况,这个时候我们就需要使用分区功能了。

  • 如何使用

    1. 根据上一步我们创建了两个消费端,在不分区的情况下其实默认是轮训消费的,我们现在使用分区来指定一个实例去消费消息

    2. 配置文件修改

      server:
        port: 8130
      
      spring:
        application:
          name: bike-stream
        rabbitmq:
          host: localhost
          port: 5672
          username: username
          password: password
        cloud:
          stream:
            binders:
              #自定义命名
              defaultRabbit:
                #指定类型,如果是kafka就写kafka
                type: rabbit
            bindings:
      #        通道名称,可指定多个通道名称,与配置中要一一对应
              input_one:
      #          相当于交换机exchange的名称,默认是topic交换机,输入和输出配置要一样
                destination: bike.stream.one
                binder: defaultRabbit
                contentType: application/json
      #         在应用集群分布中,分组只会产生一个队列,轮训消费消息,队列名称为 destination名字+group名字
                group: bike.group
                consumer:
      #           通过该参数开启消费者分区功能
                  partitioned: true
              output_one:
                destination: bike.stream.one
                binder: defaultRabbit
                contentType: application/json
                producer:
      #           分区表达式,headers['partitionKey']这个是由MessageBuilder类的setHeader()方法完成赋值,partitionKey为header的key,value为instance-index索引值
                  partitionKeyExpression: headers['partitionkey']
      #           指定参与消息分区的节点为2个
                  partitionCount: 2
      #     表示消费分区节点数量为2
            instance-count: 2
      #     设置消费端实例的索引,从0开始
            instance-index: 0
      

      我们根据上述配置,来开启消费端分区功能,指定消费端的节点数量,并给当前消费端一个索引。在输出端我们需要配置producer.partitionKeyExpression和producer.partitionCount两个参数

    3. 发送端修改

      @SpringBootTest
      class BikeStreamApplicationTests {
      
          @Autowired
          MessageProcess messageProcess;
      
          @Test
          void contextLoads() {
              messageProcess.outputOne().send(MessageBuilder.withPayload("hello spring cloud stream 4 ...").build());
          }
      //模拟分区发送mq消息,通过指定producer.partitionKeyExpression配置的key来设置header消费端实例的索引值
          @Test
          void partitionLoad() {
              messageProcess.outputOne().send(MessageBuilder.withPayload("hello spring cloud stream 4 ...").setHeader("partitionkey",1).build());
          }
      
      }
      
    4. 我们循环发送消息,观察两个消费端的控制台发现,只有实例1消费了消息,另一个实例并无日志打印

源码地址:源码