消息驱动-消息整合利器SpringCloud Stream

一、初识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
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值