RabbitMQ对延迟任务的两种实现方式及利弊
1.开门见山 两种方式分别为原生的死信队列 与 延迟插件安装而来的延迟交换机功能
2.死信队列原理不再概述,主要实现方式就是先扔入普通消息队列且设置消息的过期时间,一旦消息过期即进入死信队列。此时监听死信队列的消息即为消息的延迟消费;
队列-交换机配置类
package com.fawkes.cybereng.asset.mq.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * @Description 计划修-任务最大提前时间及最大延期时间的死信队列配置 * @Author 薛铁琪 * @CreateTime 2022/8/22 10:27 * @Version 1.0 */ @Configuration public class RabbitMQConfiguration { //队列名称 public final static String CYBERENG_ASSET_PRP_TASK_QUEUE = "cybereng-asset-prp-task-queue"; //交换机名称 public final static String CYBERENG_ASSET_PRP_TASK_EXCHANGE = "cybereng-asset-prp-task-exchange"; // routingKey public final static String CYBERENG_ASSET_PRP_TASK_ROUTINGKEY = "cybereng-asset-prp-task-routingkey"; //死信消息队列名称 public final static String CYBERENG_ASSET_PRP_TASK_QUEUE_DEAD = "cybereng-asset-prp-task-queue-dead"; //死信交换机名称 public final static String CYBERENG_ASSET_PRP_TASK_EXCHANGE_DEAD = "cybereng-asset-prp-task-exchange-dead"; //死信 routingKey public final static String CYBERENG_ASSET_PRP_TASK_ROUTINGKEY_DEAD = "cybereng-asset-prp-task-routingkey-dead"; //死信队列 交换机标识符 public static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange"; //死信队列交换机绑定键标识符 public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key"; @Autowired private CachingConnectionFactory connectionFactory; @Bean public Queue prpTaskQueue() { // 将普通队列绑定到死信队列交换机上 Map<String, Object> args = new HashMap<>(2); args.put(DEAD_LETTER_QUEUE_KEY, CYBERENG_ASSET_PRP_TASK_EXCHANGE_DEAD); args.put(DEAD_LETTER_ROUTING_KEY, CYBERENG_ASSET_PRP_TASK_ROUTINGKEY_DEAD); return new Queue(RabbitMQConfiguration.CYBERENG_ASSET_PRP_TASK_QUEUE, true, false, false, args); } //声明一个direct类型的交换机 @Bean DirectExchange prpTaskExchange() { return new DirectExchange(RabbitMQConfiguration.CYBERENG_ASSET_PRP_TASK_EXCHANGE); } //绑定Queue队列到交换机,并且指定routingKey @Bean Binding bindingDirectExchange() { return BindingBuilder.bind(prpTaskQueue()).to(prpTaskExchange()).with(CYBERENG_ASSET_PRP_TASK_ROUTINGKEY); } //创建配置死信队列 @Bean public Queue prpTaskQueueDead() { Queue queue = new Queue(CYBERENG_ASSET_PRP_TASK_QUEUE_DEAD, true, false, false); return queue; } //创建死信交换机 @Bean public DirectExchange prpTaskExchangeDead() { return new DirectExchange(CYBERENG_ASSET_PRP_TASK_EXCHANGE_DEAD); } //死信队列与死信交换机绑定 @Bean public Binding bindingDeadExchange() { return BindingBuilder.bind(prpTaskQueueDead()).to(prpTaskExchangeDead()).with(CYBERENG_ASSET_PRP_TASK_ROUTINGKEY_DEAD); } }
生产者
/** * COPYRIGHT HangZhou 99Cloud Technology Company Limited * All right reserved. */ package com.fawkes.cybereng.asset.mq.provider; import com.fawkes.cybereng.asset.mq.config.RabbitMQConfiguration; import com.fawkes.cybereng.asset.mq.message.PrpTaskTimeMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Date; /** * @Description * @Author 薛铁琪 * @CreateTime 2022/8/22 10:27 * @Version 1.0 */ @Component @Slf4j public class PrpTaskTimeProvider { @Autowired RabbitTemplate rabbitTemplate; public void submit(PrpTaskTimeMessage prpTaskTimeMessage) { log.info("扔入队列的任务信息{}", prpTaskTimeMessage); this.rabbitTemplate.convertAndSend( //发送至订单交换机 RabbitMQConfiguration.CYBERENG_ASSET_PRP_TASK_EXCHANGE, //routingKey RabbitMQConfiguration.CYBERENG_ASSET_PRP_TASK_ROUTINGKEY, prpTaskTimeMessage , message -> { // 如果配置了 params.put("x-message-ttl", 5 * 1000); // 那么这一句也可以省略,具体根据业务需要是声明 Queue 的时候就指定好延迟时间还是在发送自己控制时间 // 自己计算下超时时间(秒放入) Date current = new Date(); long ex = prpTaskTimeMessage.getPlanTimeAdvanceMax().getTime() - current.getTime(); log.info("延迟秒数{}", ex / 1000); message.getMessageProperties().setExpiration(ex + ""); return message; }); } }
消费者
package com.fawkes.cybereng.asset.mq.consumer; import com.fawkes.cybereng.asset.mq.config.RabbitMQConfiguration; import com.fawkes.cybereng.asset.mq.message.PrpTaskTimeMessage; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Map; /** * @Description * @Author 薛铁琪 * @CreateTime 2022/8/22 10:27 * @Version 1.0 */ @Component @Slf4j public class PrpTaskTimeConsumer { @RabbitListener( queues = RabbitMQConfiguration.CYBERENG_ASSET_PRP_TASK_QUEUE_DEAD , ackMode = "MANUAL" ) public void process(PrpTaskTimeMessage prpTaskTimeMessage, Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException { log.info("消费死信队列的任务信息{}", prpTaskTimeMessage); // TODO 判断消息类型 最大提前 最晚到期 // TODO 任务类型是否为自动 // 手动ack Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); // 手动签收 channel.basicAck(deliveryTag, false); } }
优:利用rabbitmq本身机制,不需要额外安装插件,性能好
缺:一个致命的问题就是消息顺序,不会按照延迟时间的先后顺序输出,而是按照queue本身先进先出的规则。即10秒延迟的消息如果是在20秒延迟消息后扔入的,那么也要等20秒延迟的消息输出后才能输出。除非消息的延迟时间是一致的否则无法满足业务要求。
3.延迟插件顾名思义,直接实现了延迟扔入队列的功能;
配置类
package com.fawkes.cybereng.asset.mq.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.CustomExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * @Description 计划修-任务最大提前时间及最大延期时间的死信队列配置 * @Author 薛铁琪 * @CreateTime 2022/8/22 10:27 * @Version 1.0 */ @Configuration public class RabbitMQConfiguration { //队列名称 public final static String CYBERENG_ASSET_PRP_TASK_QUEUE = "cybereng-asset-prp-task-queue"; //交换机名称 public final static String CYBERENG_ASSET_PRP_TASK_EXCHANGE = "cybereng-asset-prp-task-exchange"; // routingKey public final static String CYBERENG_ASSET_PRP_TASK_ROUTINGKEY = "cybereng-asset-prp-task-routingkey"; /** * 初始化延迟交换机 * * @return */ @Bean public CustomExchange delayedExchangeInit() { Map<String, Object> args = new HashMap<>(); // 设置类型,可以为fanout、direct、topic args.put("x-delayed-type", "direct"); // 第一个参数是延迟交换机名字,第二个是交换机类型,第三个设置持久化,第四个设置自动删除,第五个放参数 return new CustomExchange(CYBERENG_ASSET_PRP_TASK_EXCHANGE, "x-delayed-message", true, false, args); } /** * 初始化队列 * * @return */ @Bean public Queue delayedQueueInit() { return new Queue(CYBERENG_ASSET_PRP_TASK_QUEUE, true, false, false); } /** * 队列绑定到交换机 * * @param delayedSmsQueueInit * @param customExchange * @return */ @Bean public Binding delayedBindingQueue(Queue delayedSmsQueueInit, CustomExchange customExchange) { return BindingBuilder.bind(delayedSmsQueueInit).to(customExchange).with(CYBERENG_ASSET_PRP_TASK_ROUTINGKEY).noargs(); } }
生产者
/** * COPYRIGHT HangZhou 99Cloud Technology Company Limited * All right reserved. */ package com.fawkes.cybereng.asset.mq.provider; import com.fawkes.cybereng.asset.mq.config.RabbitMQConfiguration; import com.fawkes.cybereng.asset.mq.message.PrpTaskTimeMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Date; /** * @Description * @Author 薛铁琪 * @CreateTime 2022/8/22 10:27 * @Version 1.0 */ @Component @Slf4j public class PrpTaskTimeProvider { @Autowired RabbitTemplate rabbitTemplate; public void submit(PrpTaskTimeMessage prpTaskTimeMessage) { log.info("扔入队列的任务信息{}", prpTaskTimeMessage); this.rabbitTemplate.convertAndSend( //发送至订单交换机 RabbitMQConfiguration.CYBERENG_ASSET_PRP_TASK_EXCHANGE, //routingKey RabbitMQConfiguration.CYBERENG_ASSET_PRP_TASK_ROUTINGKEY, prpTaskTimeMessage , message -> { // 如果配置了 params.put("x-message-ttl", 5 * 1000); // 那么这一句也可以省略,具体根据业务需要是声明 Queue 的时候就指定好延迟时间还是在发送自己控制时间 // 自己计算下超时时间(秒放入) Date current = new Date(); Long ex = prpTaskTimeMessage.getPlanTimeAdvanceMax().getTime() - current.getTime(); log.info("延迟秒数{}", ex / 1000); message.getMessageProperties().setDelay(ex.intValue()); return message; }); } }
消费者
package com.fawkes.cybereng.asset.mq.consumer; import com.fawkes.cybereng.asset.mq.config.RabbitMQConfiguration; import com.fawkes.cybereng.asset.mq.message.PrpTaskTimeMessage; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Map; /** * @Description * @Author 薛铁琪 * @CreateTime 2022/8/22 10:27 * @Version 1.0 */ @Component @Slf4j public class PrpTaskTimeConsumer { @RabbitListener( queues = RabbitMQConfiguration.CYBERENG_ASSET_PRP_TASK_QUEUE , ackMode = "MANUAL" ) public void process(PrpTaskTimeMessage prpTaskTimeMessage, Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException { log.info("消费死信队列的任务信息{}", prpTaskTimeMessage); // TODO 判断消息类型 最大提前 最晚到期 // TODO 任务类型是否为自动 // 手动ack Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); // 手动签收 channel.basicAck(deliveryTag, false); } }
效果
利:解决了不通消息不通延迟时间的问题
弊:需要安装延迟插件,对rabbitmq重启有一定的风险。性能待观察!