上一节我们介绍了基于Redis实现简单的延时消息队列,通过Redis的Zset特性对元素进行排序,实现了简单延时消息队列的功能。我们还提到过,通过消息队列的方式也可以实现延时消息的功能。
什么是消息队列?百度百科对它的定义相当简单,就一句话:
“消息队列”是在消息的传输过程中保存消息的容器。
我们目前在工作中接触的消息队列中间件,无论是性能,稳定性,对高级特性的支持度都是比较成熟和稳定的。常用的和知名度比较高的消息队列有开源社区的,也有商业版的,一般有以下几种:
- ActiveMQ:Apache旗下的老牌消息引擎;
- RabbitMQ:AMQP的默认实现;
- Kafka:AMQP的默认实现;
- RocketMQ:阿里系的;
- 以及实现了JMS(Java Message Service)标准的OpenMQ;
消息队列中间件的核心功能是队列(Queue),所以消息都是发送到这个队列上,需要消费消息的时候就去监听这个队列就可以了。
那么,什么是延时队列呢?
延时队列,最重要的特性就体现在它的延时属性上,跟普通的队列不一样的是,普通队列中的元素总是等着希望被早点取出处理,而延时队列中的元素则是希望被在指定时间得到取出和处理,所以延时队列中的元素是都是带时间属性的,通常来说是需要被处理的消息或者任务。
简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
RabbitMQ有一个高级特性就是TTL(Time To Live),它是消息或者队列的一个重要属性,单位是毫秒。它决定了一条消息或者一个队列里的所有消息的最大存活时间,换句话说,如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用。
给一条消息设置TTL和给一个队列设置TTL是有区别的,那么它们的区别在哪里呢?
如果设置了队列的TTL属性,那么一旦消息过期,就会被队列丢弃,而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间。
下面我们通过演示一个简单例子来验证下:
- RabbitMQ的延时消息队列功能;
- 如何给一个消息设置TTL,如何给一个队列设置TTL;
- 一条消息设置TTL与一个消息队列设置TTL的区别;
整个消息的流转如下图:
消息流转
- 生产者根据routing key将消息发送到延时队列交换机上;
- 延时队列交换机根据routing key将消息转发到延时消息队列中;
- 当消息过期后,消息根据routing key将消息发送到死信交换机上;
- 死信交换机根据routing key将消息转发到死信队列中;
- 消费者监听死信队列,拿到过期的消息;
使用Docker快速的构建一套RabbitMQ环境,为了方便验证我们使用RabbitMQ的manage web界面:
docker pull rabbitmq:management
将RabbitMQ在Docker运行起来:
docker run -d --hostname rabbit-host --name rabbitmq-management -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password -p 15672:15672 -p 5672:5672 rabbitmq:management
这样就快速的构建了一个RabbitMQ环境。
新建一个工程delay-message-queue-rabbitmq,源代码地址将在文末附上
pom.xml
<?xml version="1.0" encoding="UTF-8"?>4.0.0com.delay.message.queuedelay-message-queue0.0.1com.delay.message.queuedelay-message-queue-rabbitmq0.0.1delay-message-queue-rabbitmqDemo project for Spring Boot1.8org.springframework.bootspring-boot-starter-amqporg.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-devtoolsruntimetrueorg.springframework.bootspring-boot-configuration-processortrueorg.projectlomboklomboktrueorg.springframework.bootspring-boot-starter-testtestorg.springframework.amqpspring-rabbit-testtestorg.springframework.bootspring-boot-maven-plugin
交换机与队列的绑定关系
@Configurationpublic class RabbitmqConfig { public static final String DELAY_EXCHANGE_NAME = "delay.queue.exchange"; public static final String DELAY_QUEUE_NAME = "delay.queue.business.queue"; public static final String DELAY_QUEUE_ROUTING_KEY = "delay.queue.routingkey"; public static final String DEAD_LETTER_EXCHANGE = "delay.queue.deadletter.exchange"; public static final String DEAD_LETTER_QUEUE_NAME = "delay.queue.deadletter.queue"; public static final String DEAD_LETTER_QUEUE_ROUTING_KEY = "delay.queue.deadletter.routingkey"; // 声明延时Exchange @Bean("delayExchange") public DirectExchange delayExchange() { return new DirectExchange(DELAY_EXCHANGE_NAME); } // 声明死信Exchange @Bean("deadLetterExchange") public DirectExchange deadLetterExchange() { return new DirectExchange(DEAD_LETTER_EXCHANGE); } // 声明延时队列 // 并绑定到对应的死信交换机 @Bean("delayQueue") public Queue delayQueue() { Map args = new HashMap<>(); // x-dead-letter-exchange 这里声明当前队列绑定的死信交换机 args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE); // x-dead-letter-routing-key 这里声明当前队列的死信路由key args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUE_ROUTING_KEY); // x-message-ttl 声明队列的TTL,单位是毫秒 args.put("x-message-ttl", 60000); return QueueBuilder.durable(DELAY_QUEUE_NAME).withArguments(args).build(); } // 声明死信队列 @Bean("deadLetterQueue") public Queue deadLetterQueue() { return new Queue(DEAD_LETTER_QUEUE_NAME); } // 声明延时队列绑定关系 @Bean public Binding delayBinding(@Qualifier("delayQueue") Queue queue, @Qualifier("delayExchange") DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(DELAY_QUEUE_ROUTING_KEY); } // 声明死信队列绑定关系 @Bean public Binding deadLetterBinding(@Qualifier("deadLetterQueue") Queue queue, @Qualifier("deadLetterExchange") DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUE_ROUTING_KEY); }}
给延时队列设置TTL为60秒,意味着队列里的所有消息将在60秒后过期。
消息生产者
@Slf4j@Servicepublic class ProducerService { @Autowired private AmqpTemplate amqpTemplate; public void produceByQueueTTL(String orderId, long expiredTime) { amqpTemplate.convertAndSend(RabbitmqConfig.DELAY_EXCHANGE_NAME, RabbitmqConfig.DELAY_QUEUE_ROUTING_KEY, orderId); log.info("[produceByQueueTTL]order id:{}, delay {}s send success", orderId, expiredTime); } public void produceByMessageTTL(String orderId, long expiredTime) { //rabbit默认为毫秒级 MessagePostProcessor messagePostProcessor = new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { message.getMessageProperties().setExpiration(String.valueOf(expiredTime)); return message; } }; amqpTemplate.convertAndSend(RabbitmqConfig.DELAY_EXCHANGE_NAME, RabbitmqConfig.DELAY_QUEUE_ROUTING_KEY, orderId, messagePostProcessor); log.info("[produceByMessageTTL]order id:{}, delay {}s send success", orderId, expiredTime / 1000); }}
- produceByQueueTTL:不给每一条消息设置TTL,使用队列的TTL;
- produceByMessageTTL:不但使用队列的TTL,还给每一条消息设置一个TTL;
消息消费者
@Slf4j@Servicepublic class ConsumerService { private long time = new Date().getTime(); @RabbitListener(queues = RabbitmqConfig.DEAD_LETTER_QUEUE_NAME) @RabbitHandler public void consume(Message message, Channel channel) throws IOException { String orderId = new String(message.getBody()); long nowTime = new Date().getTime(); long expired = (nowTime - time) / 1000; time = nowTime; log.info("[consume]order id:{}, delay:{}s handle success", orderId, expired); }}
消息者主要监听死信队列,并将死信队列中的消息进行打印。
通过单元测试的形式发送消息
@Slf4j@SpringBootTest@RunWith(SpringJUnit4ClassRunner.class)public class ProducerServiceTest { @Autowired private ProducerService producerService; @Test public void produceByQueueTTL() throws IOException { Random random = new Random(1); for (int i = 0; i < 10; i++) { String orderId = "order-id-" + i; //log.info("order id:{}", orderId); int delay = random.nextInt(10); try { Thread.sleep(delay * 1000); } catch (InterruptedException e) { log.error("InterruptedException", e); } producerService.produceByQueueTTL(orderId, delay); } System.in.read(); } @Test public void produceByMessageTTL() throws IOException { for (int i = 0; i < 10; i++) { //int expiredTime = 65; int expiredTime = 20; String orderId = "order-id-" + i; //log.info("order id:{}, expired:{}", orderId, time); producerService.produceByMessageTTL(orderId, expiredTime * 1000); try { //log.info("no data will sleep"); Thread.sleep(500); } catch (InterruptedException e) { log.error("InterruptedException", e); } } System.in.read(); } @Test public void produceByMessageDiffTTL() throws IOException { Random random = new Random(1); for (int i = 0; i < 10; i++) { int expiredTime = random.nextInt(100); String orderId = "order-id-" + i; //log.info("order id:{}, expired:{}", orderId, time); producerService.produceByMessageTTL(orderId, expiredTime * 1000); try { //log.info("no data will sleep"); Thread.sleep(500); } catch (InterruptedException e) { log.error("InterruptedException", e); } } System.in.read(); }}
- produceByQueueTTL:随机的延时发送消息;
- produceByMessageTTL:给每条消息设置一个相同的TTL;
- produceByMessageDiffTTL:给每条消息随机的设置TTL;
运行produceByQueueTTL结果如下:
produceByQueueTTL结果
给每一条消息一个随机小于队列TTL(60秒)的TTL时间,每一个消息的过期时间是不同的,进入死信队列被消息的时间也是不同的。
运行produceByMessageTTL结果如下:
produceByMessageTTL结果
给每一条消息设置一个固定TTL为20秒的过期时间,该20秒过期时间小于队列的TTL(60秒)。每一条消息的过期时间都是相同的,在20秒过期时间后统一被消费者消费掉。
运行produceByMessageDiffTTL结果如下:
produceByMessageDiffTTL结果
给每一条消息随机的设置一个TTL,有的消息TTL大于队列的TTL(60秒),有的消息TTL小于队列的TTL(60秒),在61秒的时候所有进入延时队列的消息全部过期,过期的消息被转发到死信队列,消费者监听死信队列,消息掉了所有过期的消息。
通过以上演示,我们实现了基于RabbitMQ的延时消息队列功能,同时验证了当队列和消息都设置了TTL,那么消息的过期时间将取决于较小的TTL时间。
查看RabbitMQ的Manage界面我们发现创建了两个交换机和两个队列:
RabbitMQ交换机
RabbitMQ队列