RabbitMq延迟重试简单队列

197 阅读2分钟
/**
 * @author ycs
 * 延迟队列的实现方案
 * Rabbitmq的延迟队列实现实际上依靠的是Rabbitmq的死信队列来做的,
 * 给消息设定一个过期时间,消息被发送到一个没有消费者的队列中,
 * 当过期时间到了的时候,消息会被认定为死信,就会被转发到设定好的死信队列,
 * 此时将死信队列设定为实际的消费队列就达到了延迟消费的目的
 * ------------------------------------------------------
 * 使用此队列的方式:
 * 创建一个子类继承该类,并把新创建的子类注入容器
 * #@Service
 * #@RabbitListener(queues = "${application.rabbit.queue:xxxxx.queue}")
 * #public class SyncService extends SimpleDirectRabbitQueue {
 * #    public SyncService(RabbitTemplate rabbitTemplate, @Value("${application.rabbit.queue:xxxxx.queue}")String queue){
 * #         super(rabbitTemplate,queue);
 * #    }
 * #}
 * 在子类上使用注解RabbitListener标记监听的是那个队列,由spring容器触发监听方法
 * 启动时由构造注入子类得到RabbitTemplate类和需要创建的队列名称
 * ------------------------------------------------------
 * 接下来是父类的工作:
 * 父类会根据需要创建的队列名称创建出两个交换器,和两个队列,分别对应实际队列和死信队列。
 * 由于子类继承了父类的rabbitListenerHandle方法,rabbitListenerHandle方法由@RabbitHandler(isDefault = true)注解标注主要消费方法。
 * 在这个方法内包含重试逻辑。
 */
@Slf4j
public abstract class DelayedRetryQueue {

    private final static String DELAY_MARKING = ".delay";
    private final static String EXCHANGE_MARKING = ".direct-exchange";
    private final static String DELAY_COUNT = "delay-count";

    /**
     * 消费队列
     */
    private final String queue;
    /**
     * 消费队列交换器
     */
    private final String exchange;
    /**
     * 死信队列
     */
    private final String delayQueue;
    /**
     * 死信队列交换器
     */
    private final String delayExchange;
    /**
     * 从spring获取的rabbitTemplate
     */
    private final RabbitTemplate rabbitTemplate;

    private boolean retry = true;

    public DelayedRetryQueue(RabbitTemplate rabbitTemplate, String queue) throws Exception {
        this.rabbitTemplate = rabbitTemplate;
        this.queue = queue;
        this.exchange = this.queue + EXCHANGE_MARKING;
        this.delayQueue = this.queue + DELAY_MARKING;
        this.delayExchange = this.exchange + DELAY_MARKING;
        init();
    }

    public DelayedRetryQueue(RabbitTemplate rabbitTemplate, String queue, boolean retry) throws Exception {
        this(rabbitTemplate, queue);
        this.retry = retry;
    }

    public void init() throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = rabbitTemplate.getConnectionFactory();
        Connection connection = connectionFactory.createConnection();
        Channel channel = connection.createChannel(false);
        //消费队列
        log.info("create consumption queue of {}", queue);
        channel.queueDeclare(queue, true, false, false, null);
        channel.exchangeDeclare(exchange, "direct", true);
        channel.queueBind(queue, exchange, exchange);
        //死信队列
        log.info("create delay queue of {}", queue);
        Map<String, Object> args = new HashMap<>(2);
        args.put("x-dead-letter-exchange", exchange);
        args.put("x-dead-letter-routing-key", exchange);
        channel.queueDeclare(delayQueue, true, false, false, args);
        channel.exchangeDeclare(delayExchange, "direct", true);
        channel.queueBind(delayQueue, delayExchange, delayExchange);
        channel.close();
    }

    /**
     * 消费流程
     *
     * @param message 消息内容
     * @param channel 信道
     * @throws Exception 异常错误
     */
    protected abstract void consume(Message message, Channel channel) throws Exception;

    /**
     * 消费流程-失败
     *
     * @param message   消息内容
     * @param throwable 错误内容
     */
    protected abstract void consumeFail(Message message, Throwable throwable);

    /**
     * 丢弃策略
     *
     * @param message   消息内容
     * @param throwable 错误内容
     */
    protected abstract void discard(Message message, Throwable throwable);


    /**
     * 即时生产
     *
     * @param bytes 数据内容
     */
    protected void instantProduce(byte[] bytes) {
        Message message = new Message(bytes, new MessageProperties());
        rabbitTemplate.send(exchange, exchange, message);
    }

    /**
     * 延迟生产
     *
     * @param bytes 数据内容
     * @param time  延迟时间(毫秒)
     */
    protected void delayProduce(byte[] bytes, long time) {
        Message message = new Message(bytes, new MessageProperties());
        MessageProperties messageProperties = message.getMessageProperties();
        messageProperties.setExpiration(String.valueOf(time));
        rabbitTemplate.send(delayExchange, delayExchange, message);
    }

    /**
     * 对接spring的消费方法
     *
     * @param message
     * @param channel
     */
    @RabbitHandler(isDefault = true)
    public void rabbitListenerHandle(Message message, Channel channel) {
        try {
            //调用实际的消费方法并且手动ack,如果消费发生错误则进入重试丢弃流程
            consume(message, channel);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Throwable e) {
            consumeFail(message, e);
            if (retry) {
                //获取重试等级
                int delayCount = (int) Optional.ofNullable(message.getMessageProperties().getHeader(DELAY_COUNT)).orElse(1);
                RabbitDelayEnum level = RabbitDelayEnum.getLevel(delayCount);
                try {
                    //先确认掉
                    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                if (level == null) {
                    //丢弃
                    log.error(String.format("queue: %s 消费失败,执行丢弃", queue), e);
                    discard(message, e);
                } else {
                    //重试->即将重试等级加一发到私信队列
                    log.error(String.format("queue: %s 消费失败,尝试重试", queue), e);
                    MessageProperties messageProperties = message.getMessageProperties();
                    messageProperties.setExpiration(level.getTime());
                    messageProperties.setHeader(DELAY_COUNT, ++delayCount);
                    rabbitTemplate.send(delayExchange, delayExchange, message);
                }
            } else {
                //不重试 直接丢弃
                log.error(String.format("queue: %s 消费失败,执行丢弃", queue), e);
                discard(message, e);
            }
            throw new RuntimeException(e);
        }
    }


}