当消息处理消费失败后,SpringCloud Stream 会自动默认重试3次,重试三次失败后,RepublishMessageRecoverer类recover方法会将改变routingkey为队列名称发送至死信队列。目前产生死信队列有两种方式:
默认自动为每个消息队列产生一个死信队列,消费失败时会路由至该队列的死信队列
直接指定每个消息队列绑定的死信队列,多个消息队列可绑定同一个死信队列
本案例采用第2种方式,便于所有消费失败信息处理。
注意若系统中已经存在的消息队列,一定要在rabbitmq删除该队列,否则无法创建死信队列。 只需要配置spring.cloud.rabbit.bindings部分其中有两个注意点:
spring.cloud.bindings.input.group和spring.cloud.bindings.input.destination必须要配置,并且值不能一致,否则因为springcloud Dalston.SR1版本问题,死信消息队列的routingKey会取数错误,详细可参见RabbitMessageChannelBinder类createConsumerEndpoint方法,Spring Cloud 2.0发现该代码已经大部分改动了,没有BUG。
maxAttempts 可根据实际情况判断是否需要重试
在上一个步骤的例子中,我们在消费者端模拟一下消费消息出现异常的情况
再次运行后,可以看到消息被重复执行了三次,因为springCloud stream消息默认情况下是消费消息时若是出错了会重试三次,超过三次之后就会进入死信队列,若没有配置死信队列则该消息会丢失。
在上个列子中消费者端增加了以下代码
完整配置如下(对队列saveOrderInput配置了死信队列):
8.2.1 消费者配置文件
server:
port: 8087
spring:
application:
name: HuaXing-stream-consumer
cloud:
stream:
binders: #需要绑定的rabbitmq的服务信息
defaultRabbit: #定义的名称,用于bidding整合
type: rabbit #消息组件类型
environment: #配置rabbimq连接环境
spring:
rabbitmq:
host: localhost #rabbitmq 服务器的地址
port: 45672 #rabbitmq 服务器端口
username: guest #rabbitmq 用户名
password: guest #rabbitmq 密码
virtual-host: / #虚拟路径
bindings: #服务的整合处理
saveOrderInput: #这个是消息通道的名称 ---> 保存订单输入通道
destination: exchange-saveOrder #exchange名称,交换模式默认是topic;把SpringCloud stream的消息输出通道绑定到RabbitMQ的exchange-saveOrder交换器。
content-type: application/json #设置消息的类型,本次为json
default-binder: defaultRabbit
group: saveOrderGroup #分组
orderInput: #这个是消息通道的名称 ---> 订单输入通道
destination: exchange-order #exchange名称,交换模式默认是topic;把SpringCloud stream的消息输出通道绑定到RabbitMQ的exchange-saveOrder交换器。
content-type: application/json #设置消息的类型,本次为json
default-binder: defaultRabbit
group: orderGroup #分组
orderOutput: #这个是消息通道的名称 ---> 订单输出通道
destination: exchange-order #exchange名称,交换模式默认是topic;把SpringCloud stream的消息输出通道绑定到RabbitMQ的exchange-saveOrder交换器。
content-type: application/json #设置消息的类型,本次为json
default-binder: defaultRabbit
group: orderGroup #分组
rabbit:
bindings:
saveOrderInput:
consumer:
#ttl: 20000 # 默认不做限制,即无限。消息在队列中最大的存活时间。当消息滞留超过ttl时,会被当成消费失败消息,即会被转发到死信队列或丢弃.即消息在队列中存活的最大时间为 20s
# DLQ相关
autoBindDlq: true # 是否自动声明死信队列(DLQ)并将其绑定到死信交换机(DLX)。默认是false。
republishToDlq: true
#deadLetterExchange: exchange-order-dlq #绑定exchange
#deadLetterQueueName: exchange-order-dlq.saveOrderInput #死信队列名字:exchanName.queueName
8.2.2 生产者配置文件
server:
port: 8088
spring:
cloud:
stream:
binders:
defaultRabbit:
type: rabbit
environment:
spring:
rabbitmq:
host: localhost
port: 45672
username: guest
password: guest
virtual-host: /
bindings:
saveOrderOutput:
destination: exchange-saveOrder
content-type: application/json
default-binder: defaultRabbit
group: saveOrderGroup
8.3.1 在saveOrderInput通道消费者端模拟异常
在saveOrderInput通道消费者端增加int i = 1 / 0; 代码来模拟异常的出现。
/**
* 保存订单逻辑
* 通过OrderChannelProcessor.SAVE_ORDER_INPUT 接收消息
* 然后通过@SendTo 将处理后的消息发送到 OrderChannelProcessor.ORDER_OUTPUT
*
* @param message
* @return
*/
@StreamListener(OrderInputChannelProcessor.SAVE_ORDER_INPUT)
@SendTo(OrderOutputChannelProcessor.ORDER_OUTPUT)
public String saveOrderMessage(Message message) {
log.info("保存订单的消息:" + message);
int i = 1 / 0;
//处理之后的订单消息
return "【" + message.getPayload() + "】";
}
完整的代码如下:
8.3.2 生产者代码
/**
* @Description 订单控制器
* @author: 姚广星
* @time: 2020/11/29 12:03
*/
@RestController
@Slf4j
public classOrderController {
@Autowired
private OrderMessageProducerorderMessageProducer;
/**
* 发送保存订单消息
*
* @param message
*/
@GetMapping(value ="/sendSaveOrderMessage")
public voidsendSaveOrderMessage(@RequestParam("message") String message) {
//发送消息
orderMessageProducer.sendMsg(message);
log.info("发送保存订单消息成功");
}
}
/**
* @Description 订单消息发送者
* @author: 姚广星
* @time: 2020/11/29 19:37
*/
@Slf4j
@EnableBinding(value ={OrderOutputChannelProcessor.class})
public classOrderMessageProducer {
@Autowired
@Output(OrderOutputChannelProcessor.SAVE_ORDER_OUTPUT)
private MessageChannel channel;
/**
* 发送消息
*
* @param msg
*/
public void sendMsg(String msg) {
channel.send(MessageBuilder.withPayload(msg).build());
log.info("消息发送成功:" + msg);
}
}
8.3.3 消费者代码
/**
* @Description 订单消息监听处理服务
* @author: 姚广星
* @time: 2020/11/29 16:03
*/
@Slf4j
@EnableBinding({OrderInputChannelProcessor.class,OrderOutputChannelProcessor.class})
public classOrderMessageListener {
/**
* 保存订单逻辑
* 通过OrderChannelProcessor.SAVE_ORDER_INPUT接收消息
* 然后通过@SendTo 将处理后的消息发送到 OrderChannelProcessor.ORDER_OUTPUT
*
* @param message
* @return
*/
@StreamListener(OrderInputChannelProcessor.SAVE_ORDER_INPUT)
@SendTo(OrderOutputChannelProcessor.ORDER_OUTPUT)
public StringsaveOrderMessage(Message message) {
log.info("保存订单的消息:" + message);
int i = 1 / 0;
//处理之后的订单消息
return "【" + message.getPayload() + "】";
}
/**
* 监听OrderChannelProcessor.ORDER_INPUT通道,进行发送短信业务操作
*
* @param message
*/
@StreamListener(OrderInputChannelProcessor.ORDER_INPUT)
public void orderToSMSMessage(Stringmessage) {
log.info("进行发送短信业务操作:{}", message);
}
/**
* 监听OrderChannelProcessor.ORDER_INPUT通道,进行存储到es业务操作
*
* @param message
*/
@StreamListener(OrderInputChannelProcessor.ORDER_INPUT)
public void orderToEsMessage(Stringmessage) {
log.info("进行存储到es业务操作:{}", message);
}
}
分别启动消费者和生产者,可以在RabbitMQ web界面中可以看到已经创建exchange-saveOrder.saveOrderGroup队列的死信队列 exchange-saveOrder.saveOrderGroup.dlq。
运行4次(任何次数都行)生产者方法发送消息给消费者,在RabbitMQ web页面可以明显的看出出错了的消息已经全部进入的死信队列当中。
4、查看dlq队列
点击页面队列名exchange-saveOrder.saveOrderGroup.dlq进入:
Get messages按钮可以查看当前在dlq队列中存放的消息,可以输入数量(相当于查看几条),点击按钮:
可以看到消息原封不动保存,并且有具体的报错信息。方便与我们来进行分析代码。
5、清空dlq队列所有消息
一开始时是没有Move Messages按钮的,这时我们需要安装插件
按提示需要执行下面命令开启插件,不需要重启rabbitmq
rabbitmq-plugins enablerabbitmq_shovel rabbitmq_shovel_management
清空消息时,可以再输入框中填入目标队列名称(可以从Get messages 部分的RoutingKey进行复制),点击按钮,清空死信队列消息。这时死信队列中的消息会回到原来的队列当中去。
本文涉及到的文献
官方文档:https://www.rabbitmq.com/
Git 代码地址:https://github.com/17666555910/HuaXing-SpringCloudDemo.git
CSDN 博客地址:https://blog.csdn.net/a767815662/category_10598332.html
微信公众号:【华星详谈】