Rabbitmq手动确认结合Redis实现消息重试

189 阅读1分钟

一、思路

rabbitmq里面自带了retry的功能,但在手动确认模式里面就不太好用了。本篇文章的主要思路就是通过redis以消息的messageId为key,记录重试次数。

实现代码

首先创建队列和交换机

@Configuration
public class LogMqConfig {

    /**
     * 交换机
     */
    @Bean
    public DirectExchange logExchange() {
        return new DirectExchange(MqConstants.LOG_LOGIN_EXCHANGE, true, false);
    }

    /**
     * 队列
     */
    @Bean
    public Queue logQueue() {
        Map<String, Object> map = new HashMap<>(4);
        // 绑定该队列到私信交换机
        map.put("x-dead-letter-exchange", MqConstants.DEAD_EXCHANGE);
        map.put("x-dead-letter-routing-key", MqConstants.DEAD_ROUTING);
        return new Queue(MqConstants.LOG_LOGIN_QUEUE, true, false, false, map);
    }

    /**
     * 队列交换机绑定
     */
    @Bean
    public Binding logBinding(DirectExchange logExchange, Queue logQueue) {
        return BindingBuilder.bind(logQueue).to(logExchange).with(MqConstants.LOG_LOGIN_ROUTING);
    }
}

消息到达最大重试次数,可以让它进入死信队列,所这里创建死信队列

@Configuration
public class DeadMqConfig {

    /**
     * 死信交换机
     */
    @Bean
    DirectExchange deadExchange() {
        return new DirectExchange(MqConstants.DEAD_EXCHANGE, true, false);
    }

    /**
     * 死信队列
     */
    @Bean
    public Queue deadQueue() {
        return new Queue(MqConstants.DEAD_QUEUE, true, false, false);
    }

    @Bean
    Binding deadRouteBinding() {
        return BindingBuilder.bind(deadQueue()).to(deadExchange()).with(MqConstants.DEAD_ROUTING);
    }
}

发送消息类

@Component
@RequiredArgsConstructor
public class MessageSender {

    private final RabbitTemplate rabbitTemplate;

    public void sendMsg(String exchange, String routingKey, Object object) {
        String msg = JSONUtil.toJsonStr(object);
        MessageProperties messageProperties = new MessageProperties();
        messageProperties.setMessageId(IdUtil.fastSimpleUUID());
        messageProperties.setContentType("text/plain");
        messageProperties.setContentEncoding("utf-8");
        Message message = new Message(msg.getBytes(Charset.defaultCharset()), messageProperties);
        rabbitTemplate.convertAndSend(exchange, routingKey, message);
    }
}

消息的消费者

@Slf4j
@Component
@RequiredArgsConstructor
public class LogListener {

    private final RedisCache redisCache;
    private static final int RETRY_TIME = 3;

    @RabbitListener(queues = MqConstants.LOG_LOGIN_QUEUE)
    public void logProcess(Message message, Channel channel) {
        ActionEnum action = ActionEnum.ACCEPT;
        LogMessage logMessage = new LogMessage();
        String messageId = "";
        long deliveryTag = 0L;
        try {
            String json = new String(message.getBody());
            logMessage = JSONUtil.toBean(json, LogMessage.class);
            deliveryTag = message.getMessageProperties().getDeliveryTag();
            messageId = message.getMessageProperties().getMessageId();
            log.info("收到消息,路由:【{}】,消息体:【{}】", MqConstants.LOG_LOGIN_ROUTING, logMessage);
            int i = 1/0;
        } catch (Exception e) {
            action = ActionEnum.RETRY;
        } finally {
            boolean requeue = false;
            try {
                if (action == ActionEnum.ACCEPT) {
                    channel.basicAck(deliveryTag, false);
                    log.error("消息消费成功------message:【{}】",logMessage);
                } else if (action == ActionEnum.RETRY) {
                    String key = RedisKeys.RABBITMQ_RETRY_KEY + messageId;
                    Object obj = redisCache.get(key);
                    if (obj == null) {
                        log.error("消息消费失败,进入第【1次】重试-------messageId:【{}】",messageId);
                        // 第一次重试
                        redisCache.set(key, 1, 60 *2);
                        requeue = true;
                    } else {
                        Integer retryTime = Convert.toInt(obj);
                        if (retryTime < RETRY_TIME) {
                            log.error("消息消费失败,进入第【{}次】重试-------messageId:【{}】",retryTime + 1, messageId);
                            requeue = true;
                            redisCache.increment(key);
                        } else {
                            redisCache.delete(key);
                            log.info("重试已到最高次数,进入死信队列!");
                        }
                    }
                    // requeue为true就重新进入队列
                    channel.basicNack(deliveryTag, false, requeue);
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

消息生产者测试

@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
@Slf4j
public class TestController {

    private final MessageSender messageSender;

    @GetMapping("/sendMsg")
    public Result<LogMessage> testMq() {

        LogMessage logMessage = new LogMessage();
        logMessage.setCode("test");
        logMessage.setName("张三");
        messageSender.sendMsg(MqConstants.LOG_LOGIN_EXCHANGE, MqConstants.LOG_LOGIN_ROUTING, logMessage);
        log.info("MQ消息发送,交换机:【{}】, 路由:【{}】, 消息体:【{}】", MqConstants.LOG_LOGIN_EXCHANGE, MqConstants.LOG_LOGIN_ROUTING, logMessage);
        return Result.success(logMessage);
    }
}

测试结果

消息消费成功时:

image.png 消息消费失败时:

image.png