1.为什么引入Stream?
一提到消息想到的肯定是消息中间件,市面上用的最多的有四种MQ,分别是ActiveMQ,RabbitMQ,RocketMQ,Kafka。一般一个项目前端是vue,中台是JavaEE,后台是大数据,大数据最常用的消息中间件是Kafka,JavaEE可能用的是RabbitMQ,这就存在着一个问题,在中台和后台的交互过程中,两种不同MQ的切换,维护和开发十分繁琐,对于程序员的要求很高。比如说你今天被调到大数据组了,大数据那边用的是kafka,然后你不会kafka,然后那边告诉你兄弟学一下kafka吧,那你就爽歪歪了,白天加班,晚上熬夜,每天007,那你就彻底完了。所以为了拒绝996,防止007,贯彻爱与正义。SpringCloud Stream消息驱动闪亮登场。
SpringCloud Stream是一个构建消息驱动微服务的框架,Stream可以让我们不再关注具体MQ的细节我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换。目前仅支持RabbitMQ,Kafka。
Stream简单原理:应用程序通过inputs或者outputs来与Spring Cloud Stream中binder对象交互。通过我们配置来帮顶,Spring Cloud Stream的binder对象负责与消息中间件交互。
2.Stream设计思想
在之前我们没有Stream之前传统的MQ之间的通知方式:
- 生产者消费者靠特定的消息媒介Message约定好,消息头,消息正文,消息的格式和附件属性。
- 消息必须走特定的通道MessageChannel
- 消息通道里的消息被消费的过程是,MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器订阅处理。
之后我们通过定义绑定器作为中间层,完美的实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同消息中间件的实现。Stream中的消息通信方式遵循了发布订阅模式。
3.Stream常用注解
如图所示图看一下Stream有哪些常用注解
Middleware:中间件,目前只支持RabbitMQ和Kafka。
Binder:Binder是应用与消息中间件之间的封装,目前实行了Kafka和RabbitMQ的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(kafka的是topic,RabbitMQ的exchange),这些都可以通过配置文件来实现。
- @Input:注解标识输入通道,通过该输入通道接收到的消息进入应用程序。
- @Output:注解标识输出通道,发布消息将通过该通道离开应用程序。
- @Stream Listener:监听队列,用于消费者的队列的消息接收
- @EnableBinding:指信道channel和exchange绑定在一起
4.Stream应用小案例
4.1消息驱动之生产者
新建一个module然后再pom文件中导入相应的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
然后建一个yml文件application.yml
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地址
然后新建一个接口和实现类
public interface IMessageProvider {
String send();//发送消息
}
@EnableBinding(Source.class)
public class MessageProviderImpl implements IMessageProvider {
@Autowired
private MessageChannel output;//消息发送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("****************serial:" + serial);
return null;
}
}
这个实现类注解上的Source,就是之前stream设计思想的第二张图,提到过Source这个就有点类似于上面注解第一张图outputs管道,所以这个@EnableBinding注解定义了消息的推送管道。以前我们要在接口实现类上加一个@Service,然后具体实现就得去调DAO,但是我们这不是传统的,我们要操作的是RabbitMQ所以得用他的注解。然后我们再构建一个消息通过output发送到RabbitMQ消息中间件中。
然后就是controller类
@RestController
public class SendMessageController {
@Autowired
private IMessageProvider messageProvider;
@GetMapping("/sendMessage")
public String sendMessage(){
return messageProvider.send();
}
}
然后我们开启eureka和8801这个服务访问/sendMessge这个接口控制台应该会打出发送消息的流水号。
然后再看我们的rabbitmq的web界面
小波峰上来了说明全部整合成功了。下面我们看一下消费者如何拿到我们后台的流水号。
4.2消息驱动之消费者
再创建一个消费者module端口号为8802,pom文件和yml文件和之前的生产者十分相似。但是要注意yml文件中有一处和生产者不同output改成input
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
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 # 设置消息类型,本次为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: receive-8802.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
然后是主启动类(略)和业务类,主要看业务类,因为他是消费者所以只要有controller就行了。
@Component
@EnableBinding(Sink.class)//消费者用Sink
public class ReceviceListennerController {
@Value("${server.port}")
String serverPort;
@StreamListener(Sink.INPUT)//用于监听消息队列
public void input(Message<String> message){
System.out.println("消费者1号,-------->接收到的消息:" + message.getPayload() + "\t port:" + serverPort);
}
}
之前发送的消息是String类型所以我们这边的接收消息Message的泛型是String,然后生产者管道有一个withPlayload方法,到消费者这边就必须有一个getPayload方法获得生产者的发送的消息。
这时候访问localhost:8801/sendMessage不仅能在8801控制台下面看到发送的消息流水号而且8802端口还能接收到生产者发来的消息
5.重复消费问题及解决方案
首先我们根据8802克隆一个8803然后8801生产者发送消息结果8802和8803都能收到同样的信息。这就是重复消费问题。
比如这样
8801:
8802:
8803:
可以看出8801生产者发送了四条消息,8802,8803消费者接收到的消息是一样的都是和8801一模一样。这就是重复消费。
如果在这样的一个场景下订单系统我们做集群部署,都会从RabbitMQ获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误我们可以通过Stream消息分组解决,在Stream中处于同一个group的多个消费者是竞争关系,就能保证消息只会被其中一个应用消费一次,不同组是可以全面消费的(重复消费),同一组内会发生竞争,只有其中一个可以消费。我们可以在8802,8803的配置文件中增加group配置让他们都是同一个组的这样他们之间存在竞争就不会发生重复消费的问题了。
8801:
8802:
8803:
6.消息持久化
假如说我停掉8802和8803并且去除掉8802的分组然后8801不断发送消息看看结果
8801发送消息
8802结果:
并没有任何接收消息的现象
8803结果:
我们可以看到8803虽然之前停机了但是之后再重启的时候会接收之前8801发送的东西。