昨天研究了一下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) { } }