简介
1、什么是Spring Cloud Stream
官方文档:Spring Cloud Stream Reference Documentation
Spring Cloud Stream 是用于构建消息驱动微服务应用程序的框架,该框架提供了一个灵活的编程模型,它提供了多种中间件的合理配置,包括:public/subscribe、消息分组、消息分区处理等功能的支持,它屏蔽了 RabbitMQ 底层操作,让我们使用统一的 Input 和 Output 形式,以 Binder 为中间件。
stream 对消息中间件的进一步封装,可以做到代码层面中对中间件的无感知,可以很方便地动态地切换中间件,使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
就类似 JDBC,它能够屏蔽底层实现,我们使用统一的消息队列操作方式就能操作多种不同类型的消息队列。
1.1、核心概念
组件 | 说明 |
---|---|
Middleware | 中间件,目前只支持 RabbitMQ、Kafka |
Binder | 目标绑定器,目标就是中间件。绑定器其实就是封装了目标中间件的依赖包。 |
@Input | 注解标识输入通道,MQ 的消息通过此通道进入应用程序。<br>即 MQ 输入给 Stream。 |
@Output | 注解标识输出通道,生产者的消息通过此通道离开应用程序,进入 Stream,然后输出给 MQ。<br>即 Stream 输出给 MQ。 |
@StreamListener | 监听队列,消费者的队列的消息接收 |
@EnableBinding(Source.class) | 注解标识绑定,将信道channel 和交换机exchange 进行绑定 |
1.2、工作原理
-
Source:当需要发送消息的时候,我们就需要通过 Source.java,它会把我们所要发送的消息进行一个序列化(默认转换成 JSON字符串),然后将这些数据发送到 Channel 中。
-
Sink:当我们需要监听消息的时候就需要通过 Sink.java,它负责从消息通过中获取消息,并将消息反序列化成消息对象,然后交给具体的消息监听处理。
-
Channel:通常我们向消息中间件发送消息或者监听消息时需要指定主题(Topic)和消息队列名称,一旦我们需要变更主题的时候就需要修改消息发送或消息监听的代码。通过Channel 对象,我们的业务代码只需要对应Channel 就可以了,具体这个 Channel 对应的是哪个主题,可以在配置文件中来指定,这样当主题变更的时候我们就不用对代码做任何修改,从而实现了与具体消息中间件的解耦;
-
Binder:通过不同的 Binder 可以实现与不同的消息中间件整合,Binder 提供统一的消息收发接口,从而使得我们回以根据实际需要部署不同的消息中间件,或者根据实际生产中所部署的消息中间件来调整我们的配置。
2、简单使用
2.1、实践1
2.1.1、前置配置
2.1.1.1、引入依赖
两个依赖选一个引入即可。
spring-cloud 项目必须是 pom 工程。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-binder-rabbit</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency>
2.1.2、Producer
创建默认的 topic交换机的时候,会默认创建一个临时队列,生产者、消费者双方监听的是交换机,而不是像以前那样监听队列。
2.1.2.1、配置文件
# 服务端口 server: port: 8080 # 配置rabbitmq服务 spring: application: name: stream-producer rabbitmq: username: guest password: guest virtual-host: / host: 192.168.174.128 port: 5672 cloud: stream: bindings: # 与 Source类的注解 @Output("output") 的功能相同 # 会自动创建 名称为:stream.message 的交换机,然后绑定 output: destination: stream.message
2.1.2.2、生产者
@Component @EnableBinding(Source.class) public class MessageProducer { @Autowired private Source source; // 发送消息 public void send(String message) { // output() 会得到一个信道 MessageChannel,然后我们通过信道给 MQ 发送消息 source.output().send(MessageBuilder.withPayload(message).build()); } }
2.1.3、消费者
2.1.3.1、配置文件
# 服务端口 server: port: 8081 # 配置rabbitmq服务 spring: application: name: stream-producer rabbitmq: username: guest password: guest virtual-host: / host: 192.168.174.128 port: 5672 cloud: stream: bindings: # 与 Sink类的注解 @Input("input") 的功能相同 input: # 绑定的交换机名称 destination: stream.message nacos: discovery: # nacos服务地址 server-addr: localhost:8848
2.1.3.2、消费者
@Component @EnableBinding(Sink.class) public class MessageConsumer { // 监听队列,等待消费 @StreamListener(Sink.INPUT) public void receive(String message) { System.out.println("消费者收到消息:" + message); } }
2.2、自定义消息通道channel 实践2
2.2.1、写自己的 Source、Sink
public interface MySource { String MY_OUTPUT = "my_output"; @Output(MY_OUTPUT) MessageChannel myOutput(); } public interface MySink { String MY_INPUT = "my_input"; @Input(MY_INPUT) SubscribableChannel myInput(); }
2.2.2、修改配置文件
# 生产者的那段 output: destination: stream.message my_output: destination: my.message # 消费者的那段 input: destination: stream.message my_input: destination: my.message
2.2.3、修改生产者和消费者
@Component @EnableBinding(MySource.class) public class MessageProducer { @Autowired private MySource mySource; // 发送消息 public void send(String message) { // output() 会得到一个信道 MessageChannel,然后我们通过信道给 MQ 发送消息 mySource.myOutput().send(MessageBuilder.withPayload(message).build()); } } @Component @EnableBinding(MySink.class) public class MessageConsumer { // 监听队列,等待消费 @StreamListener(MySink.MY_INPUT) public void receive(String message) { System.out.println("消费者收到消息:" + message); } }
2.2.4、配置优化
@Output("output")、@Input("input") 这两个注解的 value,默认为绑定的交换机的名称。
即修改我们自定义的消息通道的 MySource、MySink 的 MY_OUTPUT、MY_INPUT 这两个参数的值,然后删掉配置文件中对它的配置值,然后正常操作,就可以通过注解方式来定义交换机名称了。
2.3、实践3
2.3.1、Source消息生产者
2.3.1.1、channel
public interface MyProcessor { String SOURCE_MESSAGE = "source.message"; String SMS_MESSAGE = "sms.message"; String EMAIL_MESSAGE = "email.message"; @Output(SOURCE_MESSAGE) MessageChannel sourceOutput(); @Input(SMS_MESSAGE) SubscribableChannel smsInput(); @Input(EMAIL_MESSAGE) SubscribableChannel emailInput(); }
2.3.1.2、发送 Source消息
@Component @EnableBinding(MyProcessor.class) public class SourceMessageProducer { @Autowired private MyProcessor myProcessor; private Logger logger = LoggerFactory.getLogger(SourceMessageProducer.class); // 发送 source 消息,10086|10086@email.com public void sendSource(String message) { logger.info("source消息发送成功:" + message); myProcessor.sourceOutput().send(MessageBuilder.withPayload(message).build()); } }
2.3.1.3、接收 Sms、Email消息
@Component @EnableBinding(MyProcessor.class) public class SmsAndEmailMessageConsumer { private Logger logger = LoggerFactory.getLogger(SmsAndEmailMessageConsumer.class); // 监听队列,等待消费 @StreamListener(MyProcessor.SMS_MESSAGE) public void receiveSms(String message) { logger.info("sms消息接收成功:" + message + ",准备发送短信"); } // 监听队列,等待消费 @StreamListener(MyProcessor.EMAIL_MESSAGE) public void receiveEmail(String message) { logger.info("email消息接收成功:" + message + ",准备发送邮件"); } }
2.3.2、Source消息消费者
2.3.2.1、channel
Input 和 Output 是相反的。
public interface MyProcessor { String SOURCE_MESSAGE = "source.message"; String SMS_MESSAGE = "sms.message"; String EMAIL_MESSAGE = "email.message"; @Input(SOURCE_MESSAGE) SubscribableChannel sourceIntput(); @Output(SMS_MESSAGE) MessageChannel smsOutput(); @Output(EMAIL_MESSAGE) MessageChannel emailOutput(); }
2.3.2.2、接收 Source消息
@Component @EnableBinding(MyProcessor.class) public class SourceMessageConsumer { @Autowired private SmsAndEmailMessageProducer smsAndEmailMessageProducer; private Logger logger = LoggerFactory.getLogger(SourceMessageConsumer.class); // 监听队列,等待消费 @StreamListener(MyProcessor.SOURCE_MESSAGE) public void receiveSource(String message) { logger.info("source消息接收成功:" + message); smsAndEmailMessageProducer.sendSms(message.split("[|]")[0]); smsAndEmailMessageProducer.sendEmail(message.split("[|]")[1]); } }
2.3.2.3、发送 Sms、Email消息
@Component @EnableBinding(MyProcessor.class) public class SmsAndEmailMessageProducer { @Autowired private MyProcessor myProcessor; private Logger logger = LoggerFactory.getLogger(SmsAndEmailMessageProducer.class); // 发送 sms 消息,10086|10086@email.com public void sendSms(String message) { logger.info("sms消息发送成功:" + message); myProcessor.smsOutput().send(MessageBuilder.withPayload(message).build()); } // 发送 source 消息,10086|10086@email.com public void sendEmail(String message) { logger.info("email消息发送成功:" + message); myProcessor.emailOutput().send(MessageBuilder.withPayload(message).build()); } }
2.4、消息分组和消息分区实践4
2.4.1、消息分组
Stream 会给监听同一个交换机的不同端口的消费者创建不同的临时队列,那么一个消息来了之后,因为他们的路由关系相同,那么这个交换机就会给他们两个都发送消息,这就导致了一个消息被多个消费者消费,但是假如这个消息是个订单消息,那不就是产生了两份订单吗?
所以我们可以给消费者定义组,指定组就相当于指定队列一样,然后交换机创建的队列名称就不是默认的了,而是:交换机名称.组名称。所以给多个消费者指定同一个组,那么就只会创建一个队列了。
2.4.1.1、修改配置文件
# 生产者 output: destination: stream.message # 消费者 1号 input: destination: stream.message group: group-A # 消费者 2号 input: destination: stream.message group: group-A
2.4.2、消息分区
假如来了 10个订单,这些订单都是同一个用户的,那么我们应该让同一个消费者去处理这个人的多个订单,但是现在仅仅使用消息分组是无法解决的,因为消息会被消费者争抢,使用的是公平模式。
分区会在分组基础上加 -X,即会创建两个队列,一个叫:交换机名称.组名称-0,另一个叫:交换机名称.组名称-1,但逻辑上他们监听同一个队列。
2.4.2.1、修改配置文件
# 给生产者配置 分区键的表达式规则和消息分区的数量 # 生产者 output: destination: stream.message producer: # payload是一个规则,这个单词是固定的,同样的还要 headers。 # 这个规则的意思就是,队列中的消息全部都会被分区,谁先拿到第一个,那么剩下的全给他 # headers的话是在 output().send 方法的时候,MessageBuilder多加一个规则 # .withPayload(message).setHeader("xxx", 0).build() partition-key-expression: payload partition-count: 2 # 给消费者配置 cloud: stream: # 消费者总数 instance-count: 2 # 当前消费者的索引 instance-index: 0 bindings: input: destination: stream.message group: group-A consumer: # 开启对分区的支持 partitioned: true
2.5、实践5
2.5.1、前置配置
2.5.1.1、项目管理依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2021.0.1</version> <type>pom</type> <scope>import</scope> </dependency>
2.5.1.2、项目依赖
<dependencies> <!-- RabbitMQ的Stream实现 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
2.5.2、生产者
2.5.2.1、配置文件
server: port: 8001 spring: cloud: stream: binders: #此处配置要绑定的rabbitmq的服务信息 local-server: #绑定名称,随便起一个就行 type: rabbit #消息组件类型,这里使用的是RabbitMQ,就填写rabbit environment: #服务器相关信息,按照下面的方式填写就行,爆红别管 spring: rabbitmq: host: 192.168.0.6 port: 5672 username: admin password: admin virtual-host: /test bindings: test-out-0: destination: test.exchange
2.5.2.2、Controller
启动访问后,如果没有指定 destination,会默认在 RabbitMQ 中创建 test-out-0 这个交换机,并且此交换机是 topic类型的。因为指定了,所以会创建 test.exchange 这个交换机。
@RestController public class PublishController { @Resource StreamBridge bridge; //通过bridge来发送消息 @RequestMapping("/publish") public String publish(){ //第一个参数其实就是 RabbitMQ 的交换机名称(数据会发送给这个交换机) //这个交换机的命名稍微有一些规则: //输入: <名称> + -in- + <index> //输出: <名称> + -out- + <index> //这里使用输出的方式,来将数据发送到消息队列,注意这里的名称会和之后的消费者Bean名称进行对应 bridge.send("test-out-0", "HelloWorld!"); return "消息发送成功!"+new Date(); } }
2.5.3、消费者
2.5.3.1、配置文件
server: port: 8002 spring: cloud: stream: binders: #此处配置要绑定的rabbitmq的服务信息 local-server: #绑定名称,随便起一个就行 type: rabbit #消息组件类型,这里使用的是RabbitMQ,就填写rabbit environment: #服务器相关信息,按照下面的方式填写就行,爆红别管 spring: rabbitmq: host: 192.168.0.6 port: 5672 username: admin password: admin virtual-host: /test bindings: # 消费者是输入,即从消息队列输入到程序。 # 默认名称为 方法名-in-index,这里我们将其指定为我们刚刚定义的交换机 test-in-0: destination: test.exchange
2.5.3.2、进行消费
@Component public class ConsumerComponent { @Bean("test") //注意这里需要填写我们前面交换机名称中"名称",这样生产者发送的数据才会正确到达 public Consumer<String> consumer(){ return System.out::println; } }