目录
一、为什么要引入SpringCloud Stream
举例:对于我们Java程序员来说,可能有时要使用ActiveMQ,有时要使用RabbitMQ,甚至还有RocketMQ以及Kafka,这之间的切换似乎很麻烦,我们很难,也没有太多时间去精通每一门技术,那有没有一种新技术的诞生,让我们不再关注具体MQ的细节,自动的给我们在各种MQ内切换
一句话:屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
二、SpringCloud Stream简介
官网:https://spring.io/projects/spring-cloud-stream
Spring Cloud Stream 是一个用来为微服务应用构建消息驱动能力的框架。它可以基于 Spring Boot 来创建独立的、可用于生产的 Spring 应用程序。Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,并引入了发布-订阅、消费组、分区这三个核心概念。通过使用 Spring Cloud Stream,可以有效简化开发人员对消息中间件的使用复杂度,让系统开发人员可以有更多的精力关注于核心业务逻辑的处理。但是目前 Spring Cloud Stream 只支持 RabbitMQ 和 Kafka 的自动化配置。
Spring Cloud Stream 是一个构建消息驱动微服务的框架。应用程序通过inputs 或者outputs来与Spring Cloud Stream中binder对象交互。
通过我们配置来binding(绑定),而Spring Cloud Stream的binder对象负责与消息中间件交互。所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动,Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现引用了发布-订阅、消费组、分区的三个核心概念。通过使用 Spring Cloud Stream,可以有效简化开发人员对消息中间件的使用复杂度,让系统开发人员可以有更多的精力关注于核心业务逻辑的处理。
Spring Cloud Stream 目前只支持 RabbitMQ 和 Kafka 的自动化配置。
标准MQ
生产者/消费者之间靠 消息媒介 传递消息内容 Message
消息必须走特定的通道 MessageChannel
消息通道里的消息如何被消费呢,谁负责收发处理?
消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅。
Binder input对应消费者 output对应生产者
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了 应用程序与消息中间件细节之间的隔离。Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无知干,甚至动态的切换中间件 (rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以更多的关注业务流程。
通过定义绑定器Binder作为中间件,实现了应用程序与消息中间件细节之间的隔离。
通讯方式遵循了发布-订阅模式,topic主题进行广播,在RabbitMQ就是exchange ,kakfa就是topic
Spring Cloud Stream 标准流程套路
- Binder 很方便的连接中间件,屏蔽差异
- Channel 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过channel队列进行配置
- Source和Sink 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。
三、代码示例cloud-stream-rabbitmq-provider8001
三个子模块 cloud-stream-rabbitmq-provider8001 作为生产者进行发消息模块
3.1 消息驱动生产者cloud-stream-rabbitmq-provider8001
新建module ,消息驱动生产者cloud-stream-rabbitmq-provider8001
3.1.1 pom文件
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- 注册服务-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.1.2 application.yml
server:
port: 8801
spring:
application:
name: cloud-stream-service
cloud:
stream:
binders: #在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: #表示定义的名称,用于binding整合
type: rabbit #消息组件类型
environment: #设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bingings: #服务的整合处理
output: #这个名字是一个通道的名称
destination: studyExchange #表示要使用的Exchange名称定义
content-type: application/json #设置消息类型,本次为json,文本则设置“text/plain"
binder: defaultRabbit #设置要绑定的消息服务的具体设置
eureka:
client:
#表示是否将自己注册进EurekaServer中 默认为true
register-with-eureka: true
#是否从Eureka Server抓取已有的注册信息,默认为true 单节点无所谓,集群必须设置为true 才能配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
instance:
instance-id: send-8001.com #在信息列表时显示主机名称
prefer-ip-address: true #访问路径可以显示IP地址
lease-expiration-duration-in-seconds: 5 #(默认是90s)
lease-renewal-interval-in-seconds: 2 #(默认是30s)
3.1.3 主启动类
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class,args);
}
}
3.1.4 业务类
public interface IMessageProvider {
public String send();
}
@EnableBinding(Source.class) // 定义消息的推送管道
@Slf4j
public class IMessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; //消息发送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
log.info("***********serial: "+ serial);
return null;
}
}
@RestController
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage(){
return messageProvider.send();
}
}
3.1.5 测试
访问:http://localhost:8801/sendMessage
3.2 消息驱动之消费者module cloud-stream-rabbitmq-consumer8802
新建module cloud-stream-rabbitmq-consumer8802
3.2.1 pom文件
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- 注册服务-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.2.2 application.yml
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: # 服务的整合处理
input: # 输出通道的名称
destination: studyExchange #表示要使用的 Exchange 名称定义
content-type: application/json # 消息类型
binder: defaultRabbit
group: group1
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳时间间隔默认30s
lease-expiration-duration-in-seconds: 5 # 如果超过了5秒的间隔默认90s
instance-id: receive-8002.com #信息列表显示主机名称
prefer-ip-address: true # 访问路径变为ip地址
3.2.3 主启动类
@SpringBootApplication
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class,args);
}
}
3.2.4 业务类
@Component
@EnableBinding(Sink.class)
@Slf4j
public class ReceiveMessageListenerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message){
log.info("消费者1号,----------》接收到的消息: "+message.getPayload()+"\t port:"+serverPort);
}
}
3.2.5测试
启动7001,,801,8802,测试8801发送8802接收消息:
http://localhost:8801/sendMessage
控制台输出:
3.3依照8802,clone一份8803
启动7001,8801,8002,8003
8001发送两条消息:
http://localhost:8801/sendMessage
发现8002,8003同时收到接收消息,存在重复消费的问题。
为什么会产生重复消费的问题呢?来看一下生产实际案例
比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中的消息分组来解决。
注意在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。不同组是可以全面消息的(重复消费)。
8802 8803没有分组,默认为不同的组,每一个微服务的组都不一样,不同的组都会把消息消费一次,所以导致了重复消费。
如何解决:分组和持久化属性group
四、分组和持久化
4.1分组原理:
微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。
8802与8803都变成相同组,group两个相同。
8802,8803在yml添加相同内容:记住与binder对齐
group: group1
启动7001,8801,8802,8803,测试http://localhost:8801/sendMessage
发现8802/8803实现了轮询分组,每次只有一个消费者。8801模块发送的消息只能被8802或者8803其中一个接收到,这样避免了重复消费。
4.2 持久化
通过上述,解决了重复消费问题,再看看持久化
停止8802/8803,删除8802的分组group: group1,8803的group保留。
8801先发送4条消息到rabbitmq,启动8802,无分组属性配置,后台没有打印出消息,再启动8803,有分组属性配置,后台打印MQ上的消息 (自行查看吧)
github源代码地址:https://github.com/diligentkong/cloud2020