消息中间件的几大应用场景
应用解耦
假设公司有几个不同的系统,各系统在某些业务有联动关系,比如A系统完成了某些操作,需要触发B系统及C系统,但是各个系统之间产生了耦合。针对这种场景,用消息中间件就可以完成解耦,当A系统完成操作时将数据放进消息队列,B和C系统去订阅消息就可以了,这样各系统只要约定好消息的格式就可以了。
异步处理
比如用户在电商网站下单,下单完成后会给用户推送短信或邮件,发短信和邮件的过程就可以异步完成。因为下单付款才是核心业务,发邮件和短信并不属于核心功能,且可能耗时较长,所以针对这种业务场景可以选择先放到消息队列中,由其他服务来异步处理。
流量削峰
比如秒杀活动,一下子进来好多请求,有的服务可能承受不住瞬时高并发而崩溃,针对这种场景,在中间加一层消息队列,把请求先放入到队列中,然后再把队列中的请求平滑的推送给服务,或者让服务去队列拉取。
日志处理
对于小型项目来说,我们通常对日志的处理没有那么多的要求,但是当用户量、数据量达到一定的峰值后,问题就会随之而来,比如:
- 用户日志怎么存放
- 用户日志存放后怎么利用
- 怎么在存储大量日志而不对系统造成影响
等很多其它的问题,这样我们就需要借助消息队列进行业务上的解耦,数据上更好的传输。
总结
消息队列,是分布式系统中重要的组件,其通用的使用场景可以简单的描述为:当不需要立即获得结果,但是并发量又需要进行控制的时候,差不多就是需要使用消息队列的时候。在项目中,将一些无需及时返回且耗时的操作提取出来,进行了异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。
当遇到上面几种情况的时候,就要考虑使用消息队列了。如果你碰巧使用的是Kafka或RabbitMQ,而且同样也在使用SpringCloud,那你可以考虑使用SpringCloud Stream。
什么是SpringCloud Stream
SpringCloud Stream是用于构建消息驱动微服务应用程序的框架。该框架提供了一个灵活的编程模型,该模型建立在已经熟悉Spring习惯用法的基础上,它提供了来自多家供应商中间件的合理配置,包括publish-subscribe,消息分组和消息消息区分处理的支持。
SpringCloud Stream解决了开发人员无感知的使用消息中间件的问题,因为Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件,使得微服务开发的高度解耦,服务可以更多的关注自己的业务流程。
核心概念
组成 | 说明 |
---|---|
Middleware | 中间件,支持RabbitMQ和Kafka |
Binder | 目标绑定器,目标指的是Kafka还是RabbitMQ。绑定器就是封装了目标中间件的包。如果操作的是Kafka就使用spring-cloud-stream-binder-kafka,如果操作的是RabbitMQ就使用spring-cloud-stream-binder-rabbit |
@Input | 注解标识输入通道,接收(消息消费者)的消息将通过该通道进入应用程序。 |
@Output | 注解标识输出通道,发布(消息生产者)的消息将通过该通道离开应用程序。 |
@StreamListener | 监听队列,消费者队列的消息接收 |
@EnableBinding | 注解标识绑定,将信道channel和交换机exchange绑定在一起。 |
工作原理
通过定义绑定器作为中间层,实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件的实现。当需要升级消息中间件,或者是更换其它消息中间件时,我们需要做的就是更换对应的Binder绑定器而不需要修改任何应用逻辑。
该模型图中有如下几个核心概念:
- Source:当需要发送消息时,我们就需要通过Source.java,它会把我们所要发送的消息进行序列化(默认转换成JSON格式字符串),然后将这些数据发送到Channel中。
- Sink:当我们需要监听消息时就需要通过Sink.java,它会负责从消息通道中获取消息,并将消息反序列化成消息对象,然后交给具体的消息监听处理
- Channel:通常我们向消息中间件发送消息或者监听消息时需要指定主题(Topic)和消息队列名称,一旦我们需要变更主题的时候就需要修改消息发送或消息监听的代码。通过Channel对象,我们的业务代码只需要对应的Channel就可以了。具体这个Channel对应的是哪个主题,可以在配置文件中来指定,这样当主题变更的时候我们就不用对代码做任何修改,从而实现了与具体消息中间件的解耦。
- Binder:通过不同的Binder可以实现不同的消息中间件整合,Binder提供统一的消息收发接口,从而使得我们可以根据实际需要部署不同的消息中间件,或者根据实际生产中所部署的消息中间件来调整我们的配置
环境准备
创建stream-demo父工程
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>stream-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>stream-producer</module>
</modules>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.cloud.alibaba.version>2.2.7.RELEASE</spring.cloud.alibaba.version>
<spring.boot.version>2.3.12.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR12</spring.cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!--SpringCloudAlibaba的版本管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringBoot的版本管理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringCloud的版本管理-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
在stream-demo下创建stream-producer工程
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>stream-demo</artifactId>
<groupId>com.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>stream-producer</artifactId>
<version>1.0-SNAPSHOT</version>
<name>stream-producer</name>
<dependencies>
<!--Nacos Client-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloudStream与rabbit的binder-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
</dependencies>
</project>
编写配置文件
server:
port: 8001
#应用名称
spring:
application:
name: stream-producer
#Rabbitmq
rabbitmq:
host: #服务器IP
port: #服务器端口号
username: #用户名
password: #密码
virtual-host: #虚拟主机地址
cloud:
#注册中心
nacos:
discovery:
#nacos注册中心地址,默认端口号:8848
server-addr: 127.0.0.1:8848
#nacos管理平台账号,默认nacos
username: nacos
#nacos管理平台密码,默认nacos
password: nacos
#命令空间,默认public
namespace: public
#SpringCloudStream
stream:
bindings:
#消息发送通知
#与org.springframework.cloud.stream.messaging.Source中的 @Output("output")注解的value相同
output:
destination: stream.message #绑定的交换机名称
编写发送消息工具类
package com.example.producer;
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.support.MessageBuilder;
import org.springframework.stereotype.Component;
/**
* 消息生产者
*/
@Component
@EnableBinding(Source.class)
public class MessageProducer {
@Autowired
private Source source;
/**
* 发送消息
* @param message
*/
public void send(String message){
source.output().send(MessageBuilder.withPayload(message).build());
}
}
在stream-demo下创建stream-consumer工程
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>stream-demo</artifactId>
<groupId>com.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.example</groupId>
<artifactId>stream-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>stream-consumer</name>
<dependencies>
<!--Nacos Client-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloudStream与rabbit的binder-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
</dependencies>
</project>
编写配置文件
server:
port: 8002
#应用名称
spring:
application:
name: stream-consumer
#Rabbitmq
rabbitmq:
host: #服务器IP
port: #服务器端口号
username: #用户名
password: #密码
virtual-host: #虚拟主机地址
cloud:
#注册中心
nacos:
discovery:
#nacos注册中心地址,默认端口号:8848
server-addr: 127.0.0.1:8848
#nacos管理平台账号,默认nacos
username: nacos
#nacos管理平台密码,默认nacos
password: nacos
#命令空间,默认public
namespace: public
#SpringCloudStream
stream:
bindings:
#消息接收通道
#与org.springframework.cloud.stream.messaging.Sink中的 @Input("input")注解的value相同
input:
destination: stream.message #绑定的交换机名称
编写消息消费工具类
package com.example.consumer;
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;
/**
* 消息消费者
*/
@Component
@EnableBinding(Sink.class)
public class MessageConsumer {
/**
* 接收消息
* @param message
*/
@StreamListener(Sink.INPUT)
public void receive(String message){
System.out.println("message="+message);
}
}
自定义消息通道
参考源码Source.java 和Sink.java创建自定义消息通道。
创建MySource.java
package com.example.channel;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
public interface MySource {
String MY_OUTPUT = "my_output";
@Output(MY_OUTPUT)
MessageChannel myOutput();
}
创建MySink.java
package com.example.channel;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface MySink {
String MY_INPUT = "my_input";
@Input(MY_INPUT)
SubscribableChannel myInput();
}
修改配置文件
server:
port: 8001
#应用名称
spring:
application:
name: stream-producer
#Rabbitmq
rabbitmq:
host: #服务器IP
port: #服务器端口号
username: #用户名
password: #密码
virtual-host: #虚拟主机地址
cloud:
#注册中心
nacos:
discovery:
#nacos注册中心地址,默认端口号:8848
server-addr: 127.0.0.1:8848
#nacos管理平台账号,默认nacos
username: nacos
#nacos管理平台密码,默认nacos
password: nacos
#命令空间,默认public
namespace: public
#SpringCloudStream
stream:
bindings:
#消息发送通知
#与org.springframework.cloud.stream.messaging.Source中的 @Output("output")注解的value相同
output:
destination: stream.message #绑定的交换机名称
my_output:
destination: my.message #绑定的交换机名称
server:
port: 8002
#应用名称
spring:
application:
name: stream-consumer
#Rabbitmq
rabbitmq:
host: #服务器IP
port: #服务器端口号
username: #用户名
password: #密码
virtual-host: #虚拟主机地址
cloud:
#注册中心
nacos:
discovery:
#nacos注册中心地址,默认端口号:8848
server-addr: 127.0.0.1:8848
#nacos管理平台账号,默认nacos
username: nacos
#nacos管理平台密码,默认nacos
password: nacos
#命令空间,默认public
namespace: public
#SpringCloudStream
stream:
bindings:
#消息接收通道
#与org.springframework.cloud.stream.messaging.Sink中的 @Input("input")注解的value相同
input:
destination: stream.message #绑定的交换机名称
my_input:
destination: my.message #绑定的交换机名称
编写消息发送工具类
package com.example.producer;
import com.example.channel.MySource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
@Component
@EnableBinding(MySource.class)
public class MessageMyProducer {
@Autowired
private MySource mySource;
public void send(String message){
mySource.myOutput().send(MessageBuilder.withPayload(message).build());
}
}
编写消息消费工具类
package com.example.consumer;
import com.example.channel.MySink;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Component;
/**
* 消息消费者
*/
@Component
@EnableBinding(MySink.class)
public class MessageMyConsumer {
/**
* 接收消息
* @param message
*/
@StreamListener(MySink.MY_INPUT)
public void receive(String message){
System.out.println("message="+message);
}
}
配置优化
SpringCloud微服务开发之所以简单,除了官方做了许多彻底的封装之外还有一个优点就是约定大于配置。开发人员仅需规定应用中不符约定的部分,在没有规定配置的地方采用默认配置,以力求最简配置为核心思想。
简单理解就是:Spring遵循了推荐默认配置的思想,当存在特殊需求的时候,自定义配置即可,否则无需配置。
在SpringCloudStream中,@Output("output")和@Input("input")注解的value默认即为绑定的交换机名称,所以自定义消息通道的案例我们就可以重构为以下方式。
创建MySource02.java
package com.example.channel;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
public interface MySource02 {
//这就是交换机的名称
String DEFAULT = "default.message";
@Output(DEFAULT)
MessageChannel output();
}
创建MySink02.java
package com.example.channel;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface MySink02 {
String DEFAULT = "default.messagge";
@Input(DEFAULT)
SubscribableChannel input();
}
创建MessageMyProducer02.java
package com.example.producer;
import com.example.channel.MySource02;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
@Component
@EnableBinding(MySource02.class)
public class MessageMyProducer02 {
@Autowired
private MySource02 mySource02;
public void send(String message){
mySource02.output().send(MessageBuilder.withPayload(message).build());
}
}
创建MessageMyConsumer02.java
package com.example.consumer;
import com.example.channel.MySink02;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Component;
/**
* 消息消费者
*/
@Component
@EnableBinding(MySink02.class)
public class MessageMyConsumer02 {
/**
* 接收消息
* @param message
*/
@StreamListener(MySink02.DEFAULT)
public void receive(String message){
System.out.println("message="+message);
}
}
修改配置文件
server:
port: 8001
#应用名称
spring:
application:
name: stream-producer
#Rabbitmq
rabbitmq:
host: #服务器IP
port: #服务器端口号
username: #用户名
password: #密码
virtual-host: #虚拟主机地址
cloud:
#注册中心
nacos:
discovery:
#nacos注册中心地址,默认端口号:8848
server-addr: 127.0.0.1:8848
#nacos管理平台账号,默认nacos
username: nacos
#nacos管理平台密码,默认nacos
password: nacos
#命令空间,默认public
namespace: public
server:
port: 8002
#应用名称
spring:
application:
name: stream-consumer
#Rabbitmq
rabbitmq:
host: #服务器IP
port: #服务器端口号
username: #用户名
password: #密码
virtual-host: #虚拟主机地址
cloud:
#注册中心
nacos:
discovery:
#nacos注册中心地址,默认端口号:8848
server-addr: 127.0.0.1:8848
#nacos管理平台账号,默认nacos
username: nacos
#nacos管理平台密码,默认nacos
password: nacos
#命令空间,默认public
namespace: public
消息分组
如果有多个消息消费者,那么消息生产者发送的消息会被多个消费者都接收到,这种情况在某些实际场景下是有很大问题的,比如在如下场景中,订单系统做集群部署,都会从RabbitMQ中获取订单信息,如果一个订单消息同时被两个服务消费,系统肯定会出现问题,为了避免这种情况,Stream提供了消息分组来解决该问题。
在Stream中处于同一个group中的多个消费者是竞争关系,能够保证消息只会被其中一个应用消费,不同的组是可以消费的,同一个组会发生竞争关系,只有其中一个可以消费。通过spring.cloud.stream.bindings.<bindingName>.group属性指定组名。
stream:
bindings:
input:
destination: stream.message #绑定的交换机名称
group: group-A #分组
消息分区
通过消息分组可以解决消息被重复消费的问题,但在某些场景下分组还不能满足我们的需求,比如,同时有多条同一个用户的数据发送过来,我们需要根据用户统计,但是消息被分散到了不同的集群节点上,这时我们就可以考虑使用消息分区了。
当生产者将消息发送给多个消费者时,保证同一消息始终由同一个消费者实例接收和处理。消息分区是对消息分组的一种补充。
第一个消息被哪个消费者消费了,后续的消息都会给到这个消费者。
修改消息生产者的配置文件
stream:
bindings:
output:
destination: stream.message #绑定的交换机名称
producer:
partition-key-expression: payload #配置分区键的表达式规则
partition-count: 2 #配置消息分区的数量
修改消费者的配置文件
stream:
instance-count: 2 #消费者总数
instance-index: 0 #当前消费者的索引
bindings:
input:
destination: stream.message #绑定的交换机名称
group: group-A #分组
consumer:
partitioned: true #开启分区支持