文章目录
Spring Cloud(13)——Spring Cloud Stream 消息驱动
1、Stream概述
Spring Cloud Stream 是一个用来为微服务应用构建消息驱动能力的框架。它可以基于 Spring Boot 来创建独立的、可用于生产的 Spring 应用程序。Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,并引入了发布-订阅、消费组、分区这三个核心概念。
通过使用 Spring Cloud Stream,可以有效简化开发人员对消息中间件的使用复杂度,让系统开发人员可以有更多的精力关注于核心业务逻辑的处理。但是目前 Spring Cloud Stream 只支持 RabbitMQ 和 Kafka 的自动化配置。
消息的中间件有(RabbitMQ, Kafka, ActiveMQ,RocketMQ).本文以RabbitMQ作为中间件。
Spring Cloud Stream 官网:https://spring.io/projects/spring-cloud-stream
Spring Cloud Stream 中文文档:https://m.wang1314.com/doc/webapp/topic/20971999.html
Stream 的设计思想
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各种消息中间件构建的初衷不同,他们的实现细节上会有较大的差异性,通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至与动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
Stream中的消息通信方式遵循发布-订阅模式。
Spring Cloud Stream 标准流程套路
Binder:很方便的连接中间件
Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
Source 和 Sink:参照Stream,发布消息就是输出,接收消息就是输入
2、Stream 编码API和常用注解
3、消息驱动之生产者
1、创建cloud-stream-rabbitmq-provider8801模块
2、导入pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</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-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3、编写yml配置文件
server:
port: 8801
spring:
application:
name: cloud-stream-provider
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
cloud:
stream:
binders: #配置要绑定的rabbitmq的服务信息
defaultRabbit:
type: rabbit #消息组件类型
bindings:
output:
destination: studyExchange
content-type: application/json #设置消息类型,本次为json,文本类型则设置text/plain
binder: {defaultRabbit}
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #设置心跳时间间隔
lease-expiration-duration-in-seconds: 5 # 最大间隔时间
instance-id: send-8801.com #自定义主机名称
prefer-ip-address: true # 显示IP
4、创建主启动类
@SpringBootApplication
@EnableEurekaClient
public class StreamMQProvider8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQProvider8801.class,args);
}
}
5、编写业务
-
编写业务类接口
public interface IMessageProvider { public String send(); }
2.接口实现类 ,定义消息的推送管道
package com.cheng.springcloud.service.impl;
import com.cheng.springcloud.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.UUID;
@EnableBinding(Source.class) //定义消息的推送管道
public class IMessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; //消息发送通道,队列Queue的一种抽象
@Override
public String send() {
String s = UUID.randomUUID().toString();
output.send(MessageBuilder
.withPayload(s)
.build());
System.out.println(s);
return null;
}
}
- 编写controller
@RestController
public class IMessageController {
@Resource
private IMessageProvider iMessageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage(){
return iMessageProvider.send();
}
}
6、测试
-
启动cloud-eureka-server7001模块
-
启动RabbitMQ 访问请求;http://localhost:15672
-
启动cloud-stream-rabbitmq-provider8801
-
访问请求:http://localhost:8801/sendMessage,查看后台输出
- 查看rabbitmq上面的流量波峰的变动:
消息驱动之生产者搭建成功!
4、消息驱动之消费者
1、创建cloud-stream-rabbitmq-consumer8802模块
2、导入pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3、编写yml配置文件
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
cloud:
stream:
binders: #配置要绑定的rabbitmq的服务信息
defaultRabbit:
type: rabbit #消息组件类型
bindings:
input: #接收消息通道
destination: studyExchange #要使用交换机的名称
content-type: application/json #设置消息类型,本次为json,文本类型则设置text/plain
binder: {defaultRabbit}
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #设置心跳时间间隔
lease-expiration-duration-in-seconds: 5 # 最大间隔时间
instance-id: receive-8802.com #自定义主机名称
prefer-ip-address: true # 显示IP
4、创建主启动类
@SpringBootApplication
@EnableEurekaClient
public class StreamMQConsumer8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQConsumer8802.class,args);
}
}
5、编写业务类
package com.cheng.springcloud.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;
@Component
@EnableBinding(Sink.class)
public class IMessageConsumerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message){
System.out.println("消息接受者1号,------》接收到的消息"+message.getPayload()+"\t port: "+serverPort);
}
}
6、测试
启动cloud-stream-rabbitmq-consumer8802模块
访问http://localhost:7001/,可以看到8801和8802两个模块都注册进注册中心了
现在用8801消息生产者发送消息,测试8802消息消费者是否能接受到:
执行请求:http://localhost:8801/sendMessage
8801消息生产者成功发送消息:
8802消息消费者成功接受到8801发送的消息:
5、分组消费与持久化
依据cloud-stream-rabbitmq-consumer8802模块,clone一个cloud-stream-rabbitmq-consumer8803模块
启动运行:存在两个问题
- 消息消费者8802和8803同时接受到消息,存在重复消费问题
- 消息持久化问题
如何解决:分组(group)
在Stream中,出于同一个group中的多个消费者是竞争关系,不同组是可以重复消费的,同一组会发生竞争关系,只有一个可以消费。
自定义配置分组
将消费者8802和8803模块变成不同组
在消费者8802模块的yml中配置group属性:
input: #接收消息通道
destination: studyExchange #要使用交换机的名称
content-type: application/json #设置消息类型,本次为json,文本类型则设置text/plain
binder: {defaultRabbit}
group: wanliA
同样,也在消费者8803模块的yml中配置group属性:
input: #接收消息通道
destination: studyExchange #要使用交换机的名称
content-type: application/json #设置消息类型,本次为json,文本类型则设置text/plain
binder: {defaultRabbit}
group: wanliB
查看RabbitMQ中的Exchanges配置,
自定义分组成功!
有上面可知,给group分配不同的值,即可分为不同消费组。那要想把多个微服务实例分为同一组,只需给group分配相同的值。
把消费者8802和8803模块yml配置文件中group的值都设为 wanli
再启动运行:
执行请求:http://localhost:8801/sendMessage 两次,即发送两条消息
发送成功,
查看消息消费者8802和8803模块接收的情况:
消费者8802
消费者8803
实现了轮流接收消息,避免了重复消费。
持久化
消息的持久化:如果给消息消费者配置相应的分组,那么即使当消息消费者宕机时,消息提供者发送消息,在消息消费者启动应用成功之后也能接收到消息提供者发来的消息。
测试:
-
关闭消费者8802和8803模块
-
去掉消费者8802里的group消费组属性,保留8003模块的group消费组属性
-
执行请求:http://localhost:8801/sendMessage 4次,给RabbitMQ发送四条消息
发送成功:
-
启动消费者8802和8803模块,查看两个服务的接受情况:
消费者8802:
没有接收到任何消息
消费者8803:全部接收
如果给消息消费者配置相应的分组,那么即使当消息消费者宕机时,消息提供者发送消息,在消息消费者启动应用成功之后也能接收到消息提供者发来的消息。