SpringCloud Stream消息驱动
1、消息驱动概述
什么是消息驱动
在实际应用中有很多消息中间件,比如现在企业里常用的有ActiveMQ、RabbitMQ、RocketMQ、Kafka等,学习所有这些消息中间件无疑需要大量时间经历成本,那有没有一种技术,使我们不再需要关注具体的消息中间件的细节,而只需要用一种适配绑定的方式,自动的在各种消息中间件内切换呢?消息驱动就是这样的技术,它能**屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。**好比jdb能够操作MySQL、Oracle等数据库。
SpringCloud Stream是一个构件消息驱动的微服务框架。应用程序通过inputs和outputs来与SpringCloud Stream中的绑定器(binder)对象交互,通过配置来绑定,而SpringCloud Stream的绑定器对象负责与消息中间件交互,所以,我们只需要搞清楚如何与SpringCloud Stream交互就可以方便使用消息驱动的方式。但是截至到目前时间,SpringCloud Stream目前仅支持RabbitMQ和Kafka。
官网
https://spring.io/projects/spring-cloud-stream
设计思想
在经典的消息队列中,生产者/消费者之间靠消息媒介传递信息内容,消息必须走特定的通道Message Channel,消息通道里的子接口Subscribable Channel消费消息,然后MessageHandler负责收发处理。
在SpringCloud Stream中,通过定义绑定器(binder)作为中间层,实现了应用程序与消息中间件细节之间的隔离。在消息绑定器中,INPUT对应于消费者,OUTPUT对应于生产者,Stream中的消息通信方式遵循了发布—订阅模式:用Topic(主题)进行广播(RabbitMQ中对应于Exchange交换机,Kafka中就是Topic)。
编码API和常用注解
组成 | 说明 |
---|---|
Middleware | 中间件,目前只支持RabbitMQ和Kaf |
Binder | Binder是应用与消息中间件之间的封装,目前实行了RabbitMQ和Kafka的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应于Kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现 |
@Input | 注解标识输入通道,通过该输入通道接收到的消息进入应用程序 |
@Output | 注解标识输出通道,发布的消息将通过该通道离开应用程序 |
@StreamListner | 监听队列,用于消费者的队列的消息接收 |
@EnableBinding | 使信道Channel和交换机/主题(Exchange/Topic)绑定在一起 |
2、SpringCloud Stream之消息生产者
新建Module:stream_rabbitmq_provider8801作为消息的生产者用来发送消息
pom文件
在其POM文件中除引入web、actuator、eureka-client等必要启动器外,还需要引入SpringCloud Stream对应实现RabbitMQ的启动器依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
编写其配置文件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: # 这个名字是一个通道的名称,OUTPUT表示这是消息的发送方
destination: testExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://eureka7002.com:7002/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: send-8801.com # 在信息列表时显示主机名称
service业务类
@EnableBinding
注解用来绑定消息的推送管道,消息生产者绑定的消息推送管道为org.springframework.cloud.stream.messaging.Source
:
package com.jun.Service;
/**
* @author junfeng.lin
* @date 2021/3/2 18:44
*/
public interface IMessageProvider {
public String send();
}
package com.jun.Service.Impi;
import com.jun.Service.IMessageProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import java.util.UUID;
/**
* @author junfeng.lin
* @date 2021/3/2 18:45
*/
@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;
}
}
controller
package com.jun.Controller;
import com.jun.Service.IMessageProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author junfeng.lin
* @date 2021/3/2 18:48
*/
@RestController
public class SendMessageController {
@Autowired
private IMessageProvider messageProvider;
@GetMapping("/sendMessage")
public String sendMessage() {
return messageProvider.send(); //发送消息
}
}
测试
先后启动eureka注册中心、消息生产者模块和RabbitMQ,在控制面板中我们看到了有一个名为testExchange的交换机
访问 http://localhost:8801/sendMessage 使用消息生产者微服务发送消息,在其微服务后台我们看到了打印的消息,在RabbitMQ的控制面板中我们也看到了确实发送了消息。
3、SpringCloud Stream之消息消费者
新建Module:stream_rabbitmq_provider8802和stream_rabbitmq_provider8803作为消息的生产者用来发送消息
其POM文件中引入的启动器依赖和消息生产者微服务的依赖几乎相同
编写其配置文件application.yml
其配置文件的书写和消息生产者的几乎一致,特别需要注意的是,消息生产者微服务用到的通道为OUTPUT,而消息消费者微服务用到的通道为INPUT,其他的配置文件信息就只需要注意端口号、注册服务名的区别即可
spring:
cloud:
bindings:
input: # 这个名字是一个通道的名称,INPUT表示消息消费者
destination: testExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
controller
由于是消费者,所以只需要编写其Controller即可,@EnableBinding注解用来绑定消息的推送管道,消息消费者绑定的消息推送管道为import org.springframework.cloud.stream.messaging.Sink,在接收消息的方法中需要使用@StreamListner注解来监听其绑定的消息推送管道
package org.jun.Controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
/**
* @author junfeng.lin
* @date 2021/3/2 18:48
*/
@EnableBinding(Sink.class)
public class ReceiveMessageController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消费者" + serverPort + "号,收到消息:" + message.getPayload());
}
}
测试
先后启动eureka注册中心、消息生产者模块和消息消费者模块,调用消息生产者接口发送消息,此时消息消费者接收到消息,并在控制台中打印如下
4、分组消费
重复消费问题
当生产者发送消息后,此时的我们的消费者都接受了消息并进行了消费,也就是说同一条消息被多个消息消费者所消费。其实重复消费这个问题本身不可怕,可怕的是没考虑到重复消费之后,怎么保证幂等性,在有些情况下不能使用重复消费,所以可以利用SpringCloud Stream的消息分组功能来避免重复消费。
在SpringCloud Stream中,处于同一组中的多个消息消费者是竞争关系,也就是保证生产者所发送的同一个消息只会被其中一个消费者消费一次。在不同组的消费者是可以对消息进行重复消费的,只有同一组内才会发生竞争关系。
分组解决重复消费问题
在RabbitMQ控制面板中我们看到的组流水号是系统随机分配的,这样无疑不好控制,所以我们应该自定义配置分组,将8802/8803两个消息消费者微服务分为同一个组,以此来解决消息的重复消费问题,先来演示如何自定义分组。
在8802/8803微服务中的配置文件中分别添加组名属性,当组名相同时表示为同一组
spring:
cloud:
stream:
bindings:
input:
group: groupName # 分组名称
将两个消费者微服务设置相同的组名myGroup并重启项目后,可以看到rabbitmq的队列如下
此时用生产者发送11条信息,可以看到8802微服务收到6条,而8803微服务收到5条,说明处于同一分组的消费者是竞争关系