前言
本Blog根据自己学习SpringCloudStream过程中实践的案例撰写,使用的是新版的函数式编程模型定义binding,共有两个Demo。理解有限,如有不正确的地方,十分欢迎指正。
Demo1:常规的实现Topic交换机实现根据不同的routingKey路由消息到两个routingKey不同的Queue中。
Demo2:通过RabbitMQ中的TTL机制和死信队列实现延时队列功能。
一、依赖添加
<!--MQ产品使用的是RabbitMQ,Maven中添加如下依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
<version>3.2.5</version>
</dependency>
二、MQ服务器连接配置
spring:
rabbitmq:
username: guest
password: guest
host: localhost
port: 5672
virtual-host: /
三、Binder配置
spring:
cloud:
stream:
# 默认binder使用rabbit,可以配置多个binder,每个binder可以连接不同的MQ产品
default-binder: rabbit
binders:
# 配置一个binder,设置它的类型为"rabbit"(表示MQ产品是RabbitMQ,固定写法),如果使用kafka则type后面写:kafka
rabbit:
type: rabbit
# 环境配置,引用前面配置好的RabbitMQ相关信息,Ip、Port、userName、password等
environment:
spring:
rabbitmq:
host: ${spring.rabbitmq.host}
port: ${spring.rabbitmq.port}
username: ${spring.rabbitmq.username}
password: ${spring.rabbitmq.password}
virtual-host: ${spring.rabbitmq.virtual-host}
四、Binding配置(重点、难点)
4.1 Demo1
Demo1配置,一个交换机两个队列,实现Exchange根据routingKey路由到不同的队列中
spring:
cloud:
stream:
bindings:
# 不以函数式编程方式定义,正常我们命名规则为xxxx-out-0,这里由于该Bindings的destination需要被两个消费者订阅,因此采用这种定义方法,可能原来的方式定义也可,不过我这样就实现了,还需继续研究
# 配置一个生产者binding,在RabbiMQ生成一个名称为delayOutExchange的Exchange
delayOut:
destination: delayOutExchange
# 配置一个消费者Binding,订阅的Exchange是delayOutExchange;在RabbitMQ生成一个名称为delayOutExchange.30s-queue的Queue
delay30-in-0:
destination: delayOutExchange
group: 30s-queue
# 配置一个消费者Binding,订阅的Exchange是delayOutExchange;在RabbitMQ生成一个名称为delayOutExchange.60s-queue的Queue
delay60-in-0:
destination: delayOutExchange
group: 60s-queue
# 注意,这里的rabbit和上面的rabbit不是一个概念,上面定义了一个binder,名称是rabbit;这里的rabbit是针对RabbitMQ的类型为rabbit(上面配置的type字段)的所有binding配置
rabbit:
bindings:
# 配置delayOut这个binding
delayOut:
producer:
# 交换机类型
exchange-type: topic
# routingKey存放在发送消息时的header中(说明:发送消息时可以在header中添加键值对,key为routingKey,值自己随便设置,这个值即为routingkey的值,用于后续交换机路由到不同的Queue中)
routing-key-expression: "headers['routingKey']"
# 配置delay30-in-0这个binding
delay30-in-0:
consumer:
exchange-type: topic
# 当前队列绑定的routingKey,交换机会把符合下面这个routingKey表达式的消息路由到队列delayOutExchange.30s-queue中
binding-routing-key: "A.*"
# 配置delay60-in-0这个binding
delay60-in-0:
consumer:
exchange-type: topic
# 当前队列绑定的routingKey,交换机会把符合下面这个routingKey表达式的消息路由到队列delayOutExchange.60s-queue中
binding-routing-key: "*.D"
# 使用函数式定义消费者,和Binding中的最前面一部分保持一致,非常重要
function:
definition: delay60;delay30;
Demo1图解
说明:
① 定义了一个Binding,名字为delayOut,创建一个名称为delayOutExchange
的Exchange,该交换机从生产者发送的消息的消息头即headers
中以key"routingKey"
获取值,作为消息的routingKey,用于决定消息路由到哪一个Queue
② 定义了两个Binding,名称分别为"delay30-in-0"
和"delay60-in-0"
,连接的Exchange均为"delayOutExchange"
;分别创建的Queue为"delayOutExchange.30s-queue"
和"delayOutExchange.60-queue"
(就是bindings配置中的"destination+group")
③ Queue"delayOutExchange.30s-queue"
中绑定的routingKey匹配表达式为:"A.*"
,Queue"delayOutExchange.60s-queue"
中绑定的routingKey匹配表达式为:"*.D"
;"A.*"
表示只接收routingKey以A.开头的消息,"*.D"
表示只接收routingKey以".D"
结尾的消息
举例说明:
如果消息header中存在键值对数据routingKey="A.B",则消息只会被exchange路由到队列"delayOutExchange.30s-queue"中,即只有消费者01能消费到消息;
如果消息header中存在键值对数据routingKey="B.D",则消息只会被Exchange路由到队列"delayOutExchange.60s-queue"中,即只有消费者02能消费 到消息;
如果消息header中存在键值对数据routingKey="A.D",则消息会被Exchange路由到两个队列中,即消费者01和消费者02都能消费到消息
Demo1代码
注意名称:方法名称即Bean名称必须和配置文件中的Binding名称的最前面一部分一致,即"xxx-in-0"
中的"xxx"
一、在配置类中添加如下两个Bean,表示两个Binding的消费者
import java.util.function.Consumer;
@Bean
public Consumer<String> delay30() {
return msg -> System.out.println("【delay30】接收到消息:" + msg);
}
@Bean
public Consumer<String> delay60() {
return msg -> System.out.println("【delay60】接收到消息:" + msg);
}
二、生产者发送消息到exchange代码
@GetMapping("/60s/{routingKey}")
public String sendDelay1mMsg(@PathVariable("routingKey") String routingKey) {
streamBridge.send(
"delayOut",
MessageBuilder.withPayload(routingKey).setHeader("routingKey", routingKey).build());
return "发送成功";
}
Demo1效果截图
说明:向上面定义的controller发送请求,并设置routingKey,查看控制台打印
以上是MQ中创建的Exchange和Queue截图!
以上是RoutingKey为A.B,匹配"delayOutExchange.30s-queue"
Queue的routingKey表达式,因此该队列的消费者消费到了消息。
以上是RoutingKey为B.D,匹配"delayOutExchange.60s-queue"
Queue的routingKey表达式,因此该队列的消费者消费到了消息。
以上是RoutingKey为A.D,匹配"delayOutExchange.60s-queue"
Queue和"delayOutExchange.60s-queue"
的routingKey表达式,因此两个队列的消费者均消费到了消息。
4.2 Demo2
Demo2新增配置
说明:Binder配置使用Demo1中的即可,不需要动,在Demo1的基础上。Bindings配置添加如下内容:
spring:
cloud:
stream:
bindings:
# 这个binding可以理解成我们正常的消息队列,它接收生产者消息
delay-out-0:
destination: delay-exchange
group: delay-30s-queue
producer:
# 这个配置非常重要,没有该配置死信队列无法消费数据!!!(查阅资料得知该值配置的是生产者的组,我没有搞懂具体的作用,目前也是一知半解的状态,但是测试了一下,似乎是只需要配置就行,值具体作用不清楚,但是可以实现我们需要的效果)
required-groups: 30s-queue
handleDeadLetter-in-0:
# 必须与下方的dead-letter-exchange对应
# 下方配置中的dead-letter-queue-name一致,即配置destination+group
destination: dead-exchange
group: 40s-queue
rabbit:
bindings:
# 配置正常消息队列的属性
delay-out-0:
producer:
# 在该队列中的消息的TTL,30秒
ttl: 30000
# 必须开启自动绑定死信队列功能
auto-bind-dlq: true
# 死信队列交换机
dead-letter-exchange: dead-exchange
# 死信队列(这里的值和上面配置的订阅该死信队列消费者binding的 destination+group 一致)
dead-letter-queue-name: dead-exchange.40s-queue
handleDeadLetter-in-0:
consumer:
# 死信交换机必须是direct类型
exchange-type: direct
function:
# 增加后面俩个函数定义
definition: delay60;delay30;handleDeadLetter;delay;
Demo2图解
Demo2代码
// 新增Bean
@Bean
public Consumer<Message<String>> handleDeadLetter() {
return msg -> {
String payload = msg.getPayload();
log.info("【handleDeadLetter】接收到消息:{}", payload);
};
}
// 添加发送数据测试接口
@GetMapping("/dead/30s/{msg}")
public String sendToDead30sMsg(@PathVariable("msg") String msg) {
streamBridge.send("delay-out-0", MessageBuilder.withPayload(msg + "-" + new Date()).build());
return "发送消息成功,发送时间:" + new Date();
}
Demo2效果截图:
注意:服务刚启动后发现,MQ中创建了名称为"dead-exchange"
的Exchange和名称为"dead-exchange-40s-queue"
的Queue;并没有创建我们定义的"delay-out-0"
这个Binding所配置的"delay-exchange"
Exchange和"delay-exchange.delay-30s-queue"
Queue。
通过浏览器请求向"delay-out-0"
Exchange中发送数据:
30s之后看控制台输出:
到此功能实现!
注意:这里为了模拟"死信"
,针对binding"delay-out-0"
中定义的队列delay-exchange.30s-queue
,并没有定义消费者,因此到了30s之后,消息在队列中过期,成为"死信"
。