实习遇到一个场景:
在rabbitmq消息确认设置为手动提交的时候,消息怎么在重试一定次数的情况下才放入死信队列。
rabbitmq的配置:消费端手动应答
在手动确认的条件下这些参数不太好使
原以为使用如上配置消息在重试三次之后,就会放入死信队列,事实上手动提交的时候,basicNack的最后一个参数requeue = true时,消息会被无限次的放入消费队列重新消费,直至回送ACK。
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
但是当requeue = false 的时候,此时消息就会立马进入到死信队列。
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
那么手动提交怎么去设置消息消费失败后回到队列的思路呢?
RabbitMQ实现重试次数方法一-SpringRetry
RabitMQ实现重试次数二-放入消息体
结合Redis
使用redis 存储消息的重试次数,redis 的key 就是消息的Id, value为消息的重试次数,代码如下
//消费失败重试3次,3次失败后放入死信队列
msgId = (String) message.getMessageProperties().getHeaders().get("spring_returned_message_correlation");
int retryCount = (int) redisUtil.get(msgId);
System.out.println("------ retryCount : " + retryCount);
if (retryCount >= MAX_RECONSUME_COUNT) {
//requeue = false 放入死信队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
} else {
//requeue = true 放入消费队列重试消费
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
redisUtil.set(msgId, retryCount + 1);
}
结果:
总结 投递成功 + 消费成功 + 幂等性保证
100% 投递到MQ成功
- 前提:开启confirm模式,在生产者那里设置了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如果写入了RabbitMQ中,RabbitMQ回回传一个ack消息,告诉你这个消息ok了。如果没有的话,会回调给你一个nack接口,告诉你这个消息接受失败,你可以重试。(这里我只记录了一下日志)
- 发送消息的时候, 调用uuid随机生成一个msgId,作为消息的correlationData,可以确保消息的唯一性,并且用这个msgid构造Rediskey,然后生成消费表记录,设置状态为投递正在处理中0,然后进行发送。
- 如果投递到exchange成功,回调了ack接口,此时根据correlationData获取消息msgId,然后更改消费表里面的消息的消费状态为投递成功1
- 定时任务接入: 定时任务去扫描消息消费表,扫描那些已经超时(2分钟)但是消息状态还在投递中的消息,进行重新投递(此时就直接投递,不需要表记录),并设置retrycount,如果大于最大的retrycount(3次),就更改消息状态为投递失败。
- 消息表结构
CREATE TABLE `msg_log` (
`msg_id` varchar(255) NOT NULL DEFAULT '' COMMENT '消息唯一标识',
`msg` text COMMENT '消息体, json格式化',
`exchange` varchar(255) NOT NULL DEFAULT '' COMMENT '交换机',
`routing_key` varchar(255) NOT NULL DEFAULT '' COMMENT '路由键',
`status` int(11) NOT NULL DEFAULT '0' COMMENT '状态: 0投递中 1投递成功 2投递失败 3已消费',
`try_count` int(11) NOT NULL DEFAULT '0' COMMENT '重试次数',
`next_try_time` datetime DEFAULT NULL COMMENT '下一次重试时间',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`msg_id`),
UNIQUE KEY `unq_msg_id` (`msg_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息投递日志';
select <include refid="Base_Column_List"/>
from c_consume_log
where status = 0
and nextTryTime <= now()
Broker弄丢了数据
开启Broker持久化,消息写入之后会持久化到磁盘,哪怕Broker挂了,回复之后会自动读取之前存储的数据。
设置持久化有两个步骤 :
- 创建queue的时候将其设置为持久化,这样就可以保证RabbitMQ持久化queue的原数据,但是它不会持久化queue里的数据
- 第二个是发送消息的时候将消息的deleveryMode设置为2,就是将消息设置为持久化的,此时RabbitMQ就会将消息持久化到磁盘上去
持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack 了,所以哪怕是在持久化到磁盘之前,Broker 挂了,数据丢了,生产者收不到 ack,你也是可以自己重发的
Consumer 消费重试 以及幂等性保证
-
前提: 关闭自动ACK,改为消费端手动应答,但是关闭自动ACK的时候springboot中配置的retry次数就会失效,手动提交的时候basicNack最后一个参数requeue = true的时候,消息会被无限次的重新放入到消费队列重新消费,而requeue =false的时候,这个消息又会立马进入到死信队列。
怎么才能设置一定的消费重试次数呢? -
幂等性保证
根据msgID从消费记录表中取值,如果该记录状态已经为CONSUMED_SUCCESS,则直接返回MsgLog msgLog = msgLogService.selectByMsgId(msgId); if (null == msgLog || msgLog.getStatus().equals(Constant.MsgLogStatus.CONSUMED_SUCCESS)) {// 消费幂等性 log.info("重复消费, msgId: {}", msgId); return; }
-
然后如果短信发送成功了,就回送MQ一个ACK,并且更改消费记录表中为消费成功
-
如果短信发送失败了,此时根据redis key 获取消息的重试次数,如果小于3次的话,requeu = true重新放入队列中并且重新消费,重试次数+1,如果大于3次的话,会送NACK,并且queue = false,此时消息被拒收放入死信队列。 进入死信队列可以去查看消息消费失败的原因