rabbitmq失败重试机制+死信队列,防止消息未被消费
如何保证消息一定被消费掉?请看下面三步骤:
一、重试机制
首先说一下RabbitMQ的消息重试机制,顾名思义,就是消息消费失败后进行重试,重试机制的触发条件是消费者显式的抛出异常,这个很类似@Transactional,如果没有显式地抛出异常或者try catch起来没有手动回滚,事务是不会回滚的。
相关配置如下:
spring:
rabbitmq:
host: cl-cloud-rabbitmq
username: swh
password: swh123
port: 5672
publisher-confirms: true
publisher-returns: true
virtual-host: /
listener:
simple:
acknowledge-mode: manual
#消费者的最小数量
concurrency: 1
#消费者的最大数量
max-concurrency: 5
retry:
#是否支持重试
enabled: true
#最大重试次数
max-attempts: 12
#最大间隔时间
max-interval: 3600000
#初始重试间隔时间
initial-interval: 1000
#乘子 重试间隔*乘子得出下次重试间隔 3s 9s 27s
multiplier: 3
#重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)
default-requeue-rejected: false
二、死信队列
死信就是消息在特定场景下的一种表现形式,这些场景包括:
- 消息被拒绝(basic.reject / basic.nack),并且requeue = false
- 消息的 TTL 过期时
- 消息队列达到最大长度
- 达到最大重试限制
死信队列就是用于储存死信的消息队列,在死信队列中,有且只有死信构成,不会存在其余类型的消息。死信队列也是一个普通队列,也可以被消费者消费,区别在于业务队列需要绑定在死信队列上,才能正常地把死信发送到死信队列上。
死信队列实现:
1)创建死信交换机
/**
* 延迟交换器构造器
*
* @author: zyf
* @date: 2019/3/8 13:31
* @description:
*/
public class ExchangeBuilder {
/**
* 默认延迟消息交换器
*/
public final static String DEFAULT_DELAYED_EXCHANGE = "cl.delayed.exchange";
/**
* 普通交换器
*/
public final static String DEFAULT_DIRECT_EXCHANGE = "cl.direct.exchange";
/**
* 死信交换器
*/
public final static String DEAD_EXCHANGE = "cl.exchange.dead";
/**
* 死信交换机绑定ke
*/
public final static String DEAD_EXCHANGE_ROUTING_KEY = "deadQueue";
/**
* 死信队列名称
*/
public final static String DEAD_QUEUE = "deadQueue";
/**
* 构建延迟消息交换器
*
*/
public static CustomExchange buildDelayExchange() {
Map<String, Object> args = new HashMap<>(1);
args.put("x-delayed-type", "direct");
return new CustomExchange(DEFAULT_DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
}
/**
* 构建普通交换机
*/
public static DirectExchange buildDirectExchange() {return new DirectExchange(DEFAULT_DIRECT_EXCHANGE, true, false);
}
/**
* 构建死信交换机
*/
public static DirectExchange buildDeadExchange() {
return new DirectExchange(DEAD_EXCHANGE, true, false);
}
}
2)创建死信队列并绑定到死信交换机
DirectExchange deadExchange = ExchangeBuilder.buildDeadExchange();
rabbitAdmin.declareExchange(exchange);
Queue queue = new Queue(ExchangeBuilder.DEAD_QUEUE);
rabbitAdmin.declareQueue(queue);
Binding binding = BindingBuilder.bind(queue).to(deadExchange).with(ExchangeBuilder.DEAD_QUEUE);
rabbitAdmin.declareBinding(binding);
3)普通队列绑定死信交换机
// 队列绑定我们的死信交换机
Map<String, Object> arguments = new HashMap<>(2);
//死信交换机
arguments.put("x-dead-letter-exchange", ExchangeBuilder.DEAD_EXCHANGE);
//死信队列
arguments.put("x-dead-letter-routing-key", ExchangeBuilder.DEAD_EXCHANGE_ROUTING_KEY);
Queue queue = new Queue(queueName, true, false, false, arguments);
rabbitAdmin.declareQueue(queue)
出现如下features表示消息成为死信消息后,会进入死信队列
4)更改配置
#重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)
default-requeue-rejected: false
5)失败重试后进入死信队列有两种方案
方案一:使用自动ACK + RabbitMQ重试机制
只需更改配置acknowledge-mode为auto,变为自动ack,达到最大失败次数后会自动进入我们配置的死信队列中
方案二:使用手动ACK + 手动重试机制
①配置acknowledge-mode为manual,手动ack。
②代码中控制,达到失败重试次数后,拒绝消息
private final String REPEAT_CHECK = "rabbitmq:repeatCheck:";
private final String RETRY_COUNT = "rabbitmq:retryCount:";
/**
* 消息最大重试次数
*/
@Value("${spring.rabbitmq.listener.simple.retry.max-attempts}")
private int MAX_RETRIES;
// 正常消费消息
try {
mqListener.handler(t, channel);
channel.basicAck(deliveryTag, false);
redisUtil.set(REPEAT_CHECK+version, "1", expireSecond);
}catch (Exception e){
// 删除重复消费标记,以免失败重试时,被误认为是重复消息
if(StrUtil.isNotEmpty(version)){
redisUtil.del(REPEAT_CHECK + version);
}
// 达到失败重试次数后,进入死信队列
Object retryCountObj = redisUtil.get(RETRY_COUNT + version);
if(retryCountObj == null){
redisUtil.set(RETRY_COUNT+version, 1);
}else {
int retryCount = Integer.valueOf(retryCountObj.toString()) + 1;
if(retryCount>=MAX_RETRIES){
// 拒绝,不重新入队。则会进入死信队列
channel.basicNack(deliveryTag, false, false);
redisUtil.del(RETRY_COUNT+version);
}else {
redisUtil.set(RETRY_COUNT+version, retryCount);
}
}
throw e;
}
总结:两种方案都可以达到我们的预期效果,相比起来方案一会更加的方便简洁,方案二的可控性更高
三、持久化数据库+人工处理
死信队列消费端将所有消息持久化到数据库,人工根据具体情况写定时任务或其他方式处理