rabbitMQ系列之非统一时间延时队列

rabbitMQ系列之非统一时间延时队列

上篇文章:rabbitMQ 系列 之 死信 有讲到什么是rabbitMQ的死信以及怎样实现一个延时队列(TTL方式),但是这种方式有一个缺点,就是对于非统一失效时间的事件无法及时失效,比如商品的上下架时间,对于每个商品有不同的上下架时间,那么相应的消息失效时间也不同,而TTL方式,只能在最近一条消息失效变为死信后,才能将之后的失效消息变更为死信。

TTL死信失效顺序说明
像上图说明,本来我们期望进入延时队列的消息顺序为A->C->B-D,但是实际情况为只有先messageD失效后,才能是C…显然这种顺序是无法满足我们一些非统一失效时间的消息消费的需求的。

那么怎样才能达到只要失效时间到达就直接进入消费队列中而不是只有等到前置消息进入后才进入呢,这里就只能使用插件了:rabbitmq_delayed_message_exchange(要求rabbitMQ版本为3.6以上)。

过程

  1. 生产者将消息(message)和路由键(routingKey)发送到指定的延时交换器(exchange)上
  2. 消息一直存储在exchange中,直至达到消息失效时间。
  3. 消息失效后,exchange根据routingKey将消息路由到相应绑定的queue中
  4. 消费者消费queue中的消息

rabbitmq_delayed_message_exchang说明

安装

  1. 下载插件

    官网下载地址:https://www.rabbitmq.com/community-plugins.html

    [root@coder webdata]# wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v3.8.0/rabbitmq_delayed_message_exchange-3.8.0.ez
    
  2. docker安装rabbitmq

    [root@coder webdata]# docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
    Unable to find image 'rabbitmq:management' locally
    management: Pulling from library/rabbitmq
    171857c49d0f: Pull complete 
    419640447d26: Pull complete 
    61e52f862619: Pull complete 
    856781f94405: Pull complete 
    fd5f3d3bac09: Pull complete 
    e526190d8f2c: Pull complete 
    bcaa754c1ece: Pull complete 
    41118e0c01b4: Pull complete 
    ac3f2ab39238: Pull complete 
    cd9ffc55132f: Pull complete 
    efec50445663: Pull complete 
    598675d7eebd: Pull complete 
    fe4e66a2587e: Pull complete 
    Digest: sha256:70a3b5de3bd8d408cde0b98382887dbb1af1ac6d42c606edccb7edc922a2fcef
    Status: Downloaded newer image for rabbitmq:management
    cf7e32deb6c3eaabfdc70dec5698bbf58102913185c7b7c7769f814a7a43d857
    
  3. 插件复制到容器内

    [root@coder webdata]# docker cp /webdata/rabbitmq/rabbitmq_delayed_message_exchange-3.8.0.ez rabbitmq:/plugins
    
  4. 重启容器

    [root@coder webdata]# docker restart rabbitmq
    rabbitmq
    

使用

  1. pom依赖

    <!--rabbitmq-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    
  2. 配置rabbitmq配置

    spring: 
        rabbitmq:
            password: domi
            username: domi
            addresses: 39.101.133.223
            port: 5672
            listener: # 手动ACK 
              direct:
                acknowledge-mode: manual
              simple:
                acknowledge-mode: manual
            virtual-host: /
    
  3. 配置类编写

    @Configuration
    public class MQConfig {
    
        @Bean
        public MessageConverter messageConverter() {
            Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
            jackson2JsonMessageConverter.setCreateMessageIds(true);
            return jackson2JsonMessageConverter;
        }
    
       
    	/**
         * @desc: 延时交换器,注意返回类型是CustomExchange
         * @return: org.springframework.amqp.core.TopicExchange
         * @auther: Michael Wong
         * @email:  michael_wong@yunqihui.net
         * @date:   2020/9/8 18:52
         * @update:
         */
        @Bean
        public CustomExchange customDlxExchange() {
            Map<String, Object> args = new HashMap<>();
            args.put("x-delayed-type", "direct");
            return new CustomExchange(MQKeyStatic.EXCHANGE_GOOD_CUSTOM_DLX,"x-delayed-message",true,false,args);
        }
    
        /**
         * @desc: 商品上下架队列
         * @return: org.springframework.amqp.core.Queue
         * @auther: Michael Wong
         * @email:  michael_wong@yunqihui.net
         * @date:   2020/9/5 14:46
         * @update:
         */
        @Bean
        public Queue goodOnShelfDlxQueue() {
            return new Queue(MQKeyStatic.QUEUE_GOOD_DLX_GOOD, true, false, false, null);
        }
    
        @Bean
        public Binding dlxExchangeBindingGoodOnShelfDlx() {
            return BindingBuilder.bind(goodOnShelfDlxQueue()).to(customDlxExchange()).with(MQKeyStatic.ROUTING_GOOD_DLX_GOOD).noargs();
        }
        
    }
    
  4. 生产者发送消息

    // 延迟队列,上下架
            JSONObject messageObject = new JSONObject();
            messageObject.put("goodId", good.getId());
            messageObject.put("onShelf", 1);
            long expirStart = good.getStartSellingTime().getTime() - System.currentTimeMillis();
            rabbitTemplate.convertAndSend(MQKeyStatic.EXCHANGE_GOOD_CUSTOM_DLX,MQKeyStatic.ROUTING_GOOD_DLX_GOOD,messageObject, message -> {
                // 这里的失效时间是long类型,普通的TTL方式的类型是String类型
                message.getMessageProperties().setHeader("x-delay",expirStart);
                return message;
            });
            messageObject.put("onShelf", 0);
            long expirEnd = good.getEndSellingTime().getTime() - System.currentTimeMillis();
            rabbitTemplate.convertAndSend(MQKeyStatic.EXCHANGE_GOOD_CUSTOM_DLX,MQKeyStatic.ROUTING_GOOD_DLX_GOOD,messageObject, message -> {
    			// 这里的失效时间是long类型,普通的TTL方式的类型是String类型
                message.getMessageProperties().setHeader("x-delay",expirEnd);
                return message;
            });
    
  5. 消费者消费消息

    @Slf4j
    @Component
    public class MQListener {
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Autowired
        private IGoodService goodService;
    
        /**
         * @desc: 商品上下架处理队列监听
         * @param message
         * @param channel
         * @return: void
         * @auther: Michael Wong
         * @email:  michael_wong@yunqihui.net
         * @date:   2020/9/9 15:59
         * @update:
         */
        @RabbitListener(queuesToDeclare =
        @Queue(name = MQKeyStatic.QUEUE_GOOD_DLX_GOOD, durable = "true", exclusive = "false", autoDelete = "false"))
        public void goodOnShelfListener(Message message, Channel channel) throws IOException {
            log.info("开始监听到good:{}",message);
    
            if (idempotentMessage(message, channel)) {
                return;
            }
    
            log.info(" 幂等 完成");
            JSONObject messageJson = JSONObject.parseObject(new String(message.getBody()));
            Integer onShelf = messageJson.getInteger("onShelf");
            Integer goodId = messageJson.getInteger("goodId");
            try {
                Good good = goodService.getById(goodId);
                if (good == null) {
                    log.info("null ack message");
                    ackMessage(message,channel);
                }
                
                if (onShelf == 1) {
                    // 上架
                    goodService.onShelf(goodId);
                } else {
                    // 下架
                    goodService.downShelf(goodId);
                }
                ackMessage(message,channel);
            } catch (Exception e) {
                channel.basicRecover(false);
            }
        }
    
        /**
         * @param message
         * @param channel
         * @desc: 幂等消息
         * @return: boolean
         * @auther: Michael Wong
         * @email: michael_wong@yunqihui.net
         * @date: 2020/9/7 10:43
         * @update:
         */
        private boolean idempotentMessage(Message message, Channel channel) throws IOException {
            String messageId = message.getMessageProperties().getMessageId();
            String rediskey = RedisKeyStatic.IDEMPOTENT_MESSAGE + messageId;
            long deliveryTag = message.getMessageProperties().getDeliveryTag();
            if (stringRedisTemplate.hasKey(rediskey)) {
                channel.basicReject(deliveryTag, false);
                return true;
            }
            return false;
        }
    
        /**
         * @param message
         * @param channel
         * @desc: 确认消息公共方法
         * @return: void
         * @auther: Michael Wong
         * @email: michael_wong@yunqihui.net
         * @date: 2020/9/5 16:51
         * @update:
         */
        private void ackMessage(Message message, Channel channel) throws IOException {
            try {
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                String rediskey = RedisKeyStatic.IDEMPOTENT_MESSAGE + message.getMessageProperties().getMessageId();
                stringRedisTemplate.opsForValue().set(rediskey, "1", 3, TimeUnit.HOURS);
            } catch (Exception e) {
                log.error("接收消息失败,重新放回队列,message:{}", message);
                // 解决方案,剔除此消息,然后记录到db中去补偿
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            }
        }
    }
    
    

    在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值