SpringCloud Stream消息驱动
一.Stream消息驱动定义
概念
1.应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream 中binder 交互,通过我们配置来 binding ,而 Spring Cloud Stream 的 binder 负责与消息中间件交互。通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。
2. Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。目前仅支持RabbitMQ、Kafka。
3. 简单的讲就是一种生产者,消费者模式。发布者是生产,将输出发布到数据中心,订阅者是消费者,订阅自己感兴趣的数据。当有数据到达数据中心时,就把数据发送给对应的订阅者。
4.Binder可以理解为提供了Middleware操作方法的类。Spring Cloud 提供了Binder抽象接口以及KafKa和Rabbit MQ的Binder的实现。
发布者(生产者)
简单的讲就是一种生产者,消费者模式。发布者是生产,将输出发布到数据中心,订阅者是消费者,订阅自己感兴趣的数据。当有数据到达数据中心时,就把数据发送给对应的订阅者。
订阅者(消费者)
直观的理解就是一群消费者一起处理消息。需要注意的是:每个发送到消费组的数据,仅由消费组中的一个消费者处理。
二.选择SpringCloud Stream消息驱动原因
比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic,partitions分区,这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候springcloud Stream给我们提供了一种解耦合的方式。
Binder: 很方便的连接中间件,屏蔽差异
Channel: 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过对Channel对队列进行配置
Source和Sink: 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入
本例子是使用的RabbitMQ
下载及安装
微服务工程构建(十二)Spring Cloud Bus配合Spring Cloud Config使用可以实现配置的动态刷新(2.自动动态刷新)
开启RabbitMQ
http://localhost:15672/
输入账号密码并登录: guest guest
三. 创建 eureka7001
微服务工程构建(四) eureka集群环境构建 server与server之间相互注册配置 client修改成多个注册
四. 创建module 消息驱动生产者
springcloud-stream-provider8401
1.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.psf.springcloud</groupId>
<artifactId>springcloud-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.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
2.yml
server:
port: 8401
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
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-8401.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
3.启动
@SpringBootApplication
public class StreamProviderMain8401 {
public static void main(String[] args) {
SpringApplication.run(StreamProviderMain8401.class,args);
}
}
4.消息接口
public interface IMessageProviderService {
public String send();
}
5.实现
import com.psf.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 MessageProvider implements IMessageProviderService {
@Resource
private MessageChannel output;
@Override
public String send() {
String random=UUID.randomUUID().toString();
System.out.println("传消息过去"+random);
output.send(MessageBuilder.withPayload("传"+random).build());
return null;
}
}
6.controller
import com.psf.springcloud.service.IMessageProviderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class SendMessageController {
@Resource
private IMessageProviderService messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage() {
return messageProvider.send();
}
}
五. 创建module 消息驱动消费者
springcloud-stream-consumer8405 springcloud-stream-consumer8406
1.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.psf.springcloud</groupId>
<artifactId>springcloud-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.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
2.yml
server:
port: 8405
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
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-8405.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
3.启动
@SpringBootApplication
public class StreamConsumerMain8405 {
public static void main(String[] args) {
SpringApplication.run(StreamConsumerMain8405.class,args);
}
}
4.controller
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;
@EnableBinding(Sink.class)
@Component
public class MessageListener8405Controller {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消费者"+serverPort+"号:"+message.getPayload());
}
}
执行 http://localhost:8401/sendMessage
成功,但是有重新消费问题
六.重复消费问题
微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。
yml添加
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
group: stream8405 #只添加group
当把8405,8405设置为一组
8405/8406实现了轮询分组,每次只有一个消费者 8401模块的发的消息只能被8405或8406其中一个接收到,这样避免了重复消费,同一个组的多个微服务实例,每次只会有一个拿到。
七.消息反馈sendTo使用
8401 修改
yml
server:
port: 8401
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
userOutput: # 这个名字是一个通道的名称
destination: stream # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
logUserInput8405: # 这个名字是一个通道的名称
destination: stream8405 # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
logUserInput8406: # 这个名字是一个通道的名称
destination: stream8406 # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
mqScoreOutput: # 这个名字是一个通道的名称
destination: exchangeName # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
group: mqScore
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-8401.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
controller
@RestController
public class SendsMessageController {
@Resource
private Providers providers;
@Resource
private MySource mySource;
@GetMapping(value = "/sendUser")
public void sendMessage1() {
providers.sendUser();
}
@GetMapping(value = "/sendServer")
public void SendServer() {
providers.send1();
}
/**
* 接收8405
*/
@StreamListener(MySource.LOG_userInput8405)
public void SendServer8405(Message<String> message){
System.out.println("接收成功:"+message.getPayload());
}
/**
* 接收8406
*/
@StreamListener(MySource.LOG_userInput8406)
public void SendServer8406(Message<String> message){
System.out.println("接收成功:"+message.getPayload());
}
}
public interface MySource{
String OUTPUT1 = "userOutput";
@Output(MySource.OUTPUT1)
MessageChannel output1();
//回传 8405
String LOG_userInput8405 = "logUserInput8405";
@Input(MySource.LOG_userInput8405)
SubscribableChannel logUserInput8405();
//回传 8406
String LOG_userInput8406 = "logUserInput8406";
@Input(MySource.LOG_userInput8406)
SubscribableChannel logUserInput8406();
String SCORE_OUPUT = "mqScoreOutput";
@Output(MySource.SCORE_OUPUT)
MessageChannel scoreOutput();
}
8405修改
yml
server:
port: 8405
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
myUserInput: # 这个名字是一个通道的名称
destination: stream # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
logUserOutput8406: # 这个名字是一个通道的名称
destination: stream8406 # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json application/json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
mqScoreInput: # 这个名字是一个通道的名称
destination: exchangeName # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
group: mqScore
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-8405.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
controller
@EnableBinding({MySink.class})
@Component
public class ConsumerController {
@Value("${server.port}")
private String serverPort;
/**
* 接收对象
* @param
*/
@StreamListener(MySink.User_INPUT)
@SendTo(MySink.LOG_userOutput8405)
public String sendUser(User user) {
System.out.println(serverPort+"号 "+user);
User newUser = new User();
newUser.setId(55);
newUser.setName(user.getName());
return "接收到"+serverPort+"的数据"+newUser;
}
@StreamListener(MySink.SCORE_INPUT)
public void SCORE_INPUT(Message<String> message) {
System.out.println(serverPort+"号SCORE_INPUT:"+message.getPayload());
}
}
public interface MySink {
String User_INPUT = "myUserInput";
@Input(MySink.User_INPUT)
SubscribableChannel userInput();
/**
* 接收后转发
*/
String LOG_userOutput8405 = "logUserOutput8405";
@Output(MySink.LOG_userOutput8405)
MessageChannel logUserOutput8405();
String SCORE_INPUT = "mqScoreInput";
@Input(MySink.SCORE_INPUT)
SubscribableChannel scoreInput();
}
8406修改
yml
server:
port: 8406
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
myUserInput: # 这个名字是一个通道的名称
destination: stream # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
logUserOutput8406: # 这个名字是一个通道的名称
destination: stream8406 # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json application/json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
mqScoreInput: # 这个名字是一个通道的名称
destination: exchangeName # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
group: mqScore
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-8406.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
controller
@EnableBinding({MySink.class})
@Component
public class ConsumerController {
@Value("${server.port}")
private String serverPort;
/**
* 接收对象
* @param
*/
@StreamListener(MySink.User_INPUT)
@SendTo(MySink.LOG_userOutput8406)
public String sendUser(User user) {
System.out.println(serverPort+"号 "+user);
User newUser = new User();
newUser.setId(55);
newUser.setName(user.getName());
return "接收到"+serverPort+"的数据"+newUser;
}
@StreamListener(MySink.SCORE_INPUT)
public void SCORE_INPUT(Message<String> message) {
System.out.println(serverPort+"号SCORE_INPUT:"+message.getPayload());
}
}
public interface MySink {
String User_INPUT = "myUserInput";
@Input(MySink.User_INPUT)
SubscribableChannel userInput();
/**
* 接收后转发
*/
String LOG_userOutput8406 = "logUserOutput8406";
@Output(MySink.LOG_userOutput8406)
MessageChannel logUserOutput8406();
String SCORE_INPUT = "mqScoreInput";
@Input(MySink.SCORE_INPUT)
SubscribableChannel scoreInput();
}
注意:如果@StreamListener注解的方法有返回值,那么必须使用@SendTo注解指明返回的值通道
8401
8405
例 回传过程
8401 output mySource.output1().send(MessageBuilder.withPayload(user).build()); 将对象发送去8405
public void sendUser() {
User user = new User();
user.setId(2);
user.setName("zhangsan");
mySource.output1().send(MessageBuilder.withPayload(user).build());
System.out.println("发送消息sendUser");
}
8405用@StreamListener(MySink.User_INPUT)进行接收数据,方法再有返回值的情况下,添加 @SendTo(MySink.LOG_userOutput8405)进行转发(我这里简单做了回传至8401)
@StreamListener(MySink.User_INPUT)
@SendTo(MySink.LOG_userOutput8405)
public String sendUser(User user) {
System.out.println(serverPort+"号INPUT1 "+user);
User newUser = new User();
newUser.setId(55);
newUser.setName(user.getName());
return "接收到"+serverPort+"的数据"+newUser;
}
8401接收回传
/**
* 接收
*/
@StreamListener(MySource.LOG_userInput8405)
public void SendServer8405(Message<String> message){
System.out.println("接收成功:"+message.getPayload());
}
最后:这里的yml配置destination名称 发送与接收一定要相互对应
8401 output(destination=stream)–》8405 接收input(destination=stream)–》8405 回传发送 output(destination=stream8405)–》8401接收 input(destination=stream8405)
八.分组
yml 添加
group: mqScore
8405 8406 设置相同分组后,会防止重复消费,只有一个能接收到数据。
九.持久化
设置8405 不分组,8406 分组,并断开8405,8406服务,执行8401操作
8405,8406启动,只有分组的其中一个能够获取数据
运行 7001-》8401-》8405/8406
十.代码下载
代码下载
其余:
微服务工程构建(十二)Spring Cloud Bus配合Spring Cloud Config使用可以实现配置的动态刷新(2.自动动态刷新)
微服务工程构建(十一)SpringCloud config分布式配置中心(1.半自动)
微服务工程构建(十)spingcloud整合Gateway(网关)
微服务工程构建(九)spingcloud整合eureka、 Hystrix断路器的降级、熔断、图形化
微服务工程构建(八)eureka环境OpenFeign使用
微服务工程构建(四) eureka集群环境构建 server与server之间相互注册配置 client修改成多个注册
END