SpringCloud-Stream
什么是SpringCloudStream
概述
Spring Cloud Stream
是一个框架,用于构建与共享消息系统连接的高度可扩展的事件驱动微服务。就是一种消息驱动框架。目前只支持RabbitMQ
和Kafka
主要解决哪些问题?
对于一个平台,可能使用了多种消息中间件,比如同时使用了RabbitMQ
和Kafka
等消息中间键,对于程序员,可能不能学到所有的消息中间件。于是Spring Cloud Stream产生了,它的作用就是屏蔽底层MQ的差异,将MQ的操作封装成一个模型,我们只需要对这个模型进行操作,不用管底层的多种MQ之间如何工作,类似与JDBC技术,解决了多种数据库之间的差异性。
中文参考手册:https://blog.csdn.net/qq_32734365/article/details/81413218
工作方式
应用程序只需要通过 inputs和 outputs来与SpringCloud Stream 中的Binder经行交互,也就是说我们只需要配置Bingding(绑定),而Spring Cloud Stream的 Binder对象负责与消息中间件经行交互,所以我们只需要知道如何与Spring Cloud Stream交互,就能操作各种不同的消息中间件了。Spring Integration为消息代理提供链接。
原理图:
在微服务与微服务之间,我们只需要绑定好Binder
,我们需要接收消息就使用inputs 需要发送消息就使用outputs,具体在消息中间件中如何交互,我们是无需关心的。
开发实际运行原理图:
Binder:很方便的连接中间件,屏蔽差异
Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过对Channel对队列进行配置
Source和Sink:简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入
编码API和常用注解
SpringCloudStream使用
- 父项目搭建,参考:https://www.cnblogs.com/Rampant/p/14770855.html
- 准备RabbitMQ环境,参考:https://dg.
消息生产者搭建
-
创建项目:cloud-stream-rabittmq-provider-5000
-
导入pom.xml
<dependencies> <!-- <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> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> <!--连接 consul 的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <!--spring-boot-web 模块 常用的3个--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--热部署插件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <!--测试插件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-test</artifactId> <scope>test</scope> </dependency> <!--lombok 依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
-
application.yaml
server: port: 5000 spring: application: name: cloud-stream-provider rabbitmq: host: 47.97.218.81 port: 5672 username: admin password: 970699 cloud: stream: bindings: # 服务的整合处理 output: # 这个名字是一个通道的名称 destination: stream.message # 表示要使用的Exchange名称定义 consul: # 服务的主机 host: localhost # 服务的端口号 port: 8500 discovery: # 注册到服务中心的名称 service-name: ${spring.application.name}
-
主启动类
package com.wyx.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class StreamRabbitMQMain5000 { public static void main(String[] args) { SpringApplication.run(StreamRabbitMQMain5000.class,args); } }
-
业务类
package com.wyx.cloud.service; public interface IMessageProvider { public String send(); }
package com.wyx.cloud.service; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.messaging.Source; import org.springframework.context.annotation.Bean; import org.springframework.integration.support.MessageBuilder; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.UUID; import java.util.function.Function; @Component @EnableBinding(Source.class) public class IMessageProviderImpl implements IMessageProvider{ @Resource Source source; @Override public String send() { String serial = UUID.randomUUID().toString(); source.output().send(MessageBuilder.withPayload(serial).build()); return null; } }
package com.wyx.cloud.controller; import com.wyx.cloud.service.IMessageProvider; import lombok.extern.log4j.Log4j2; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController @Log4j2 public class SendMessageController { @Resource private IMessageProvider messageProvider; @GetMapping("/send") public String sendMessage(){ log.info("开始发送消息"); messageProvider.send(); return null; } }
消息消费者搭建
-
创建模块 cloud-stream-rabbitmq-consumer-5001
-
pom.xml
<dependencies> <!-- <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> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> <!--连接 consul 的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <!--spring-boot-web 模块 常用的3个--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--热部署插件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <!--测试插件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-test</artifactId> <scope>test</scope> </dependency> <!--lombok 依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
-
application.yaml
server: port: 5001 spring: application: name: cloud-stream-consumer rabbitmq: host: 47.97.218.81 port: 5672 username: admin password: 970699 cloud: stream: bindings: # 服务的整合处理 input: # 这个名字是一个通道的名称 destination: stream.message # 表示要使用的Exchange名称定义 consul: # 服务的主机 host: localhost # 服务的端口号 port: 8500 discovery: # 注册到服务中心的名称 service-name: ${spring.application.name}
-
主启动类
package com.wyx.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class StreamMQConsumerMain5001 { public static void main(String[] args) { SpringApplication.run(StreamMQConsumerMain5001.class,args); } }
-
业务类
package com.wyx.cloud.service; 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 GetMessage { @StreamListener(Sink.INPUT) public void getMessage(Message message){ System.out.println(message); } }
启动consul 在启动两个服务,访问 http://localhost:5000/send 发送消息,查看5001端口控制台即可
自定义信道
参考Source.java,和 Sink.java创建自定义通道
-
MySource.java
package com.wyx.cloud.channer; 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.wyx.cloud.channer; 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(); }
-
修改application.yaml
spring: cloud: stream: bindings: # 服务的整合处理 my_input: # 这里表示信道名称,和我们自定义的信道名字绑定 destination: my.message # 表示要使用的Exchange名称定义
spring: cloud: stream: bindings: # 服务的整合处理 my_output: # 这个名字是一个通道的名称 destination: my.message # 表示要使用的Exchange名称定义
启动测试,效果依然可以。
配置优化
默认情况下,如果没有配置
spring:
cloud:
stream:
bindings: # 服务的整合处理
my_input: # 这里表示信道名称,和我们自定义的信道名字绑定
destination: my.message # 表示要使用的Exchange名称定义
则他默认使用信道的名称作为交换机名称。即我们可以如下优化
-
删除application.yaml中的如下配置
spring: cloud: stream: bindings: # 服务的整合处理 my_input: # 这里表示信道名称,和我们自定义的信道名字绑定 destination: my.message # 表示要使用的Exchange名称定义
spring: cloud: stream: bindings: # 服务的整合处理 my_output: # 这个名字是一个通道的名称 destination: my.message # 表示要使用的Exchange名称定义
-
修改自定义的信道的信道名称,如下
package com.wyx.cloud.channer; import org.springframework.cloud.stream.annotation.Output; import org.springframework.messaging.MessageChannel; public interface MySource { String DEFAULT = "default.exchange"; @Output(DEFAULT) MessageChannel myOutput(); }
package com.wyx.cloud.channer; import org.springframework.cloud.stream.annotation.Input; import org.springframework.messaging.SubscribableChannel; public interface MySink { String DEFAULT = "default.exchange"; @Input(DEFAULT) SubscribableChannel myInput(); }
-
修改监听端口的service
package com.wyx.cloud.service; import com.wyx.cloud.channer.MySink; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.annotation.StreamListener; import org.springframework.messaging.Message; import org.springframework.stereotype.Component; @Component @EnableBinding(MySink.class) public class GetMessage { @StreamListener(MySink.DEFAULT)//x public void getMessage(Message message){ System.out.println(message); } }
消息分组
什么是消息分组,对于一个高可用的系统,微服务集群是必须的存在,但是当我们使用上面的方式将消费者微服务集群后,那么当我们在向交换机写信息时,同时两个微服务简单一个交换机,那么消息会在两个微服务上都消费,这样会破坏系统的容错性。于是为了解决这个问题,我们可以对消息进行分组。
还有一个特点:加了分组之后,如果消息消费服务突然宕机,那么这时候如果消息生产者继续生产消息,当消息消费者恢复后能继续消费未消费的消息,也就是消息具有持久化功能
环境准备
:在搭建一个消息消费者cloud-stream-rabbitmq-consumer-5002具体参考上面消息消费者搭建,修改端口即可。将5001还原成5002的样子,5000还原最初状态
修改5000的业务类的IMessageProviderImpl.java
package com.wyx.cloud.service;
import com.wyx.cloud.channer.MySource;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.UUID;
@Component
//@EnableBinding(MySource.class)
@EnableBinding(Source.class)
public class IMessageProviderImpl implements IMessageProvider{
// @Resource
// MySource source;
@Resource
Source source;
@Override
public String send() {
String serial = UUID.randomUUID().toString();
// source.myOutput().send(MessageBuilder.withPayload(serial).build());
for (int i = 0; i < 10; i++) {
source.output().send(MessageBuilder.withPayload(serial).build());
}
return null;
}
}
分区步骤:
-
修改 5001和5002的配置文件application.yaml
spring: cloud: stream: bindings: # 服务的整合处理 input: # 这个名字是一个通道的名称 destination: stream.message # 表示要使用的Exchange名称定义 group: group #添加一个分组,配置即可实现消息不重复消费。
消息分区
在上面的消息分组中,他解决的消息重复消费的问题,但是对于有一些特殊消息,他存在一定级联关系,发送者发送的消息(一次性发送进队列),只能由一个微服务来处理,那么我们可以使用消息分区来进行处理。
消息分区配置:
消息生产者:
spring:
cloud:
stream:
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: stream.message # 表示要使用的Exchange名称定义
producer: # 添加消息分区
partition-key-expression: payload # 配置分区规则,还有一种设置head头的,利用MessageBuilder.withPayload(serial).setHeader()来配置
partition-count: 2 # 分区数量,即几个微服务
消息消费者
spring:
cloud:
stream:
instance-count: 2 # 消费者总数
instance-index: 0 #当前消费者索引,重0开始,根据不同的端口切换
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: stream.message # 表示要使用的Exchange名称定义
group: group
consumer: # 开启分区支持
partitioned: true