【SpringCloud笔记】(11)消息驱动之Stream

Stream

技术背景

底层不同模块可能使用不同的消息中间件,这就导致技术的切换,微服务的维护及开发变得麻烦起来

概述

官网:
https://spring.io/projects/spring-cloud-stream#overview
https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/3.0.1.RELEASE/reference/html/

Spring Cloud Stream中文指导手册:
https://m.wang1314.com/doc/webapp/topic/20971999.html

什么是SpringCloudStream

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

应用程序通过inputs 或者 outputsSpring Cloud Stream中binder对象交互

通过我们配置来binding(绑定),而Spring Cloud Stream的binder对象负责与消息中间件交互

所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。

通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。

Spring Ccloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。

目前仅支持RabbitMQ、Kafka

Stream设计思想

标准MQ流程

  • 生产者/消费者之间靠消息媒介传递信息内容(Message)

  • 消息必须走特定的通道(消息通道MessageChannel)

  • 消息通道里的消息如何被消费呢,谁负责收发处理(消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅)

为什么要引入Stream

比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic和Partitions分区

这些中间件的差异导致我们实际项目开发给我们造成了一定的困难,我们如果用了两个消息队列的其中一种,后面的业务需求,我们想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候SpringCloud Stream给我们提供了一种解耦合的方式。

在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性。

通过定义绑定器Binder作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离

Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程

Stream的消息通信方式遵循了发布-订阅模式

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

Stream标准流程套路

  • binder:很方便的连接中间件,屏蔽差异
  • Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过channel对队列进行配置
  • Source(生产)和sink(消费):简单地可理解为参照对象是spring cloud stream自身,从stream发布消息就是输出,接收消息就是输入

常用注解

组成 说明
Middleware 中间件,目前只支FRabbitMQ和Kafka
Binder Binder是应用与消息中间件之间的封装,目前实行了KafKa和RabbitMQ的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现
@Input 注解标识输入通道,通过该输入通接收到的消息息进入应用程序
@Output 注解标识输出通道,发布的消息将通过该通道离开应用程序
@StreamListener 监听队列,用于消费者的队列的消息接收
@EnableBinding 指信道channel和exchange绑定在一起

要新建3个子模块

cloud-stream-rabbitmq-consumer8802:作为消息接收模块

cloud-stream-rabbitmq-consumer8802:作为消息接收模块

消息驱动之生产者

新建cloud-stream-rabbitmq-provide8801:作为生产者进行发消息模块

pom文件

xml 复制代码
<artifactId>cloud-stream-rabbitmq-provide8801</artifactId>

  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
      <groupId>com.mzr.springcloud</groupId>
      <artifactId>cloud-api-commons</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>

yml文件

yaml 复制代码
server:
  port: 8801

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

eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    instance-id: send-8801.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址

主启动类

java 复制代码
@SpringBootApplication
public class StreamMQMain8801
{
    public static void main(String[] args)
    {
        SpringApplication.run(StreamMQMain8801.class,args);
    }
}

业务类

定义发送消息的接口

java 复制代码
public interface IMessageProvider
{
    public String send();
}

定义发送消息的接口

java 复制代码
//@Service 这个地方不再需要该注解,不再是和controller打交道的service,而是发送消息的推送服务类与Stream打交道,可结合Stream标准流程图来看
@EnableBinding(Source.class) //定义消息的推送管道
public class MessageProviderImpl implements IMessageProvider
{
    @Resource
    private MessageChannel output; // 消息发送管道

    @Override
    public String send()
    {
        String serial = UUID.randomUUID().toString();
        //将消息与绑定器绑定
        output.send(MessageBuilder.withPayload(serial).build());
        System.out.println("*****serial: "+serial);
        return null;
    }
}

controller

java 复制代码
@RestController
public class SendMessageController
{
    @Resource
    private IMessageProvider messageProvider;

    @GetMapping(value = "/sendMessage")
    public String sendMessage()
    {
        return messageProvider.send();
    }
}

启动rabbitMQ

这里的studyExchange便是yml文件配置的

启动eureka7001、生产者8801,多次访问localhost:8801/sendMessage

可以看到rabbitMQ中也有流量的起伏

消息驱动之消费者

新建模块 cloud-stream-rabbitmq-consumer8802

pom文件与8801一样

java 复制代码
省略..

yml文件

yaml 复制代码
server:
  port: 8802

spring:
  application:
    name: cloud-stream-consumer
  cloud:
      stream:
        binders: # 在此处配置要绑定的rabbitmq的服务信息;
          defaultRabbit: # 表示定义的名称,用于于binding整合
            type: rabbit # 消息组件类型
            environment: # 设置rabbitmq的相关的环境配置
              spring:
                rabbitmq:
                  host: localhost
                  port: 5672
                  username: guest
                  password: guest
        bindings: # 服务的整合处理
          #output: # 这个名字是一个通道的名称,这是8802与8801唯一的区别,一个是消息发送一个是消息接收
          input: # 这个名字是一个通道的名称
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置"text/plain"
          binder: defaultRabbit # 设置要绑定的消息服务的具体设置

eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    instance-id: receive-8802.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址

主启动类与8801一致

java 复制代码
省略..

业务类

因为这是消费者是接受消息,所以只需关注controller层业务逻辑

controller层

java 复制代码
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController
{
    @Value("${server.port}")
    private String serverPort;


    @StreamListener(Sink.INPUT)//监听输入源
    public void input(Message<String> message)
    {
        System.out.println("消费者1号,----->接受到的消息: "+message.getPayload()+"\t  port: "+serverPort);
    }
}

启动8802测试

可以看到这个通道已经被监听了

可以看到8801发送的流水号与8802接受到的流水号对应

分组消费及持久化

依照8802,clone出来一份cloud-stream-rabbitmq-consumer8802作为消费者并启动

运行后有两个问题

  • 有重复消费问题
  • 消息持久化

消息重复消费

8801发送的消息,8802及8803同时都收到了,存在着重复消费问题

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

注意在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次不同组是可以全面消费的(重复消费)

默认分组group是不同的,组流水号不一样,被认为不同组,可以消费,所以导致8002 8003同时收到相同的消息

解决消息重复消费

原理:微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。

实现步骤:

1、8802/8803都变成不同组,group两个不同

8802/8803配置yml文件

yaml 复制代码
	# 8802
	bindings:
        input:   
          destination: studyExchange  
          content-type: application/json  
          binder: defaultRabbit  
          group: atguiguA  # 自定义分组配置
          
    # 8803
	bindings:
        input:   
          destination: studyExchange  
          content-type: application/json  
          binder: defaultRabbit  
          group: atguiguB  # 自定义分组配置

可以看到RabbitMQ上已经变成了我们自定义的分组

此时8002 8003同时收到相同的消息

2、8802/8803都变成相同组,group两个相同

将8802/8803yml文件中group修改为atguiguA

访问2次localhost:8801/sendMessage触发生产者发送2条信息

消费者8802接收到1条

消费者8803接收到1条

8802/8803实现了轮询分组,每次只有一个消费者,8801模块的发的消息只能被8802或8803其中一个接收到,这样避免了重复消费。

消息持久化

做个小测试感受一下消息持久化的重要性

停止8802/8803并去除掉8802的分组group: atguiguA,此时8803的分组group: atguiguA并没有去掉

8801先发送4条消息到rabbitmq

重新启动8802,因为没有分组属性配置,后台没有打出来消息

重新启动8803,因为有分组属性配置,后台打出来了8801生产者发送的消息

group分组属性对于避免消息重复消费 及 消息持久化避免消息丢失是非常重要的

相关推荐
Exclusive_Cat10 分钟前
SpringMVC参数接收与数据返回详解
spring·mvc
ChinaRainbowSea2 小时前
补充:问题:CORS ,前后端访问跨域问题
java·spring boot·后端·spring
hqxstudying4 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
春生野草4 小时前
关于SpringMVC的整理
spring
Bug退退退1235 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
guojl6 小时前
Ribbon原理和源码分析
spring cloud·微服务
hello早上好6 小时前
CGLIB代理核心原理
java·spring
先睡12 小时前
Redis的缓存击穿和缓存雪崩
redis·spring·缓存
Bug退退退12317 小时前
RabbitMQ 高级特性之死信队列
java·分布式·spring·rabbitmq
guojl1 天前
RestTemplate使用手册
spring cloud·微服务