RabbitMq動態添加監聽

昨天研究了一下RabbitMQ 想做一個動態添加監聽的功能

依靠springboot 實現起來也簡單

就2個類 1個主類 實現動態添加隊列及綁定關係、動態添加監聽、動態調整監聽線程池大小、動態刪除隊列、動態取消監聽、發送動態隊列的消息。

還有個類就是自定義消費者 都是採用string接收參數,後面可以採用指定統一對象,然後用個type字段區分消息類型,再用策略模式分開處理。

原理就是注入rabbitTemplate、rabbitAdmin(所以頂部一定要加@Component),有了rabbitAdmin就可以動態聲明交換機、路由、隊列。

有了這3樣以後就是添加監聽了(就是新增消費者),採用DirectMessageListenerContainer 實現監聽(推薦用DirectMessageListenerContainer,關鍵就是setConsumersPerQueue的設置,消費線程都是給線程池管理的,減少了創建線程的開銷。還有個SimpleMessageListenerContainer實現,不推薦使用,區別參考https://blog.csdn.net/yingziisme/article/details/86418580)。這裏還用了個map保存container方便,因爲動態創建隊列有N個,就不用bean的方式了。

這裏採用的TopicExchange(以前看篇文章記得是direct性能好於topic,8C16G大概10W比6w的樣子,但是少了靈活性,topic的隊列可以模糊監聽不同的routingkey消息)

更新:今天新增了死信隊列、重試機制

package com.xx.rabbitmq;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.RetryInterceptorBuilder;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer;
import org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.retry.interceptor.RetryOperationsInterceptor;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 動態MQ
 * 實現動態添加隊列及綁定關係、動態添加監聽
 * 動態調整監聽線程池大小
 * 動態刪除隊列、動態取消監聽
 * 發送動態隊列的消息
 *
 * @author tiancong
 * @date 2020/11/26 15:55
 */
@Component
@Data
@Slf4j
public class DynamicRabbitMq {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    @Lazy
    private RabbitAdmin rabbitAdmin;

    /**
     * 動態交換機默認名稱
     */
    private static final String EXCHANGE = "yd.dynamic.exchange";

    /**
     * 默認死信交換機名稱
     */
    private static final String DLX_EXCHANGE = "yd.dynamic.exchange.dlx";

    /**
     * 隊列前綴
     */
    private static final String QUEUE_PREFIX = "yd.dynamic.queue.";

    /**
     * 默認死信隊列名稱
     */
    private static final String DLX_QUEUE = "yd.dynamic.queue.dlx";

    /**
     * 默認死信隊列名稱
     */
    private static final String DLX_ROUTING = "yd.dynamic.routing.dlx";

    private static final Map<String, DirectMessageListenerContainer> CONTAINER_MAP = new ConcurrentHashMap<>(8);

    /**
     * 動態添加隊列及綁定關係
     *
     * @param queueName  隊列名
     * @param exchange   交換機名
     * @param routingKey 路由名
     * @param needDlx    需要死信隊列
     */
    public void addQueueAndExchange(String queueName, String exchange, String routingKey, boolean needDlx) {
        queueName = getFullQueueName(queueName);
        Queue queue = new Queue(queueName);
        if (needDlx) {
            Map<String, Object> arguments = new HashMap<>(2);
            arguments.put("x-dead-letter-exchange", DLX_EXCHANGE);
            arguments.put("x-dead-letter-routing-key", DLX_ROUTING);
            queue = new Queue(queueName, true, false, false, arguments);
            QueueInformation queueInfo = rabbitAdmin.getQueueInfo(DLX_QUEUE);
            if (queueInfo == null) {
                Queue dlxQueue = new Queue(DLX_QUEUE);
                DirectExchange dlxDirectExchange = new DirectExchange(DLX_EXCHANGE);
                rabbitAdmin.declareQueue(dlxQueue);
                rabbitAdmin.declareExchange(dlxDirectExchange);
                rabbitAdmin.declareBinding(BindingBuilder.bind(dlxQueue).to(dlxDirectExchange).with(DLX_ROUTING));
                log.info("創建死信隊[{}]列成功", DLX_QUEUE);
            }
        }
        TopicExchange topicExchange = new TopicExchange(exchange);
        rabbitAdmin.declareQueue(queue);
        rabbitAdmin.declareExchange(topicExchange);
        rabbitAdmin.declareBinding(BindingBuilder.bind(queue).to(topicExchange).with(routingKey));
    }

    /**
     * 動態刪除隊列(隊列有消息時不刪除)
     *
     * @param queueName 隊列名
     */
    public void deleteQueue(String queueName) {
        queueName = getFullQueueName(queueName);
        if (Objects.requireNonNull(rabbitAdmin.getQueueInfo(queueName)).getMessageCount() == 0) {
            rabbitAdmin.deleteQueue(queueName);
            log.info("成功刪除mq隊列{}", queueName);
        } else {
            log.info("mq隊列[{}]裏還有消息。不做刪除操作", queueName);
        }
    }


    /**
     * 動態添加隊列監聽
     *
     * @param queueName  隊列名
     * @param routingKey 路由名
     */
    public void startListener(String queueName, String routingKey) {
        startListener(queueName, routingKey, 1, false);
    }

    /**
     * 動態添加隊列監聽及修改消費者線程池大小
     *
     * @param queueName   隊列名
     * @param routingKey  路由名
     * @param consumerNum 消費者線程數量
     * @param needDlx     需要死信隊列
     */
    public void startListener(String queueName, String routingKey, int consumerNum, boolean needDlx) {
        queueName = getFullQueueName(queueName);
        addQueueAndExchange(queueName, EXCHANGE, routingKey, needDlx);
        DirectMessageListenerContainer container = new DirectMessageListenerContainer(rabbitTemplate.getConnectionFactory());
        DirectMessageListenerContainer getContainer = CONTAINER_MAP.putIfAbsent(queueName, container);
        if (getContainer != null) {
            log.info("動態修改mq監聽成功,交換機:{},路由key:{},隊列:{},線程數:{}", EXCHANGE, routingKey, queueName, consumerNum);
            container = getContainer;
        } else {
            container.setQueueNames(queueName);
            log.info("動態添加mq監聽成功,交換機:{},路由key:{},隊列:{},線程數:{}", EXCHANGE, routingKey, queueName, consumerNum);
        }
        container.setPrefetchCount(consumerNum);
        if (needDlx) {
            container.setAcknowledgeMode(AcknowledgeMode.AUTO);
        } else {
            container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        }
        container.setConsumersPerQueue(consumerNum);
        container.setMessageListener(new ConsumerHandler(!needDlx));
        container.setAdviceChain(createRetry());
        container.setDefaultRequeueRejected(false);
        container.start();
    }

    /**
     * 動態停止監聽並刪除隊列
     *
     * @param queueName 隊列名
     */
    public void stopListener(String queueName) {
        queueName = getFullQueueName(queueName);
        DirectMessageListenerContainer container = CONTAINER_MAP.get(queueName);
        if (container != null) {
            container.stop();
            container.destroy();
            CONTAINER_MAP.remove(queueName);
        }
        log.info("停止監聽mq隊列{}", queueName);
        deleteQueue(queueName);
    }

    /**
     * 發送動態隊列的消息
     *
     * @param routingKey 路由名
     * @param data       數據
     */
    public void sendMsg(String routingKey, String data) {
        rabbitTemplate.convertAndSend(EXCHANGE, routingKey, data);
    }


    /**
     * 獲取隊列名全稱
     *
     * @param queueName 隊列名
     * @return 全稱
     */
    private String getFullQueueName(String queueName) {
        if (queueName.startsWith(QUEUE_PREFIX)) {
            return queueName;
        }
        return QUEUE_PREFIX + queueName;
    }

    /**
     * 重試機制
     *
     * @return
     */
    private RetryOperationsInterceptor createRetry() {
        return RetryInterceptorBuilder
                .stateless()
                //重試次數
                .maxAttempts(3)
                //重試間隔  指數遞增時間參數   最大間隔時間
                .backOffOptions(1000, 3, 5000)
                //次數用完之後的處理,用的是默認處理類,失敗消息會到死信
                .recoverer(new RejectAndDontRequeueRecoverer())
                .build();
    }
}
package com.xx.rabbitmq;

import com.rabbitmq.client.Channel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;

/**
 * @author tiancong
 * @date 2020/11/25 15:57
 */
@Slf4j
@Data
@AllArgsConstructor
public class ConsumerHandler implements ChannelAwareMessageListener {

    /**
     * 是否需要回應
     */
    private final Boolean needAck;

    /**
     * 接收消息
     *
     * @param message
     * @param channel
     * @throws Exception
     */
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        int flag = (int) message.getMessageProperties().getHeaders().getOrDefault("retryCount", 0);
        flag++;
        if (flag > 1) {
            log.info("此消息第{}次執行", flag);
        }
        message.getMessageProperties().setHeader("retryCount", flag);
        String data = new String(message.getBody());
        log.info("[{}]收到mq消息: {}", message.getMessageProperties().getConsumerQueue(), data);
        if (getNeedAck()) {
            long deliveryTag = message.getMessageProperties().getDeliveryTag();
            try {
                handleMessage(data);
                channel.basicAck(deliveryTag, false);
            } catch (Exception e) {
                channel.basicNack(deliveryTag, false, true);
            }
        } else {
            handleMessage(data);
        }
    }

    /**
     * 處理消息
     *
     * @param data 消息體
     */
    public void handleMessage(String data) {

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