看一张小图:
比如,这是我们一个完整的项目(脑补完整!!)。此时中间件用的是RabbitMQ,如果现在项目需求换成ActiveMQ(对,就是一个很傻x的需求),你咋办?
思考60分钟3秒。......................
决定改代码!!!
那你就太low了,我们srpingCloud为我们提供了一个组件去解决这个问题。
一、Spring Cloud Stream
在实际的企业开发中,消息中间件是至关重要的组件之一。消息中间件主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。不同的中间件其实现方式,内部结构是不一样的。如常见的RabbitMQ 和 Kafka ,由于这两个消息中间件的架构上的不同,像RabbitMQ有 exchange , kafka 有 Topic , partitions 分区,这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候 springcloud Stream 给我们提供了一种解耦合的方式。
我们通过一个绑定层来解决这个问题,消息都经过绑定层,如果切换MQ,直接可以找到对应的MQ,相当于动态的配置不同的MQ。
1、核心概念
绑定器:
Binder 绑定器是 Spring Cloud Stream 中一个非常重要的概念。在没有绑定器这个概念的情况下,我们的Spring Boot 应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,这使得我们实现的消息交互逻辑就会非常笨重,因为对具体的中间件实现细节有太重的依赖,当中间件有较大的变动升级、或是更换中间件的时候,我们就需要付出非常大的代价来实施。
通过定义绑定器作为中间层,实现了应用程序与消息中间件 (Middleware) 细节之间的隔离。通过向应用程序暴露统一的Channel 通过,使得应用程序不需要再考虑各种不同的消息中间件的实现。当需要升级消息中间件,或者是更换其他消息中间件产品时,我们需要做的就是更换对应的Binder 绑定器而不需要修改任何应用逻辑 。甚至可以任意的改变中间件的类型而不需要修改一行代码。
发布/订阅模型:
在 Spring Cloud Stream 中的消息通信方式遵循了发布 - 订阅模式,当一条消息被投递到消息中间件之后,它会通过共享的 Topic 主题进行广播,消息消费者在订阅的主题中收到它并触发自身的业务逻辑处理。这里所提到的 Topic 主题是 Spring Cloud Stream 中的一个抽象概念,用来代表发布共享消息给消费者的地方。在不同的消息中间件中, Topic 可能对应着不同的概念,比如:在 RabbitMQ 中的它对应了Exchange 、而在 Kakfa 中则对应了 Kafka 中的 Topic 。
2、案例
生产者:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
</dependencies>
server:
port: 7001 #服务端口
spring:
application:
name: rabbitmq-consumer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: user
password: password
cloud:
stream:
bindings:
output:
destination: stream-default # 指定消息发送的目的地,rabbitMQ中发送到指定的exchange上
binders: # 绑定器配置
defaultRabbit:
type: rabbit
package com.springcloud.stream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
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;
/**
* @ClassName ProviderApplication
* @Description 生产者
* @Author
* @Date 2020/5/31 13:29
* @Version 1.0
**/
@SpringBootApplication
@EnableBinding(Source.class)
public class ProviderApplication implements CommandLineRunner {
@Autowired
private MessageChannel output;
@Override
public void run(String... args) throws Exception {
output.send(MessageBuilder.withPayload("hello world").build());
}
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class);
}
}
启动,就会看到对应的交换机注入了进来:
消费者:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
</dependencies>
server:
port: 7002 #服务端口
spring:
application:
name: rabbitmq-consumer #指定服务名
rabbitmq:
addresses: 127.0.0.1
username: user
password: password
cloud:
stream:
bindings:
input:
destination: stream-default
binders:
defaultRabbit:
type: rabbit
package com.springcloud.stream;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import javax.xml.transform.Source;
/**
* @ClassName ConsumerApplication
* @Description 消费者
* @Author
* @Date 2020/5/31 13:48
* @Version 1.0
**/
@SpringBootApplication
@EnableBinding(Sink.class)
public class ConsumerApplication {
/**
* 获取监听的消息
*/
@StreamListener(Sink.INPUT)
public void print(String str){
System.out.println("收到RabbitMQ : " + str);
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class);
}
}
代码优化:
生产者(单独提出来一个工具类)
package com.springcloud.stream.util;
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 org.springframework.stereotype.Component;
/**
* @ClassName provider
* @Description 向中间件发送消息
* @Author
* @Date 2020/5/31 14:02
* @Version 1.0
**/
@Component
@EnableBinding(Source.class)
public class MessageProviderUtil {
@Autowired
private MessageChannel output;
public void send(Object obj){
output.send(MessageBuilder.withPayload(obj).build());
}
}
单元测试
package com.springcloud.stream;
import com.springcloud.stream.util.MessageProviderUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @ClassName com.springcloud.stream.ProviderTest
* @Description
* @Author 戴书博
* @Date 2020/5/31 14:15
* @Version 1.0
**/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ProviderTest {
@Autowired
private MessageProviderUtil messageProviderUtil;
@Test
public void testSend(){
messageProviderUtil.send("hello world");
}
}
消费者
package com.springcloud.stream.util;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.stereotype.Component;
/**
* @ClassName MessageComsumerUtil
* @Description 发送消息
* @Author
* @Date 2020/5/31 14:13
* @Version 1.0
**/
@Component
@EnableBinding(Sink.class)
public class MessageComsumerUtil {
/**
* 获取监听的消息
*/
@StreamListener(Sink.INPUT)
public void print(String str){
System.out.println("收到RabbitMQ : " + str);
}
}
自定义通道
package com.springcloud.stream.interf;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
/**
* @ClassName MyProvider
* @Description 生产者消息通道
* @Author
* @Date 2020/5/31 14:31
* @Version 1.0
**/
public interface MyProvider {
String MY_OUTPUT = "myoutput";
@Output("myoutput")
MessageChannel myoutput();
}
package com.springcloud.stream.interf;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
/**
* @ClassName MyConsumer
* @Description 发送消息
* @Author
* @Date 2020/5/31 14:37
* @Version 1.0
**/
public interface MyConsumer {
String MY_INPUT = "myinput";
@Input(MY_INPUT)
SubscribableChannel myinput();
}
修改:
另一个同理,依然是可以收到消息的。
消息分组
现在我们有两个消费者,如果生产者发送消息,两个消费者都可以接收的。
通常在生产环境,我们的每个服务都不会以单节点的方式运行在生产环境,当同一个服务启动多个实例的时候,这些实例都会绑定到同一个消息通道的目标主题(Topic )上。默认情况下,当生产者发出一条消息到绑定通道上,这条消息会产生多个副本被每个消费者实例接收和处理,但是有些业务场景之下,我们希望生产者产生的消息只被其中一个实例消费,这个时候我们需要为这些消费者设置消费组来实现这样的功能。
一个分组里只能有一个消费者消费这个消息。
修改:两个消费者加一个这玩意就可以了
消息分区
有一些场景需要满足 , 同一个特征的数据被同一个实例消费 , 比如同一个 id 的传感器监测数据必须被同一 个实例统计计算分析, 否则可能无法获取全部的数据。又比如部分异步任务,首次请求启动 task ,二次请求取消task ,此场景就必须保证两次请求至同一实例
生产者
从上面的配置中,我们可以看到增加了这三个参数:
- spring.cloud.stream.bindings.input.consumer.partitioned :通过该参数开启消费者分区功能;
- spring.cloud.stream.instanceCount :该参数指定了当前消费者的总实例数量;
- spring.cloud.stream.instanceIndex :该参数设置当前实例的索引号,从 0 开始,最大值为spring.cloud.stream.instanceCount 参数 - 1 。我们试验的时候需要启动多个实例,可以通过运行参数来为不同实例设置不同的索引值。
消费者
从上面的配置中,我们可以看到增加了这两个参数:
- pring.cloud.stream.bindings.output.producer.partitionKeyExpression :通过该参数指定了分区键的表达式规则,我们可以根据实际的输出消息规则来配置SpEL 来生成合适的分区键;
- spring.cloud.stream.bindings.output.producer.partitionCount :该参数指定了消息分区的数量。
到这里消息分区配置就完成了,我们可以再次启动这两个应用,同时消费者启动多个,但需要注意的是要为消费者指定不同的实例索引号,这样当同一个消息被发给消费组时,我们可以发现只有一个消费实例在接收和处理这些相同的消息。