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

news/2024/10/18 7:45:07/

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
BinderBinder是应用与消息中间件之间的封装,目前实行了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文件

<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文件

server:port: 8801spring:application:name: cloud-stream-providercloud:stream:binders: # 在此处配置要绑定的rabbitmq的服务信息;defaultRabbit: # 表示定义的名称,用于于binding整合type: rabbit # 消息组件类型environment: # 设置rabbitmq的相关的环境配置spring:rabbitmq:host: localhostport: 5672username: guestpassword: guestbindings: # 服务的整合处理output: # 这个名字是一个通道的名称destination: studyExchange # 表示要使用的Exchange名称定义content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”binder: defaultRabbit # 设置要绑定的消息服务的具体设置eureka:client: # 客户端进行Eureka注册的配置service-url:defaultZone: http://localhost:7001/eurekainstance: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地址

主启动类

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

业务类

定义发送消息的接口

public interface IMessageProvider
{public String send();
}

定义发送消息的接口

//@Service 这个地方不再需要该注解,不再是和controller打交道的service,而是发送消息的推送服务类与Stream打交道,可结合Stream标准流程图来看
@EnableBinding(Source.class) //定义消息的推送管道
public class MessageProviderImpl implements IMessageProvider
{@Resourceprivate MessageChannel output; // 消息发送管道@Overridepublic String send(){String serial = UUID.randomUUID().toString();//将消息与绑定器绑定output.send(MessageBuilder.withPayload(serial).build());System.out.println("*****serial: "+serial);return null;}
}

controller

@RestController
public class SendMessageController
{@Resourceprivate 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一样

省略..

yml文件

server:port: 8802spring:application:name: cloud-stream-consumercloud:stream:binders: # 在此处配置要绑定的rabbitmq的服务信息;defaultRabbit: # 表示定义的名称,用于于binding整合type: rabbit # 消息组件类型environment: # 设置rabbitmq的相关的环境配置spring:rabbitmq:host: localhostport: 5672username: guestpassword: guestbindings: # 服务的整合处理#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/eurekainstance: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一致

省略..

业务类

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

controller层

@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文件

	# 8802bindings:input:   destination: studyExchange  content-type: application/json  binder: defaultRabbit  group: atguiguA  # 自定义分组配置# 8803bindings: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分组属性对于避免消息重复消费 及 消息持久化避免消息丢失是非常重要的


http://www.ppmy.cn/news/1283198.html

相关文章

Hive-数据模型详解(超详细)

文章目录 一、Hive数据模型1. 概述2. 数据库和表(1) 创建数据库(2) 使用数据库(3) 创建表格(4) 查看表结构 3. 分区与桶(1) 分区(2) 桶 4. 数据加载与查询(1) 数据导入(2) 查询语句 5. 总结 一、Hive数据模型 1. 概述 Hive是基于Hadoop的数据仓库工具&#xff0c;它提供了类似…

git远程操作,推送【push】,拉取【pull】,忽略特殊文件,配置别名,标签管理

文章目录 前言&#xff1a;新建远程仓库克隆推送【push】拉取【pull】 配置git忽略特殊文件给命令配置别名 标签管理理解标签创建标签操作标签 前言&#xff1a; 大家如果没有看过前几章git的基础操作的话&#xff0c;推荐先看一下&#xff0c;看完再来看这个远程操作&#xf…

声明 | 为打击假冒账号、恶意抄袭账号等诈骗活动,提升本账号权威,本博主特此郑重声明

声明 | 为打击假冒账号、恶意抄袭账号诈骗活动&#xff0c;提升本账号权威&#xff0c;本博主特此郑重声明 一、本账号为《机器学习之心》博主CSDN唯一官方账号&#xff0c;唯一联系方式见文章底部。 二、《机器学习之心》博主未授权任何第三方账号进行模型合作、程序设计、源…

【数据库系统概论】第3章-关系数据库标准语言SQL(3)

文章目录 3.5 数据更新3.5.1 插入数据3.5.2 修改数据3.5.3 删除数据 3.6 空值的处理3.7 视图3.7.1 建立视图3.7.2 查询视图3.7.3 更新视图3.7.4 视图的作用 3.5 数据更新 3.5.1 插入数据 注意&#xff1a;插入数据时要满足表或者列的约束条件&#xff0c;否则插入失败&#x…

安全运维是做什么的,主要工作内容是什么

安全运维&#xff0c;简称SecOps&#xff0c;是一种集成安全措施和流程到信息技术运维的实践。它的目的是确保在日常运维活动中&#xff0c;如网络管理、系统维护、软件更新等&#xff0c;均考虑并融入安全策略。安全运维的核心是实现安全和运维团队的密切协作&#xff0c;以快…

【并发设计模式】聊聊 基于Copy-on-Write模式下的CopyOnWriteArrayList

在并发编程领域&#xff0c;其实除了使用上一篇中的属性不可变。还有一种方式那就是针对读多写少的场景下。我们可以读不加锁&#xff0c;只针对于写操作进行加锁。本质上就是读写复制。读的直接读取&#xff0c;写的使用写一份数据的拷贝数据&#xff0c;然后进行写入。在将新…

Dash中的callback的使用 多input 6

代码说明 import plotly.express as pxmport plotly.express as px用于导入plotly.express模块并给它起一个别名px。这样在后续的代码中&#xff0c;你可以使用px来代替plotly.express&#xff0c;使代码更加简洁。 plotly.express是Plotly的一个子模块&#xff0c;用于快速创…

Unity 代码控制Text自适应文本高度

在使用代码给Text赋值时&#xff0c;且文本有多段&#xff0c;并需要根据实际文本高度适配Text组件的高度时&#xff0c;可以使用以下方法&#xff1a; //Text文本 public TextMeshProUGUI text;void Start() {//代码赋值文本text.text "好!\n很好!\n非常好!";//获…