文章目录
一、初识Stream
SpringCloud Stream是基于SpringBoot构建的,专为构建消息驱动服务所设计的应用框架,它的底层使用Spring Integration来为消息代理层提供网络连接支持。
1、Steam名词说明
- 应用模型:Stream提供了应用模型的抽象,引入了三个角色,分别是输入通道(Input)、输出通道(Output)和通道与底层中间件之间的代理(Binder)
- 适配层抽象:Stream将组件与底层中间件之间的通信过程抽象成了Binder层,使得应用层不需要关心底层中间件是Kafka还是RabbitMQ,只需要关注自身的业务逻辑就好
- 插件式适配层:Binder层采用一种插件形式来提供服务(这和Spring的一贯设计思想一致),开发人员可以很方便的替换适配层或者开发自定义的适配逻辑
- 持久化的发布订阅模型:发布订阅是所有消息组件最核心的功能,在稍后的小节里我们将学习该模式的具体实现方式,以及在业务场景中的应用
- 消费组:Stream允许将多个Consumer加入到一个消费组,如下图所示,4个消费者分别被添加到2个不同的消费组中。消费组的作用是确保一条消息只能被组内的一台实例消费。如下图所示,一个新消息将被两个Group各消费一次
- 分区:Stream支持在多个消费者实例之间创建分区,这样我们通过某些特征量做消息分发,保证相同标识的消息总是能被同一个消费者处理,如下图所示,Partition 1和Partition 2的消息只会被指定的Consumer定向消费
2、Stream体系架构
Stream的体系架构主要包括Input、Output和Binder三个部分
(1)Input通道
也就是输入通道,它的作用是将消息组件中获取到的Message传递给消费者进行消费。在Stream里我们可以借助@Input注解轻松定义一个输入通道
public interface MyTopic {
@Input
SubscribableChannel input();
}
(2)Output通道
Output是输出通道,用来将生产者创建的新消息发送到对应的Topic中去,在Stream中我们可以借助@Output标签定义一个输出通道,@Output和@Input可以放在一个接口中声明
public interface MyTopic {
// 这里可以给Output自定义目标通道名称,比如@Output("myTarget")
@Output
MessageChannel output();
@Input
SubscribableChannel input();
}
(3)Binder
Stream提供了一个Binder抽象层,作为连接外部消息中间件(指RabbitMQ,Kafka)的桥梁,Binder它作为一个适配层,对上层应用程序和底层消息组件之间做了一层屏障,我们的应用程序不用关注底层究竟是使用了Kafka还是RabbitMQ,只管用注解开启响应的消息通道,剩下的事通通交给Binder来搞定。同样,当我们需要替换底层中间件的时候,只要变更Binder的依赖项,然后修改一些配置文件就好了,对我们的应用程序几乎是无感知的。
(4)目的地绑定
通常来说@Input和@Output如果部署在同一个项目中的话,是不能起一样的名字的,否则Spring在启动阶段就会报错了。比如我们定义了@Input(“myTopic”),就不可能再定义一个同样名字的@Output注解。考虑到发布/监听的队列名称默认就是注解里所指定的名字,如果使用了不同的名字,那自然就不会在同一个消息队列中遇到,那么我们如何将作用于同一个Topic的生产者和消费者定义在一个项目中呢?
这就要借助Stream的目的地绑定功能了,看下面一段配置:
spring.cloud.stream.bindings.<通道名>.destination=<主题名>
通过上面这段配置,我们可以将不同的通道绑定到指定的目的地,把这里的<通道名>替换成配置在@Input或@Output中的name,然后再把<主题名>改成同一个Topic,这样一来Stream就将对应的通道绑定到指定的Topic队列上
3、消费组和消息分区
(1)消费组
前面我们接触的都是广播场景,话说这个广播模式简直就是个围观模式,所有订阅相同主题的消费者都眼巴巴看着生产者发布的消息,一个消息在所有节点都要被消费一遍。如果我只想挑一个节点来消费消息,而且又不能只逮着一只羊来薅羊毛,必须利用负载均衡来分发请求。
在Stream里配置一个消费组非常简单
spring.cloud.stream.bindings.group-producer.group=Group-A
(2)消费分区
消费分区消费组,傻傻分不清楚。这两个名字听起来很像,其实并不是一码事,消费组相当于是每组派一个代表去办事儿,而消费分区相当于是专事专办,也就是说,所有消息都会根据分区Key进行划分,带有相同Key的消息只能被同一个消费者处理。
消息分区有一个预定义的分区Key,它是一个SpEL表达式。我们需要在配置文件中指定分区的总个数N,Stream就会为我们创建N个分区,这里面每个分区就是一个Queue(可以在RabbitMQ管理界面中看到所有的分区队列)。
当商品发布的消息被生产者发布时,Stream会计算得出分区Key,从而决定这个消息应该加入到哪个Queue里面。在这个过程中,每个消费组/消费者仅会连接到一个Queue,这个Queue中对应的消息只能被特定的消费组/消费者来处理。
二、Stream实战
1、Stream急速实战Demo
(1)创建一个模块,叫做stream-sample,修改pom文件
<dependencies>
<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>
<!--steam兼容rabbit版本-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
</dependencies>
(2)修改main方法
@SpringBootApplication
public class StreamApplication{
public static void main(String[] args){
SpringApplication.run(StreamApplication.class)
}
}
(3)创建配置文件
spring.application.name=stream-sample
server.port=63000
#RabbitMq连接
spring.rabbitmq.host=localhost
spring.rabbitmq.post=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
management.security.enabled=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
#绑定信道
#spring.cloud.stream.bindings.<通道名>.destination=<主题名>
#绑定Channel到
spring.cloud.stream.bindings.myTopic-consumer.destination=broadcast
spring.cloud.stream.bindings.myTopic-producer.destination=broadcast
(4)创建业务类
1.自定义一个Topic接口
public interface MyTopic{
//消费者和生产者的关联关系在配置文件中绑定的
String INPUT = "myTopic-consumer";
String OUTPUT = "myTopic-producer";
//返回值为“可以被订阅的通道”
//监听一个通道,通道名为TNPUT的值,这个过程是stream代理完成的,会在stream内部和消费者绑定一个关联关系
@Input(INPUT)
SubscribableChannel input();
//消息生产者
@Output(OUTPUT)
MessageChannel output();
}
2.实现对通道的监听
//@EnableBinding注解 会根据自定义的Topic向消息中间件中添加一个主题
//并且将消费者和生产者绑定上这个topic
@EnableBinding({
MyTopic.class
})
public class StreamConsumer{
//这里的监听,是从stream中监听,和rabbitmq已经没有直接关系,rabbitmq的监听是由stream实现的
@StreamListener(MyTopic.TNPUT)
public void consumeMyMessage(Object payload){
System.out.println(payload.toString());
}
}
3.使用一个Controller实现消息生产者
@RestController
public class Controller{
@Autowired
private MyTopic producer;
@PostMapping("send")
public void sendMessage(@RequestParam(value = "body")String body){
producer.output().send(MessageBuilder.withPayload(body).build());
}
}
2、消费组实现Demo
(1)自定义一个GroupTopic接口
public interface GroupTopic{
String INPUT = "group-consumer";
String OUTPUT = "group-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
(2)创建测试方法
@RestController
public class Controller{
@Autowired
private GroupTopic producer;
@PostMapping("sendToGroup")
public void sendMessage(@RequestParam(value = "body")String body){
producer.output().send(MessageBuilder.withPayload(body).build());
}
}
(3)绑定信道
@EnableBinding({
GroupTopic.class
})
public class StreamConsumer{
@StreamListener(GroupTopic.TNPUT)
public void consumeGroupMessage(Object payload){
System.out.println(payload.toString());
}
}
(4)修改配置文件
spring.cloud.stream.bindings.group-consumer.destination=group-topic
spring.cloud.stream.bindings.group-producer.destination=group-topic
#配置消费分组
spring.cloud.stream.bindings.group-consumer.group=Group-A
(5)测试,同时启动2个服务,分组要一样
此时发送消息,同分组下只有会有个服务收到消息,如果有多个分组,那么每个分组下都会有一个服务受到消息
3、消费分区demo
消费组和消费分区实际上只有配置文件不一样
(1)修改配置文件
spring.cloud.stream.bindings.group-consumer.destination=group-topic
spring.cloud.stream.bindings.group-producer.destination=group-topic
#配置消费分组
spring.cloud.stream.bindings.group-consumer.group=Group-A
#打开消费者消费分区的功能
spring.cloud.stream.bindings.group-consumer.consumer.partitioned=true
#表示是2个消费分区
spring.cloud.stream.bindings.group-producer.producer.partitioned-count=2
#只有索引参数为1的节点(消费组),才能消费消息
#spEL
spring.cloud.stream.bindings.group-producer.producer.partitioned-key-expression=1
#当前消费者实例总数
spring.cloud.stream.instanceCound=2
#当前实例的索引号,最大值为instanceCound-1
#当设置了partitioned-key-expression,那么instanceIndex只有与之相等的分区才能消费消息
spring.cloud.stream.instanceIndex=0
4、延迟消息Demo
延迟消息很好理解,就是一种不会立即被消费,而是延迟到未来某个时间点才能被消费的消息类型。
(1)开启延迟消息功能
1)下载插件
打开RabbitMQ官方的插件下载页面https://www.rabbitmq.com/community-plugins.html网址,从中找到rabbitmq_delayed_message_exchange这个插件。
不同版本的RabbitMQ对应不同版本的插件,先看一下你的安装版本,然后选择对应的插件下载。比如我的RabbitMQ版本是3.7.15,所以选择3.7.x版本的插件。
2)安装插件
下载完成后将插件解压,把解压后的文件copy到RabbitMQ安装目录下的plugins文件夹。然后运行rabbitmq-plugins enable rabbitmq_delayed_message_exchange
这个命令安装插件。安装好后你会看到日志中打印出了这个插件的名称
3)重启服务
本地直接执行rabbitmqctl stop命令关闭RabbitMQ(前提是安装路径已经添加到系统变量中,否则要先进入安装目录后再执行该命令)。待完全关闭之后,再执行rabbitmq-server命令启动服务。
对Mac系统来说,添加RabbitMQ到系统变量只要修改~/.bash_profile添加这行就可以了:export PATH=
P
A
T
H
:
{PATH}:
PATH:RABBIT_HOME/sbin。对Windows同理,需要你向系统变量的PATH属性中添加RabbitMQ的路径信息。
(2)延迟消息demo实现
1)创建一个Topic
public interface DelayedTopic{
String INPUT = "delayed-consumer";
String OUTPUT = "delayed-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
2)创建一个消息bean
public class MessageBean{
private String payload;
// getset省略
}
3)创建测试方法
@RestController
public class Controller{
@Autowired
private DelayedTopic producer;
@PostMapping("sendToDelayed")
public void sendToDelayed(@RequestParam(value = "body")String body
,@RequestParam(value = "seconds")Integer seconds){
Message msg = new MessageBean();
msg.setPayload(body);
producer.output()
.send(MessageBuilder.withPayload(msg)
//延迟的时间
.setHeader("x-delay",1000*seconds)
.build());
}
}
4)绑定信道
@EnableBinding({
DelayedTopic.class
})
public class StreamConsumer{
@StreamListener(DelayedTopic.TNPUT)
public void consumeDelayedMessage(Object payload){
System.out.println(payload.toString());
}
}
5)修改配置文件
spring.cloud.stream.bindings.delayed-consumer.destination=delayed-topic
spring.cloud.stream.bindings.delayed-producer.destination=delayed-topic
#声明生产者为延迟消息
spring.cloud.stream.rabbit.bindings.delayed-producer.producer.delayed-exchange=true
5、异常重试消息Demo(本机重试)
1)创建一个Topic
public interface ErrorTopic{
String INPUT = "error-consumer";
String OUTPUT = "error-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
2)创建测试方法
@RestController
public class Controller{
@Autowired
private ErrorTopic producer;
@PostMapping("sendToError")
public void sendMessage(@RequestParam(value = "body")String body){
producer.output().send(MessageBuilder.withPayload(body).build());
}
}
3)绑定信道
@EnableBinding({
ErrorTopic.class
})
public class StreamConsumer{
private AtomicInteger count = new AtomicInteger(1);
@StreamListener(ErrorTopic.TNPUT)
public void consumeErrorMessage(Object payload){
System.out.println("Are you OK?");
//这里抛个错
if(count.incrementAndGet()%3==0){
System.out.println("Fine,thank you.and you?");
count.set(0);
}else{
throw new RuntimeException("I'm not OK");
}
}
}
4)修改配置文件
spring.cloud.stream.bindings.error-consumer.destination=error-topic
spring.cloud.stream.bindings.error-producer.destination=error-topic
#配置最大执行次数(本机重试),在consumer中自己重试,数值要大于1,1就相当于只执行一次,就不会重试
spring.cloud.stream.bindings.error-consumer.consumer.max-attempts=2
6、异常重试消息Demo(联机重试,re-queue(重新入队))
1)创建一个Topic
public interface RequeueTopic{
String INPUT = "requeue-consumer";
String OUTPUT = "requeue-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
2)创建测试方法
@RestController
public class Controller{
@Autowired
private RequeueTopic producer;
@PostMapping("sendToRequeue")
public void sendMessage(@RequestParam(value = "body")String body){
producer.output().send(MessageBuilder.withPayload(body).build());
}
}
3)绑定信道
@EnableBinding({
RequeueTopic.class
})
public class StreamConsumer{
private AtomicInteger count = new AtomicInteger(1);
@StreamListener(RequeueTopic.TNPUT)
public void consumeRequeueMessage(Object payload){
System.out.println("Are you OK?");
try{
Thread.sleep(3000L);
}catch(Exeption e){
}
throw new RuntimeException("I'm not OK");
}
}
4)修改配置文件
spring.cloud.stream.bindings.requeue-consumer.destination=requeue-topic
spring.cloud.stream.bindings.requeue-producer.destination=requeue-topic
#最大执行次数必须设置为1,否则无效
spring.cloud.stream.bindings.requeue-consumer.consumer.max-attempts=1
#仅对 requeue-consumer 开启重新入队的功能
spring.cloud.stream.rabbit.bindings.requeue-consumer.requeueRejected=true
#全局开启重新入队功能
#spring.rabbitmq.listener.default-requeue-rejected=true
7、死信交换机、死信队列Demo
(1)开启死心队列功能
这次不用下载插件了,已经内置好了,我们只要开启就可以了
rabbitmq-plugins enable rabbitmq_shovel
rabbitmq-plugins enable rabbitmq_shovel_management
(2)死信队列Demo
1)创建一个Topic
public interface DlqTopic{
String INPUT = "dlq-consumer";
String OUTPUT = "dlq-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
2)创建测试方法
@RestController
public class Controller{
@Autowired
private DlqTopic producer;
@PostMapping("sendToDlq")
public void sendMessage(@RequestParam(value = "body")String body){
producer.output().send(MessageBuilder.withPayload(body).build());
}
}
3)绑定信道
@EnableBinding({
DlqTopic.class
})
public class StreamConsumer{
private AtomicInteger count = new AtomicInteger(1);
@StreamListener(DlqTopic.TNPUT)
public void consumeDlqMessage(Object payload){
System.out.println("Are you OK?");
//这里抛个错
if(count.incrementAndGet()%3==0){
System.out.println("Fine,thank you.and you?");
}else{
throw new RuntimeException("I'm not OK");
}
}
}
4)修改配置文件
spring.cloud.stream.bindings.dlq-consumer.destination=dlq-topic
spring.cloud.stream.bindings.dlq-producer.destination=dlq-topic
spring.cloud.stream.bindings.dlq-consumer.consumer.max-attempts=2
spring.cloud.stream.bindings.dlq-consumer.group=dlq-group
#开启死信队列,(默认为<topic>.dlq,这里的队列名便是dlq.topic.dlq-group.dlq(topic的名称+分组名+dlq))
spring.cloud.stream.rabbit.bindings.dlq-consumer.consumer.auto-bind-dlq=true
8、自定义异常、降级Demo和消息驱动的接口升版控制
1)创建一个Topic
public interface FallbackTopic{
String INPUT = "fallback-consumer";
String OUTPUT = "fallback-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
2)创建测试方法
@RestController
public class Controller{
@Autowired
private FallbackTopic producer;
@PostMapping("sendToFallback")
public void sendMessage(@RequestParam(value = "body")String body
@RequestParam(value = "version",defaultValue = "1.0")String version){
producer.output().send(MessageBuilder.withPayload(body)
.setHeader("version",version)
.build());
}
}
3)绑定信道
@EnableBinding({
FallbackTopic.class
})
public class StreamConsumer{
@StreamListener(FallbackTopic.TNPUT)
public void consumeFallbackMessage(Object payload
@Header("version")String version){
if("1.0".equalsIgnoreCase(version)){
System.out.println("version 1.0");
}else if("2.0".equalsIgnoreCase(version)){
System.out.println("不支持此版本");
//报错后 进入fallback
throw new RuntimeException("I'm not OK");
}else{
System.out.println("version="+version);
}
}
//编写Fallback逻辑
//降级的信道名称 是 topic名称+分组名+errors
@ServiceActivator(inputChannel = "fallback-topic.fallback-group.errors")
public void fallback(Message<?> message){
System.out.println("这是fallback逻辑");
}
}
4)修改配置文件
spring.cloud.stream.bindings.fallback-consumer.destination=fallback-topic
spring.cloud.stream.bindings.fallback-producer.destination=fallback-topic
spring.cloud.stream.bindings.fallback-consumer.consumer.max-attempts=2
spring.cloud.stream.bindings.fallback-consumer.group=fallback-group