利用死信机制来实现重试和时间间隔机制,可以控制单独的队列重试次数和时间。
总体的思路就是,将消费失败的消息发送到一个有过期时间的队列中,该队列没有消费者,并且配置有死信队列,那到了超时时间后,RabbitMQ会自动将该消息推送至死信队列,监听死信队列的消费者,获取消息后消费,从而实现控制时间间隔发送。而重试次数,可以通过在消息头中设置增加一个参数来实现。
下面是我的实现方法:
首先需要声明一个交换机和四个队列,将四个队列都绑定到交换机上。
String E_NOTIFY_CALLBACK = "E.NOTIFY_CALLBACK"; // 交换机
String Q_NOTIFY_CALLBACK_NORMAL = "Q.NOTIFY_CALLBACK@NORMAL"; // 正常队列
String Q_NOTIFY_CALLBACK_READY = "Q.NOTIFY_CALLBACK@REDAY"; // 预备队列(预备重试)
String Q_NOTIFY_CALLBACK_RETRY = "Q.NOTIFY_CALLBACK@RETRY"; // 重试队列
String Q_NOTIFY_CALLBACK_DEAD = "Q.NOTIFY_CALLBACK@DEAD"; // 死信队列
其中READY的队列,比较特殊,也就是RETRY的死信队列,需要设置消息的超时时间。
队列和交换机可以声明在RabbitConfig中,或者直接在监听的方法上用注解形式声明。
// 声明交换机
@Bean
public DirectExchange notifyCallbackExchange() {
return new DirectExchange(MQConfig.E_NOTIFY_CALLBACK);
}
// 声明预备队列
@Bean
public Queue notifyCallbackReadyQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 10000); // 队列消息超时时间
args.put("x-dead-letter-exchange", MQConfig.E_NOTIFY_CALLBACK); // 死信交换机
args.put("x-dead-letter-routing-key", "retry"); // 死信路由
return new Queue(MQConfig.Q_NOTIFY_CALLBACK_READY, true, false, false, args);
}
// 绑定预备队列到交换机,路由Key为ready
@Bean
public Binding bindNotifyCallbackReadyQueue() {
return BindingBuilder.bind(notifyCallbackReadyQueue()).to(notifyCallbackExchange()).with("ready");
}
// 声明死信队列
@Bean
public Queue notifyCallbackDeadQueue() {
return new Queue(MQConfig.Q_NOTIFY_CALLBACK_DEAD);
}
// 绑定死信队列到交换机,路由Key为dead
@Bean
public Binding bindNotifyCallbackDeadQueue() {
return BindingBuilder.bind(notifyCallbackDeadQueue()).to(notifyCallbackExchange()).with("dead");
}
其余的队列声明:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(MQConfig.Q_NOTIFY_CALLBACK_NORMAL),
exchange = @Exchange(value = MQConfig.E_NOTIFY_CALLBACK),
key = "normal"))
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = MQConfig.Q_NOTIFY_CALLBACK_RETRY),
exchange = @Exchange(value = MQConfig.E_NOTIFY_CALLBACK),
key = "retry"))
NORMAL队列
首先NORMAL队列会获取到一条消息,执行成功则结束逻辑,执行失败则推送消息至READY队列,等待重发。
@RabbitListener(bindings = @QueueBinding(
value = @Queue(MQConfig.Q_NOTIFY_CALLBACK_NORMAL),
exchange = @Exchange(value = MQConfig.E_NOTIFY_CALLBACK),
key = "normal"))
public void normal(NotifyCallbackMsg msg, Message message, Channel channel) {
log.info("首次收到通知消息:[{}]", msg);
try {
// 业务逻辑
// 直接模拟失败,发送到预备重试队列
if (msg.getRetryTimes() > 0) {
channel.basicPublish(MQConfig.E_NOTIFY_CALLBACK, "ready", basicProperties(message, 0), JSONObject.toJSONString(msg).getBytes());
}
} catch (IOException e) {
try {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
} catch (IOException ex) {
log.error("MQ手动确认消息失败!");
}
}
try { // 手动确认消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
log.error("MQ手动确认消息失败!");
}
}
请求头放入重试次数
private AMQP.BasicProperties basicProperties(Message message, int retryTimes) {
Map<String, Object> headers = message.getMessageProperties().getHeaders();
headers.put("retry-times", retryTimes);
return new AMQP.BasicProperties().builder()
.deliveryMode(2) // 传送方式
.contentEncoding("UTF-8") // 编码方式
.contentType("application/json")
.headers(headers) //自定义属性
.build();
}
READY队列
READY因为全局配置了过期时间,args.put(“x-message-ttl”, 10000),因为READY没有消费者,超过10秒的消息会被自动投放到READY的死信队列,即RETRY。
RETRY队列
正式执行重发逻辑的队列。
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = MQConfig.Q_NOTIFY_CALLBACK_RETRY),
exchange = @Exchange(value = MQConfig.E_NOTIFY_CALLBACK),
key = "retry"))
public void retry(NotifyCallbackMsg msg, Message message, Channel channel) {
log.info("尝试重新发送消息:[{}]", msg);
try {
int retryTimes = (int) message.getMessageProperties().getHeaders().get("retry-times");
log.info("执行第[{}]次重发...", retryTimes++);
// 业务逻辑
// 直接模拟失败
if (retryTimes > msg.getRetryTimes()) {
log.error("重试次数满,进入死信队列"); // 触发告警
channel.basicPublish(MQConfig.E_NOTIFY_CALLBACK, "dead", MessageProperties.PERSISTENT_BASIC, JSONObject.toJSONString(msg).getBytes());
} else {
channel.basicPublish(MQConfig.E_NOTIFY_CALLBACK, "ready", basicProperties(message, retryTimes), JSONObject.toJSONString(msg).getBytes());
}
} catch (IOException e) {
try {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
} catch (IOException ex) {
log.error("MQ手动确认消息失败!");
}
}
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
} catch (IOException e) {
log.error("MQ手动确认消息失败!");
}
}
DEAD队列
经过多次重发仍然失败的任务,最终会被投放到这个队列,用于排查问题,或者可以根据业务逻辑再实现消息的重启。