SpringCloud Stream概念
一、为什么要引入SpringCloud Stream? 面临什么问题?
简单的,我们可以系统分为三部分:
前端 》》》 后端 》》》 大数据平台
。当后端系统产生大量数据,而大数据平台需要通过这些数据进行分析时(例如:抖音视频推荐、广告推荐等)。这些数据需要通过消息中间件
(ActiveMQ
、RabbitMQ
、RocketMQ
、Kafka
)推送到大数据平台,当推送方和订阅方采用不同消息中间件时,就会面临中间件技术 切换、维护、开发等等复杂的工作。那么有没有一种新的技术能够让我们不再关注具体的MQ的细节,我们只需要用一种适配绑定的方式,自动的给我们在各种MQ中进行切换。SpringCloud Stream
则能够通过屏蔽各种MQ的底层差异而达到对MQ的统一管理,只需要通过操作SpringCloud Stream就能操作各种MQ,降低切换、维护、开发工作(一句话:屏蔽底层消息中间件的差异,降低切换维护成本,统一消息的编程模型
)。
二、什么是SpringCloud Stream?
官网定义:
SpringCloud Stream
是一个构建消息驱动
微服务的框架。
应用程序通过inputs
或者outputs
来与 SpringCloud Stream 中的binder(绑定器)
对象交互。通过我们配置来binding
(绑定),而SpringCloud Stream 的 binder 对象负责与消息中间件交互。所以我们只需要搞清楚如何与 SpringCloud Stream 交互就可以方便的使用消息驱动方式。通过使用 Spring Integration 来连接消息代理中间件以实现消息事件驱动。SpringCloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅
、消费组
、分区
的三个核心概念。目前仅支持:RabbitMQ、Kafka
。
三、设计思想
1、标准MQ
(1)生产者/消费者
之间靠消息媒介传递信息:Message
。
(2)消息必须走特定的通道
:消息通道MessageChannel
。
(3)消息通道里的消息如何被消费呢?谁来负责收发处理:消息通道里的 MessageChannel
的子接口 SubscribableChannel
,由MessageHandler
消息处理器所订阅。
2、为什么要用 SpringCloud Stream?
(1)由于RabbitMQ
和Kafka
两个消息中间件在架构上不同。例如 RabbitMQ 有 exchange ;Kafka 有 Topic 和 Partitions分区。所以二者不能够直接互通。
(2)SpringCloud Stream 凭什么可以统一底层差异?
在没有绑定器这个概念的情况下,springboot 应用要直接与消息中间件进行信息交互的时候,由于个消息中间件的构建初衷不同,我们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美的实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的 channel 通道,使得应用程序不再需要考虑各种不同的消息中间件实现。(通过定义绑定器 Binder 作为中间层,实现了应用程序与消息中间件细节之间的隔离。)
3、SpringCloud Stream 中的消息通信方式遵循了 发布-订阅模式
:
Topic主题进行广播:
(1)RabbitMQ 中就是 Exchange
(2)Kafka 中是 Topic
四、SpringCloud Stream 标准流程
1、流程图
(1)Binder
:很方便的连接中间件,屏蔽差异
(2)Channel
:通道,是队列 Queue 的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过 Channel 对队列进行配置。
(3)Source
和 Sink
:简单的可以理解为参照对象 SpringCloud Stream 自身,从 Srteam 发布消息就是输出,接受消息就是输入
。
2、编码 API 和常用注解
组成 | 说明 |
---|---|
Middleware | 中间件,目前只支持 RabbitMQ 和 Kafka |
Binder | Binder 是应用于消息中间件之间的封装,目前实现了 RabbitMQ 和 Kafka 的Binder,通过 Binder 可以很方便的连接中间件,可以动态的改变消息类型(对应于 RabbitMQ 的 Exchange ,Kafka 的 Topic),这些都可以通过配置文件来实现 |
@Input | 注解标识输入通道 ,通过改输入通道接收到的消息进入应用程序 |
@Output | 注解标识输出通道 ,发布的消息通过该通道离开应用程序 |
@StreamListener | 监听队列,用于消费者队列的消息接收 |
@EnableBinding | 指信道 channel 和 exchange 绑定在一起 |
消息驱动之生产者
1、创建cloud-stream-rabbitmq-provider8801
模块
2、引入 pom 依赖
<!--SpringCloud stream rabbitMQ-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
3、写 yml 配置文件
server:
port: 8801
spring:
application:
name: cloud-stream-rabbitmq-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:
#表示收将自己注册到EurekaServer,默认为true
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true,单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# defaultZone: http://localhost:7001/eureka #单机版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
instance:
instance-id: rabbit-send8801.com
#访问路径可以显示IP地址
prefer-ip-address: true
#eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认为30秒)
lease-renewal-interval-in-seconds: 1
#eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认为90秒),超时将删除服务
lease-expiration-duration-in-seconds: 2
3、主启动类
@SpringBootApplication
@EnableEurekaClient
public class StreamRabbitMQProviderMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamRabbitMQProviderMain8801.class, args);
}
}
4、业务类
(1)service :发到消息到MQ
import com.atguigu.springcloud.service.IMessageProviderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;
import javax.annotation.Resource;
import java.util.UUID;
@Slf4j
@EnableBinding(Source.class) //定义消息的推送管道,即:源
/*此处不再需要引入 spring 注解 @Service,这里的业务实现类是与RabbitMQ配合的,使用的 SpringCloud Stream 的注解*/
public class MessageProviderServiceImpl implements IMessageProviderService {
@Resource
private MessageChannel output; //消息发送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
log.info("*****serial:" + serial);
return "RabbitMQ 消息发送方:" + serial;
}
}
(2)controller
@RestController
public class SendMessageController {
@Resource
private IMessageProviderService messageProviderService;
@GetMapping(value = "/sendMessage")
public String sendMessage() {
return messageProviderService.send();
}
}
消息驱动之消费者
1、创建cloud-stream-rabbitmq-consumer8802
模块
2、引入 pom 依赖
<!--SpringCloud stream rabbitMQ-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
3、写 yml 配置文件
server:
port: 8802
spring:
application:
name: cloud-stream-rabbitmq-consumer
cloud:
stream:
binders: # 在此处配置要绑定的 rabbitmq 的服务信息
defaultRabbit: # 表示定义的名称,用于 binding 整合
type: rabbit # 消息组件类型
environment: # 设置 rabbitmq 的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称,消息发送方使用的output、消息接收方使用的是input
destination: studyExchange # 表示要使用的 Exchange 名称定义
content-type: application/json #设置消息类型,本次为json,文本则设置 text/plain
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client:
#表示收将自己注册到EurekaServer,默认为true
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true,单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# defaultZone: http://localhost:7001/eureka #单机版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
instance:
instance-id: rabbit-receive8802.com
#访问路径可以显示IP地址
prefer-ip-address: true
#eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认为30秒)
lease-renewal-interval-in-seconds: 1
#eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认为90秒),超时将删除服务
lease-expiration-duration-in-seconds: 2
3、主启动类
@SpringBootApplication
@EnableEurekaClient
public class StreamRabbitMQConsumerMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamRabbitMQConsumerMain8802.class, args);
}
}
4、controller
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController {
@Value(value = "${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消费者 1 号,----->接收到的消息:" + message.getPayload() + "\t port:" + serverPort);
}
}
5、测试:启动注册中心Eureka、cloud-stream-rabbitmq-provider8801、cloud-stream-rabbitmq-consumer8802等微服务。输入http://localhost:8801/sendMessage
,访问8801消息提供者,这时消息会发送到MQ中,消息订阅方(消息消费者)会通过与消息提供对方的共同的消息通道获取消息。结果如下:
分组消费与持久化
前提: 参考 cloud-stream-rabbitmq-consumer8802 模块,创建cloud-stream-rabbitmq-consumer8803
模块(负载均衡
),创建后依次启动 注册中心、消息生产者(8801)、消息消费者(8802、8803)。运行后存在两个问题:重复消费
、消息持久化
。
消息重复消费
1、目前访问
http://localhost:8801/sendMessage
接口时,8802/8803 (二者为负载均衡)同时都收到了消息存在消息的重复消费问题。如下:
2、如何解决消息的重复消费?
通过分组和持久化属性 group(重要)
解决。
3、生产实际案例
比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那么如果一个订单同时被两个服务实例获取到
,那么就会造成数据错误,我们必须得避免这种情况。
导致的原因: 默认分组 group 是不同的,组流水号不一样,被认为不同组,可以重复消费。
同一分组(解决方案):这时我们就可以使用SpringCloud Srteam 中的消息分组来解决。
注意:
在Stream 中处于同一个group
分组的多个消费者是竞争关系
,就能够保证消息只会被器中一个应用消费一次。不同分组是可以全面消费的(即可以重复消费)。
消息分组
1、原理: 微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。
2、自定义分组:
即自定义将
8802/8803
都变成不同的组,两个不同的 group ,不使用 RabbitMQ 默认的分组流水号。(不同的分组允许重复消费),修改 8802(group-8802)、8803(group-8803) 的 yml 配置文件,添加自定义的不同的group分组 。
刷新localhost:15672/#/queues
,结果如下:
3、重复消费
分布式微服务应用为了实现高可用和负载均衡
,实际上都会部署多个实例,本例中启动了两个消息订阅服务(消费者:8802/8803)。多数情况下,生产生发送消息给某个具体的微服务时,直系往被消费一次,按照上面我们启动两个应用的例子,虽然他们属于同一应用牡丹石这个消息出现被重复消费两次的情况,为了解决这个问题,在SpringCloud Stream 中提供了消费组
的概念。8802/8803 实现了轮询分组,每次只有一个消费者,8801模块(消息发送方)发送的消息只能被 8802 或 8803 器中一个接收到,避免了重复消费。即:将 8802/8803 的分组设置成为同一个分组group: same-group
。
测试: 当刷新http://localhost:8801/sendMessage
两次时,8802/8803(负载均衡) 只能其中之一微服务应用消费一次消息,避免了消息的重复消费。【同一个消费分组的多个微服务实例只会有一个实例拿到消息】
消息持久化
(1)停止微服务 8802/8803,并去掉 8802 的分组(group-8802),8803 的分组(group-8803)不用去掉。
(2)发送 4 条消息到 MQ。
(3)先重启 8802 ,发现 8802 重启后并没有重新获取并消费 8801 中发送的未曾消费的消息
。由此可知,未设置消息分组的微服务在服务宕机重启后并不会获取并消费消息发送发的消息,发生消息丢失故障。
(4)启动 8803 ,发现 8803 重启后获取获取并消费了 8801 中发送的未曾消费的消息
。
结论: 在实际的生产过程中,一定要配置消息分组(group),以免造成服务宕机造成的消息丢失的问题