Spring Cloud Stream消息驱动
一、简介
Spring Cloud Stream中文指导手册
官方定义Spring Cloud Stream是一个构建消息驱动微服务的框架。 应用程序通过inputs或者outputs与Spring Cloud Stream中binder对象交互,通过我们配置来binding(绑定) 。而Spring Cloud Stream的binder对象负责与消息中间件交互。所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引了发布-订阅、消费组、分区的三个核心概念。目前仅支持RabbitMQ、Kafka.
总而言之,就是屏蔽底层消息中间件的差异,降低切换版本,统一消息的编程模型。
为什么用Spring Cloud Stream:
比如说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange, kafka有 Topic和Partitions分区,这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰, 我们如果用了两个消息队列的其中一种, 后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的, 一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候spring cloud Stream给我们提供了一种解耦合的方式。
二、设计思想
2.1 标准MQ架构
- 生产者/消费者之间靠消息媒介传递信息内容—Message
- 消息必须走特定的通道—消息通道MessageChannel
- 消息通道MessageChannel的子接口SubscribableChannel,负责收发处理消息,由MessageHandler消息处理器订阅消费
2.2 使用Spring Cloud Stream
在没有绑定器这个概念的情况下,我们的Spring Boot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道, 使得应用程序不需要再考虑各种不同的消息中间件实现。Stream对消息中间件的进一步封装可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
通过定义绑定器Binden作为中间层,实现了应用程序与消息中间件细节之间的隔离。
- INPUT对应于消费者
- OUTPUT对应于生产者
- Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播,在RabbitMQ就是Exchange,在kafka中就是Topic
2.3 Spring Cloud Stream使用标准流程套路
- Binder: 很方便的连接中间件,屏蔽差异
- Channel: 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过对Channel对队列进行配置
- Source和Sink: 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入
2.4 编码API和常用注解
三、案例测试
首先先安装配置好RabbitMQ环境:详情
3.1 消息驱动之生产者
3.1.1 新建Module
cloud-stream-rabbitmq-provider8801
3.1.2 pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.cn.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.1.3 yml
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.177.136
port: 5672
username: qmy
password: tiger
virtual-host: /
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
#这一段可以不配置,我这里是为了解决RabbitMQ安装在Linux上,一个无关紧要的报错:连接错误。因为他默认会尝试连接localhost:5672 ,但Linux上面连接是成功的
rabbitmq:
host: 192.168.177.136
port: 5672
username: qmy
password: tiger
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: send-8801.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
3.1.4 主启动类StreamMQMain8801
@SpringBootApplication
public class StresmMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StresmMQMain8801.class,args);
}
}
3.1.5 发送消息接口及实现类
public interface IMessageProviderService {
public String send();
}
import com.cn.springcloud.service.IMessageProviderService;
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 javax.annotation.Resource;
import java.util.UUID;
@EnableBinding(Source.class) //定义消息的推送管道
public class IMessageProviderServiceImpl implements IMessageProviderService {
@Resource
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;
}
}
3.1.6 controller
import com.cn.springcloud.service.IMessageProviderService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class SendMessageController {
@Resource
private IMessageProviderService iMessageProviderServiceImpl;
@RequestMapping("/sendMessage")
public String sendMessage(){
return iMessageProviderServiceImpl.send();
}
}
3.2 消息驱动之消费者
3.2.1 新建Module
cloud-stream-rabbitmq-consumer8802
3.2.2 POM
此案例中和上面生产者一样即可
3.2.3 YML
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.177.136
port: 5672
username: qmy
password: tiger
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
group: groupA
#这个配置原因见上一个yml文件
rabbitmq:
host: 192.168.177.136
port: 5672
username: qmy
password: tiger
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: receive-8802.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
3.2.4 主启动类StreamMQMain8802
@SpringBootApplication
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class,args);
}
}
3.2.5 业务类
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;
import org.springframework.stereotype.Component;
@Component
@EnableBinding(Sink.class) //定义消息的获取管道
public class ReceiveMessageListenerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消费者1号,接受:"+message.getPayload()+"\t port:"+serverPort);
}
}
3.3 测试
测试8801发送8802接收消息,访问:
http://localhost:8801/sendMessage
在控制台看到生产者8801,发出消息,消费者8802,接受到消息即可。
3.4 分组消费与持久化
新建一个和消费者8802一样的项目,来消费生产者,端口为8803
运行后,同样8801发送消息,发现8802和8803均接受到消息。即两个一样的消费者重复消费的问题。
实际生产中相当于:比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中的消息分组来解决。
所以同一个分组就要保证不能被重复消费。即在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。不同组是可以全面消费的(重复消费),同一组内会发生竞争关系,只有其中一个可以消费。
分布式微服务应用为了实现高可用和负载均衡,实际上都会部署多个实例,本例启动了两个消费微服务(8802/8803),多数情况,产发送消息给某个具体微服务时只希望被消费一次, 按照上面我们启动两个应用的例子,虽然它们同属一个应用,但是这个消息出现了被重复消费两次的情况。为了解决这个问题,在Spring Cloud Stream中提供了消费组的概念。
3.4.1 解决原理
微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。
3.4.2 解决重复消费问题
只需要将8802和8803两个消费者放在同一组即可,即修改两个项目的yml
设置为同一个分组名即可。
3.4.2 解决持久化问题
通过上述测试,解决了重复消费问题,再看看持久化,停止8802/8803并去除掉8802的分组group:groupA,8803的分组group:groupA没有去掉。8801先发送4条信息到rabbitmq,先启动8802,无分组属性配置,后台没有打出来消息,再启动8803,有分组属性配置,后台打出来了四条MQ上的消息。即配置了group: 属性后当消费者停机在启动后任然能从 stream 获取生产者已发送的消息,即不会错过或丢失任何消息,但是不配置group属性就收不到了,这就是消息的持久化。