在实际的企业开发中,消息中间件是至关重要的组件之一。消息中间件主要解决应用解耦、异步消息、流量削锋等问题,实现高性能、高可用、可伸缩和最终一致性架构。不同的中间件其实现方式,内部结构是不一样的。如常见的RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic、partitions分区,这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,如果用了两个消息队列的其中一种,后面的业务需求想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候Spring Cloud Stream 给我们提供了一种解耦合的方式。
Spring Cloud Stream由一个中间件中立的核组成。应用通过Spring Cloud Stream插入的input(相当于消费者consumer,它是从队列中接收消息的)和output(相当于生产者producer,它是从队列中发送消息的。)通道与外界交流。通道通过指定中间件的Binder实现与外部代理连接。业务开发者不再关注具体消息中间件,只需关注Binder对应用程序提供的抽象概念来使用消息中间件实现业务即可。
说明:最底层是消息服务,中间层是绑定层,绑定层和底层的消息服务进行绑定,顶层是消息生产者和消息消费者,顶层可以向绑定层生产消息和获取消息消费。
(1)绑定器
Binder 绑定器是Spring Cloud Stream中一个非常重要的概念。在没有绑定器这个概念的情况下,Spring Boot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,这使得我们实现的消息交互逻辑就会非常笨重,因为对具体的中间件实现细节有太重的依赖,当中间件有较大的变动升级、或是更换中间件的时候,就需要付出非常大的代价来实施。
通过定义绑定器作为中间层,实现了应用程序与消息中间件(Middleware)细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件的实现。当需要升级消息中间件,或者是更换其他消息中间件产品时,我们需要做的就是更换对应的Binder绑定器而不需要修改任何应用逻辑 ,甚至可以任意的改变中间件的类型而不需要修改一行代码。
Spring Cloud Stream支持各种binder实现,下表包含GitHub项目的链接。
- Apache Kafka: (https://github.com/spring-cloud/spring-cloud-stream-binder-kafka)
- Amazon Kinesis: (https://github.com/spring-cloud/spring-cloud-stream-binder-aws-kinesis)
- Google PubSub (partner maintained):(https://github.com/spring-cloud/spring-cloud-gcp/tree/main/spring-cloud-gcp-pubsub-stream-binder)
- Solace PubSub+ (partner maintained):(https://github.com/SolaceProducts/spring-cloud-stream-binder-solace)
- Azure Event Hubs (partner maintained):(https://github.com/microsoft/spring-cloud-azure)
通过配置把应用和Spring Cloud Stream的 binder 绑定在一起,之后只需要修改 binder 的配置来达到动态修改topic、exchange、type等一系列信息而不需要修改一行代码。
(2)发布/订阅模型
在Spring Cloud Stream中的消息通信方式遵循了发布-订阅模式,当一条消息被投递到消息中间件之后,它会通过共享的Topic主题进行广播,消息消费者在订阅的主题中收到它并触发自身的业务逻辑处理。这里所提到的Topic 主题是Spring Cloud Stream中的一个抽象概念,用来代表发布共享消息给消费者的地方。在不同的消息中间件中, Topic 可能对应着不同的概念,比如:在RabbitMQ中的它对应了Exchange,而在Kakfa中则对应了Kafka中的Topic。
案例中是通过RabbitMQ作为消息中间件来完成SpringCloud Stream的案例,需要安装并启动RabbitMQ,该安装教程在上一篇博客笔记中有介绍。
(1)创建SpringBoot工程 stream_producer,并在pom.xml中引入如下依赖。
org.springframework.cloud
spring-cloud-stream
org.springframework.cloud
spring-cloud-starter-stream-rabbit
org.springframework.cloud
spring-cloud-stream-binder-rabbit
(2)配置application.yml
server:
port: 7001 #服务端口
spring:
application:
name: stream_producer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
output:
destination: etc-default-channel #指定消息发送的目的地,在rabbitmq中,发送到一个etc-default-channel的exchange中
binders: #配置绑定器
defaultRabbit:
type: rabbit
(3)定义bingding(Spring Cloud Stream 已提供)
发送消息时需要定义一个接口,其接口方法的返回对象是MessageChannel,该接口Spring Cloud Stream 已提供。Spring Cloud Stream 提供的内置接口Source,该接口声明了一个 binding 命名为 “output”,这个binding 声明了一个消息输出流,也就是消息的生产者。
public interface Source {
String OUTPUT = "output";
@Output("output")
MessageChannel output();
}
(4)创建发送数据的工具类
创建一个向中间件发送数据的工具类
/**
* 负责向中间件发送数据
*/
@Component
@EnableBinding(Source.class)
public class MessageSender {
@Autowired
@Qualifier("output")
private MessageChannel output;
//发送消息
public void send(Object obj) {
output.send(MessageBuilder.withPayload(obj).build());
}
}
(5)测试发送消息
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ProducerTest {
@Autowired
private MessageSender messageSender;
@Test
public void testSend() {
messageSender.send("hello spring cloud stream.");
}
}
(1)创建SpringBoot工程 stream_consumer,并在pom.xml中引入如下依赖。
org.springframework.cloud
spring-cloud-stream
org.springframework.cloud
spring-cloud-starter-stream-rabbit
org.springframework.cloud
spring-cloud-stream-binder-rabbit
(2)配置application.yml
server:
port: 7002 #服务端口
spring:
application:
name: stream-consumer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
input: #内置的获取消息的通道, 从etc-default-channel中获取消息
destination: etc-default-channel
binders:
defaultRabbit:
type: rabbit
(3)定义bingding(Spring Cloud Stream 已提供)
同发送消息一致,在Spring Cloud Stream中接受消息,需要定义一个接口,其注解 @Input 对应的方法,需要返回 SubscribableChannel ,并且参入一个参数值。该接口Spring Cloud Stream 已提供,Spring Cloud Stream 提供的内置接口Sink,该接口声明了一个 binding 命名为 “input” 。
public interface Sink {
String INPUT = "input";
@Input("input")
SubscribableChannel input();
}
(4)创建监听binding中的消息的工具类,如果监听到消息,则会在控制台输出。
@Component
@EnableBinding(Sink.class)
public class MessageListener {
//监听binding中的消息
@StreamListener(Sink.INPUT)
public void input(String message) {
System.out.println("获取到消息: "+message);
}
}
定义一个 java类,并且添加注解@EnableBinding(Sink.class) ,其中 Sink 就是上述的接口。同时定义一个方法(此处是 input)标明注解为 @StreamListener(Processor.INPUT),方法参数为 Message 。所有发送 exchange 为“itcast-default ” 的MQ消息都会被投递到这个临时队列,并且触发上述的方法。
运行消息生产者工程stream_producer和消息消费者工程stream_consumer,点击运行消息生产者stream_producer的测试方法,则会向RabbitMQ发送消息,而消息消费者stream_consumer的监听方法会监听到消息,并在控制台输出消息。
Spring Cloud Stream 内置了两种接口,分别定义了 binding 为 “input” 的输入流 和 “output” 的输出流,而在实际使用中,往往是需要自定义各种输入输出流。自定义输入输出流的使用方法也很简单,只需要对配置文件进行配置即可。
(1)自定义输入输出流接口
一个接口中可以定义无数个输入输出流,可以根据实际业务情况划分。以下接口中定义了输入和输出两个 binding。在消息生产者和消息消费者中都需要进行定义。
public interface MyProcessor {
String MYINPUT = "myinput";
String MYOUTPUT = "myoutput";
@Input(MYINPUT)
SubscribableChannel myinput();
@Output(MYOUTPUT)
MessageChannel myoutput();
}
(2)修改消息生产者和消息消费者的配置文件
消息生产者工程stream_producer的application.yml
server:
port: 7001 #服务端口
spring:
application:
name: stream_producer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
output:
destination: etc-default-channel #指定消息发送的目的地,在rabbitmq中,发送到一个etc-default-channel的exchange中
myoutput:
destination: etc-custom-channel
binders: #配置绑定器
defaultRabbit:
type: rabbit
消息消费者工程stream_consumer的application.yml
server:
port: 7002 #服务端口
spring:
application:
name: stream-consumer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
input: #内置的获取消息的通道, 从etc-default-channel中获取消息
destination: etc-default-channel
myinput:
destination: etc-custom-channel
binders:
defaultRabbit:
type: rabbit
(3)使用时需要在消息生产者和消息消费者的@EnableBinding注解中添加自定义的接口。在消息消费者中使用 @StreamListener 做监听的时候,需要指定 MyProcessor.MYINPUT。
/**
* 负责向中间件发送数据
*/
@Component
@EnableBinding(MyProcessor.class)
public class MessageSender {
@Autowired
@Qualifier("myoutput")
private MessageChannel myoutput;
//发送消息
public void send(Object obj) {
myoutput.send(MessageBuilder.withPayload(obj).build());
}
}
@Component
@EnableBinding(MyProcessor.class)
public class MessageListener {
//监听binding中的消息
@StreamListener(MyProcessor.MYINPUT)
public void input(String message) {
System.out.println("获取到消息: "+message);
}
}
(4)运行消息生产者工程stream_producer和消息消费者工程stream_consumer,点击运行消息生产者stream_producer的测试方法,则会向RabbitMQ发送消息,而消息消费者stream_consumer的监听方法会监听到消息,并在控制台输出消息。
通常在生产环境,我们的每个服务都不会以单节点的方式运行在生产环境,当同一个服务启动多个实例的时候,这些实例都会绑定到同一个消息通道的目标主题(Topic)上。默认情况下,当生产者发出一条消息到绑定通道上,这条消息会产生多个副本被每个消费者实例接收和处理,但是有些业务场景之下,我们希望生产者产生的消息只被其中一个实例消费,这个时候我们需要为这些消费者设置消费组来实现这样的功能。
在同一个group中的多个消费者只有一个可以获取到消息并消费。消息分组实现的方式只需要在服务消费者端的配置文件中设置 spring.cloud.stream.bindings.input.group 属性即可。
(1)将消息消费者工程stream_consumer复制一份,修改文件名称为stream_consumer2,使用Idea打开后,修改pom.xml文件中的stream_consumer为stream_consumer2,再修改application.yml配置文件。此时如果启动stream_consumer和stream_consumer2工程,再运行stream_producer的测试方法的话,则stream_consumer和stream_consumer2工程都会接收到信息。
server:
port: 7003 #服务端口
spring:
application:
name: stream-consumer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
input: #内置的获取消息的通道, 从etc-default-channel中获取消息
destination: etc-default-channel
myinput:
destination: etc-custom-channel
binders:
defaultRabbit:
type: rabbit
(2)在服务消费者端的配置文件中设置 spring.cloud.stream.bindings.input.group 属性。
server:
port: 7002 #服务端口
spring:
application:
name: stream-consumer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
input: #内置的获取消息的通道, 从etc-default-channel中获取消息
destination: etc-default-channel
myinput:
destination: etc-custom-channel
binders:
defaultRabbit:
type: rabbit
server:
port: 7003 #服务端口
spring:
application:
name: stream-consumer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
input: #内置的获取消息的通道, 从etc-default-channel中获取消息
destination: etc-default-channel
myinput:
destination: etc-custom-channel
group: group1
binders:
defaultRabbit:
type: rabbit
(3)此时如果启动stream_consumer和stream_consumer2工程,再运行stream_producer的测试方法的话,则只有stream_consumer和stream_consumer2工程其中一个会接收到信息。在同一个group中的多个消费者只有一个可以获取到消息并消费。
有一些场景需要满足同一个特征的数据被同一个实例消费,比如同一个id的传感器监测数据必须被同一个实例统计计算分析,否则可能无法获取全部的数据。又比如部分异步任务,首次请求启动任务,二次请求取消任务,此场景就必须保证两次请求至同一实例。
(1)消息生产者配置
server:
port: 7001 #服务端口
spring:
application:
name: stream_producer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
bindings:
output:
destination: etc-default-channel #指定消息发送的目的地,在rabbitmq中,发送到一个etc-default-channel的exchange中
myoutput:
destination: etc-custom-channel
producer:
partition-key-expression: payload #分区关键字-对象中的id、对象
partition-count: 2 #分区大小
binders: #配置绑定器
defaultRabbit:
type: rabbit
从上面的配置中可以看到增加了以下两个参数:
(2)消息消费者配置
server:
port: 7002 #服务端口
spring:
application:
name: stream-consumer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
instanceCount: 2 #消费者总数
instanceIndex: 0 #当前消费者的索引
bindings:
input: #内置的获取消息的通道, 从etc-default-channel中获取消息
destination: etc-default-channel
myinput:
destination: etc-custom-channel
group: group1
consumer:
partitioned: true #开启分区支持
binders:
defaultRabbit:
type: rabbit
server:
port: 7003 #服务端口
spring:
application:
name: stream-consumer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: guest
password: guest
cloud:
stream:
instanceCount: 2 #消费者总数
instanceIndex: 1 #当前消费者的索引
bindings:
input: #内置的获取消息的通道, 从etc-default-channel中获取消息
destination: etc-default-channel
myinput:
destination: etc-custom-channel
group: group1
consumer:
partitioned: true #开启分区支持
binders:
defaultRabbit:
type: rabbit
从上面的配置中可以看到增加了这三个参数:
(3)可以将消息生产者的测试方法设置发送多个值来验证。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class StreamProducerApplicationTests {
@Autowired
private MessageSender messageSender;
@Test
public void testSend() {
for (int i=0; i<5; i++) {
messageSender.send("0 12345");
}
}
}
(4)启动消息生产者和两个消息消费者,需要注意的是要为消费者指定不同的实例索引号,这样当同一个消息被发给消费组时,可以发现只有一个消费实例在接收和处理这些相同的消息。