首先看到消息驱动,我们会想到消息中间件,比如以下几种
但是在实际开发中,可能会遇到一些问题,比如说上图的中台和后台可能存在两种MQ,它们之间的实现都是不一样的,这样会导致多种问题出现,而且我们也看到了,目前主流的MQ有四种,我们不可能每个都去学习
那有没有一技术,能让我们不再关注具体MQ的细节,只需要用一种适配绑定的方式,就可以自动的在各种MQ内切换呢?
这个时候,Spring Cloud Stream诞生了,解决的痛点就是屏蔽了消息中间件底层的细节差异,我们只需要操作Stream就可以操作各种消息中间件了,大大降低了开发成本。
消息驱动就是屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。
有点像Hibernate,它同时支持多种数据库,同时还提供了Hibernate Session的语法,也就是HQL语句,这样屏蔽了SQL具体实现细节,我们只需要操作HQL语句,就能够操作不同的数据库
Spring Cloud Stream是一个构建消息驱动微服务的框架
应用程序通过inputs或者outputs来与Spring Cloud Stream中binder对象(绑定器)交互。
binder对象屏蔽了底层的差异性,统一了编程风格,我们主要就是通过它去操作不同的消息中间件。
我们只需要搞清楚如何与Spring Cloud Stream中的binder对象(绑定器)交互,就可以方便的使用消息驱动。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。
Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅,消费组,分区的三个核心概念
Spring Cloud Stream目前仅支持RabbitMQ 和 Kafka
生产者/消费者之间靠消息媒介传递消息内容
消息必须走特定的通道
消息通道里的消息如何被消费呢,谁负责收发处理
比如说开发中用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上不同
像RabbitMQ有exchange,kafka有Tpic和Partitions分区
这些中间件的差异给我们实际开发中造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我们想往另外一种消息队列进行迁移,这时候无疑就是灾难性的,一大堆东西都要推到重新做,因为它跟我们的系统耦合了,这时候Spring Cloud Stream给我们提供了一种解耦的方式。
通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同消息中间件的实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(RabbitMQ切换Kafka),使得微服务开发的高度解耦,服务可以关注更多的自己的业务流程。
Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播,在RabbitMQ中就是Exchange,在Kafka中就是Topic
我们的消息生产者和消费者只和Stream交互
前提是已经安装好了RabbitMQ
cloud-stream-rabbitmq-provider8801
,消息生产者模块cloud-stream-rabbitmq-consumer8802
,消息接收模块cloud-stream-rabbitmq-consumer8803
,消息接收模块
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
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地址
package com.indi.springcloud;
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class, args);
}
}
MessageProvider.java
package com.indi.springcloud.service;
public interface MessageProvider
/**
* 发送消息的接口
*/
String send();
}
MessageProviderImpl.java
package com.indi.springcloud.service.impl;
@EnableBinding(Source.class) // 定义消息的推送管道,导stream下的包,别导错了!!
public class MessageProviderImpl implements MessageProvider {
@Resource
private MessageChannel output; // 消息发送通道
@Override
public String send() {
String serial = IdUtil.randomUUID();
output.send(MessageBuilder.withPayload(serial).build()); // 将准备好的消息发送到RabbitMQ,导integration下的包,别导错了!!
System.out.println("*****serial:" + serial);
return null;
}
}
SendMessageController.java
@RestController
public class SendMessageController{
@Resource
private MessageProvider messageProvider;
@GetMapping("/sendMessage")
public String sendMessage(){
return messageProvider.send();
}
}
RabbitMQ后台:http://localhost:15672/
测试链接:http://localhost:8801/sendMessage
同8801
package com.indi.springcloud;
@SpringBootApplication
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class, args);
}
}
ReceiveMessageListenerController.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)
}
}
启动7001、8801、8802
还是发送几次请求,测试链接:http://localhost:8801/sendMessage
结果8801、8802都打出了相应的消息
再进入RabbitMQ后台看看:http://localhost:15672/
启动7001、8801、8802、8803
我们再次尝试让8801发送消息,结果8802 、8803同时都收到了,存在重复消费的问题
比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们需要避免这种情况,可以使用Stream中的消息分组来解决。
在Stream中只要消费者存在竞争关系,那就能够保证每次消息只被一个消费者消费
多数情况下,生产者发送消息给某个具体微服务时,只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但它们两个在不同的组,所以才出现了消息被重复消费两次的情况,为了解决这个情况,我们需要将它们两个放到一个组内,这样它们就会产生竞争关系,消息就不会被重复消费。
当然,如果是这样的话,还是会产生重复消费的问题,这里只是演示一下如何分组。
然后我们将两个消费者放到同一个组内,比如都放到indiA
中
我们在8801,发送了6条消息
8802,收到了3条
8802,也收到了3条
最后因为8802、8803都在同一个组内,然后它们通过轮询的机制,都得到了消息,也没有造成重复消费。
通过上面的方式,我们已经解决了重复消费的问题,再来看看持久化。