Rabbit MQ实现重试和时间间隔机制 rabbitmq延迟发送消息

利用死信机制来实现重试和时间间隔机制,可以控制单独的队列重试次数和时间。
总体的思路就是,将消费失败的消息发送到一个有过期时间的队列中,该队列没有消费者,并且配置有死信队列,那到了超时时间后,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队列

经过多次重发仍然失败的任务,最终会被投放到这个队列,用于排查问题,或者可以根据业务逻辑再实现消息的重启。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章