18、SpringCloud实战:Stream流

目录

1、 为什么要引入SpringCloudStream;

2、 SpringCloudStream简介;

2、 1、标准MQ架构图;

2、 2、SpringCloudStream架构图;

2、 3、SpringCloudStream处理流程;

3、 如何使用SpringCloudStream;

3、 1、创建springcloud-stream-sender项目(消息生产者);

3、 1.1、在pom文件中引入依赖;

3、 1.2、添加application.yml并配置;

3、 1.3、创建启动类;

3、 1.4、创建Service服务,发送消息到MQ;

3、 1.5、创建Controller对外提供接口服务;

3、 2、创建springcloud-stream-consumer项目(消息消费者);

3、 2.1、在pom文件中引入依赖;

3、 2.2、添加application.yml并配置;

3、 2.3、创建启动类;

3、 2.4、编写消息接收类,监听消费消息;

3、 3、启动RabbitMQ服务器,访问管理页面http://localhost:15672/;

3、 4、启动springcloud-stream-sender消息生产者和springcloud-stream-consumer消息消费者服务;

3、 5、使用IDEAJ自带的httpclient发送一个post请求到消息生产者;

4、 SpringCloudStream集群消费;

5、 切换消息中间件(RabbitMQ--->Kafka);

5、 1、修改springcloud-stream-sender项目;

5、 2、修改springcloud-stream-consumer项目;

5、 3、启动Zookeeper、Kafka(服务器)、send(消息生产者)、consumer(消息消费者);

6、 自定义通道名称;

6、 1、自定义输出通道(发布消息);

6、 1.1、创建接口MyOutput;

6、 1.2、修改消息发送服务注解@EnableBinding;

6、 1.3、修改配置application.yml文件;

6、 2、自定义输入通道(接收消息);

6、 2.1、创建接口;

6、 2.2、修改消息接收监听服务注解@EnableBinding;

6、 2.3、修改配置application.yml文件;


1、为什么要引入SpringCloud Stream

在实际的企业开发中,消息中间件是至关重要的组件之一。主要解决服务解耦、异步消费、流量削峰等问题。实现高性能、高可用、高扩展和最终一致性架构。不同消息中间件的实现方式和内部结构不一致,如常见的RabbitMQ和Kafka,由于这两个消息中间件的架构不同,像RabbitMQ有exchange,Kafka有topic和partition分区,这些中间件的差异给我们项目开发造成了一定的困扰,如果选择了其中一种消息中间件,由于中间件的差异性,一旦业务需要迁移部分消息队列到其他的消息中间件,这将是一件灾难性的工作,很多工作需要推到重新做,因为消息中间件和项目耦合了,这时候SpringCloud Stream给我们带来了福音,它给我们提供了一种解耦方式,我们不需要关注具体使用的是什么MQ,它会自动的给我们在各种MQ内切换。

引入SpringCloud Stream的目的:屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型(解耦)

2、SpringCloud Stream简介

SpringCloud Stream是一个用来为微服务应用构建消息驱动能力的框架,它可以基于SpringBoot来创建独立的、可用于生产的Spring应用程序。同时为一些供应商的消息中间件产品提供了个性化的自动化配置实现,并引入了发布-订阅、消费组、分区这三个核心概念。

通过使用 SpringCloud Stream可以有效简化开发人员对消息中间件的使用复杂度,让系统开发人员可以有更多的精力关注于核心业务的逻辑实现。但是目前SpringCloud Stream只支持RabbitMQ和Kafka的自动化配置。

SpringCloud Stream是一个构建消息驱动微服务的框架。应用程序通过inputs或者outputs来与SpringCloud Stream中的binder对象交互。

2.1、标准MQ架构图

*

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

*消息必须走特定的消息通道MessageChannel

*消息通道里的消息,消费和收发都是靠消息通道的子接口SubscribableChannel,由MessageHandler消息处理器所订阅

2.2、SpringCloud Stream架构图

*

Middleware:中间件,目前只支持RabbitMQ和Kafka

Binder:是应用与消息中间件之间的封装,完美地实现了应用程序与消息中间件细节之间的隔离,目前实现了Kafka和RabbitMQ的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应于Kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现

@Input:注解标识输入通道,通过该输入通道接收的消息进入应用程序

@Output:注解标识输出通道,发布的消息将通过该通道离开应用程序

@StreamListener:监听队列,用于消费者的队列的消息接收

@EnableBinding:指信道channel和exchange绑定在一起

2.3、SpringCloud Stream处理流程

*

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

Source:org.springframework.cloud.stream.messaging.Source

Sink:org.springframework.cloud.stream.messaging.Sink

3、如何使用SpringCloud Stream

3.1、创建springcloud-stream-sender项目(消息生产者)

3.1.1、在pom文件中引入依赖

<!--引入web模块-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入SpringCloud Stream依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

3.1.2、添加application.yml并配置

server:
  port: 9061
spring:
  application:
    name: stream-sender
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
  cloud:
    stream:
      bindings: #服务的整合处理
        output: #通道名称,根据@EnableBinding配置的类里的属性OUTPUT的值来配
          destination: TestExchange #Exchange名称,消息目的地
          context-type: application/json #设置消息的类型
          binder: rabbit #设置要绑定的消息服务类型

3.1.3、创建启动类

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

3.1.4、创建Service服务,发送消息到MQ

注意包不要引入错了

import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;

//属性为org.springframework.cloud.stream.messaging下的Source类
@EnableBinding(Source.class)
public class SendService {

    @Resource
    MessageChannel output;

    @PostMapping("/send")
    public Boolean sendMsg(String msg){
        //MessageBuilder是org.springframework.messaging.support下,注意不要引错包
        boolean result = this.output.send(MessageBuilder.withPayload(msg).build());
        return result;
    }
}

@EnableBinding(Source.class)

**

3.1.5、创建Controller对外提供接口服务

@RestController
public class SendController {

    @Resource
    private SendService sendService;

    @PostMapping("/send")
    public String send(){
        String msg="测试信息";
        Boolean result = this.sendService.sendMsg(msg);
        System.out.println("send msg status:  "+result);
        return "发送成功";
    }
}

3.2、创建springcloud-stream-consumer项目(消息消费者)

3.2.1、在pom文件中引入依赖

<!--引入web模块-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入SpringCloud Stream依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

3.2.2、添加application.yml并配置

server:
  port: 9062
spring:
  application:
    name: stream-consumer
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
  cloud:
    stream:
      bindings: #服务的整合处理
        input: #通道名称
          destination: TestExchange #Exchange名称
          context-type: application/json #设置消息的类型
          binder: rabbit #设置要绑定的消息服务类型

Springcloud stream配置基本和生产者sender端一样,只是把bindings下的output改成input

3.2.3、创建启动类

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

3.2.4、编写消息接收类,监听消费消息

import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;

@EnableBinding(Sink.class)
public class MsgListener {

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

    @StreamListener(Sink.INPUT)
    public void listener(Message<String> msg){
        System.out.println("消费端接受的消息是:  "+msg+"------port: "+port);
    }
}

3.3、启动RabbitMQ服务器,访问管理页面http://localhost:15672/

管理页面默认端口号为15672

*

3.4、 启动springcloud-stream-sender消息生产者和springcloud-stream-consumer消息消费者服务

springcloud-stream-sender:http://localhost:9061

springcloud-stream-consumer:http://localhost:9062

重新访问RabbitMQ的管理界面,可以看到多了一个Exchange,就是我们在配置文件里配置的Exchange名称

*

3.5、使用IDEAJ自带的http client发送一个post请求到消息生产者

http://localhost:9061/send

*

可以看到返回发送成功的提示

查看消息生产者的后台日志

*

查看消息消费者的后台日志

*

可以看到消息的内容是GenericMessage

消费端接受的消息是:

 GenericMessage[
payload=测试信息, 
headers={
amqp_receivedDeliveryMode=PERSISTENT, amqp_receivedExchange=TestExchange, 
amqp_deliveryTag=1,
 deliveryAttempt=1, amqp_consumerQueue=TestExchange.anonymous.gXVX6rHVSQWlXf7SasUwvQ, amqp_redelivered=false, 
amqp_receivedRoutingKey=TestExchange, 
amqp_timestamp=Fri Apr 28 12:36:31 CST 2023, amqp_messageId=fadf505d-92e9-e54c-c86b-62d577e4706b, id=fe3521c0-eeb7-c85b-c6be-43ce6c23bd92, amqp_consumerTag=amq.ctag-R7q-QWuU7PNqfmYOU5y9oQ, 
sourceData=(Body:'测试信息' MessageProperties [headers={}, timestamp=Fri Apr 28 12:36:31 CST 2023, messageId=fadf505d-92e9-e54c-c86b-62d577e4706b, contentType=application/json, 
contentLength=0, 
receivedDeliveryMode=PERSISTENT, 
priority=0, 
redelivered=false, 
receivedExchange=TestExchange, 
receivedRoutingKey=TestExchange,
deliveryTag=1, 
consumerTag=amq.ctag-R7q-QWuU7PNqfmYOU5y9oQ, consumerQueue=TestExchange.anonymous.gXVX6rHVSQWlXf7SasUwvQ]), contentType=application/json,
timestamp=1682656592010}]------port: 9062

4、SpringCloud Stream集群消费

4、 1、修改springcloud-stream-consumer的配置文件,将端口号改为9063,运行springcloud-stream-consumer的启动类启动服务;

4、 2、这样就形成了一个集群;

*

4、 3、再次请求http://localhost:9061/send发送消息;

4、 4、查看消费者端的控制台打印信息;

*

可以看到消费者A、消费者B都打印了消息,即造成了消息重复消费的问题

4、 5、如果消息生产者发送消息过程中,生产者发送消息成功,但是消费者A、消费者B在消息发送过程中由于某些原因宕机了,但是重启后没有消费消息,这导致了消息丢失;

4、 6、解决消息重复消费和消息丢失的问题,只要在消息消费端配置消费者组就可以解决这个问题;

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
  cloud:
    stream:
      bindings: #服务的整合处理
        input: #通道名称
          destination: TestExchange #Exchange名称
          context-type: application/json #设置消息的类型
          binder: rabbit #设置要绑定的消息服务类型
          group: cousumer_group #配置消费者组解决消息重复消费和丢失问题

4、 7、使用http://localhost:9061/send发送6条消息;

*

4、 8、查看消费者A、消费者B的控制台;

*

*

可以看到消费者A、消费者B各消费了3条消息,解决了重复消费

4、 9、关掉消费者A、消费者B服务器后,使用http://localhost:9061/send发送3条消息,重新启动消费者A、消费者B服务器,查看消费者A、消费者B的控制台;

*

*

消费者A消费了三条消息

消费者B没有消费消息

这样就解决了消息丢失、消息重复消费的问题

关掉消费者A、消费者B服务器后,使用http://localhost:9061/send发送3条消息后查看RabbitMQ的管理页面,可以看到有3条消息待消费

*

重启重新启动消费者A、消费者B服务器后刷新界面,消息被消费者消费了

*

5、切换消息中间件(RabbitMQ--->Kafka)

5.1、修改springcloud-stream-sender项目

5、 1.1、在pom文件中引入Kafka依赖;

<!--引入kafka依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>

5、 1.2、改造springcloud-stream-sender项目配置文件结构;

主配置文件application.yml配置如下

server:
  port: 9061
spring:
  application:
    name: stream-sender
  profiles:
    active: kafka   #激活kafka配置文件
#    active: rabbit

application-rabbit.yml配置如下

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
  cloud:
    stream:
      bindings: #服务的整合处理
        output: #通道名称,根据@EnableBinding配置的类里的属性OUTPUT的值来配
          destination: TestExchange #Exchange名称,消息目的地
          context-type: application/json #设置消息的类型
          binder: rabbit #设置要绑定的消息服务类型

application-kafka.yml配置如下

spring:
  cloud:
    stream:
      kafka:  #kafka的连接地址
        binder:
          brokers: localhost:9092
      bindings: #服务的整合处理
        output:
          destination: TestTopic #Exchange名称
          context-type: application/json #设置消息的类型
          binder: kafka #设置要绑定的消息服务类型

5.2、修改springcloud-stream-consumer项目

5、 2.1、在pom文件中引入Kafka依赖;

<!--引入kafka依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>

5、 2.2、改造springcloud-stream-consumer项目配置文件结构;

主配置文件application.yml配置如下

server:
  port: 9062
spring:
  application:
    name: stream-consumer
  profiles:
    active: kafka  #激活kafka配置文件
#    active: rabbit

application-rabbit.yml配置如下

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
  cloud:
    stream:
      bindings: #服务的整合处理
        input: #通道名称
          destination: TestExchange #Exchange名称
          context-type: application/json #设置消息的类型
          binder: rabbit #设置要绑定的消息服务类型
          group: cousumer_group #配置消费者组解决消息重复消费和丢失问题

application-kafka.yml配置如下

spring:
  cloud:
    stream:
      kafka:  #kafka的连接地址
        binder:
          brokers: localhost:9092
      bindings: #服务的整合处理
        input:
          destination: TestTopic #Exchange名称
          context-type: application/json #设置消息的类型
          binder: kafka #设置要绑定的消息服务类型
          group: consumer_group #配置消费者组解决消息重复消费和丢失问题

5.3、启动Zookeeper、Kafka(服务器)、send(消息生产者)、consumer(消息消费者)

本地启动Zookeeper、Kafka时需要修改的配置文件可百度查询

6、自定义通道名称

参照Source与Sink的格式自定义输入输出通道名称

Source:org.springframework.cloud.stream.messaging.Source

Sink:org.springframework.cloud.stream.messaging.Sink

6.1、自定义输出通道(发布消息)

6.1.1、创建接口MyOutput

public interface MyOutput {
    String OUTPUT = "MyOutput";

    @Output("MyOutput")
    MessageChannel output();
}

*

6.1.2、修改消息发送服务注解@EnableBinding

//属性为org.springframework.cloud.stream.messaging下的Source类
//@EnableBinding(Source.class)
@EnableBinding(MyOutput.class)
public class SendService {

    @Resource
    MessageChannel MyOutput;

    @PostMapping("/send")
    public Boolean sendMsg(String msg){
        //MessageBuilder是org.springframework.messaging.support下,注意不要引错包
        boolean result = this.MyOutput
.send(MessageBuilder.withPayload(msg).build());
        return result;
    }
}

6.1.3、修改配置application.yml文件

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
  cloud:
    stream:
      bindings: #服务的整合处理
        output: #通道名称,根据@EnableBinding配置的类里的属性OUTPUT的值来配
          destination: TestExchange #Exchange名称,消息目的地
          context-type: application/json #设置消息的类型
          binder: rabbit #设置要绑定的消息服务类型
        MyOutput: #通道名称,根据@EnableBinding配置的类里的属性OUTPUT的值来配
          destination: MyOutputExchange #Exchange名称,消息目的地
          context-type: application/json #设置消息的类型
          binder: rabbit #设置要绑定的消息服务类型

*

可以定义多个通道,每个通道可以配置自己独立的消息中间件(可以rabbit,也可以kafka)

6.2、自定义输入通道(接收消息)

6.2.1、创建接口

public interface MyInput {
    String INPUT = "MyInput";

    @Input("MyInput")
    SubscribableChannel input();
}

*

6.2.2、修改消息接收监听服务注解@EnableBinding

//@EnableBinding(Sink.class)
@EnableBinding(MyInput.class)
public class MsgListener {

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

    @StreamListener(MyInput.INPUT)
    public void listener(Message<String> msg){
        System.out.println("消费端接受的消息是:  "+msg+"------port: "+port);
    }
}

*

6.2.3、修改配置application.yml文件

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
  cloud:
    stream:
      bindings: #服务的整合处理
        input: #通道名称
          destination: TestExchange #Exchange名称
          context-type: application/json #设置消息的类型
          binder: rabbit #设置要绑定的消息服务类型
          group: cousumer_group #配置消费者组解决消息重复消费和丢失问题
        MyInput: #通道名称
          destination: MyOutputExchange #Exchange名称,与对应的输出绑定同一个Exchange
          context-type: application/json #设置消息的类型
          binder: rabbit #设置要绑定的消息服务类型
          group: myoutput_group #配置消费者组解决消息重复消费和丢失问题

*

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: