上一节我们介绍了基于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属性,那么一旦消息过期,就会被队列丢弃,而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间。


spring event和消息队列的区别 消息队列和socket_Time



下面我们通过演示一个简单例子来验证下:

  • RabbitMQ的延时消息队列功能;
  • 如何给一个消息设置TTL,如何给一个队列设置TTL;
  • 一条消息设置TTL与一个消息队列设置TTL的区别;

整个消息的流转如下图:


spring event和消息队列的区别 消息队列和socket_Time_02

消息流转


  • 生产者根据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环境。


spring event和消息队列的区别 消息队列和socket_消息队列_03


新建一个工程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结果如下:


spring event和消息队列的区别 消息队列和socket_Time_04

produceByQueueTTL结果


给每一条消息一个随机小于队列TTL(60秒)的TTL时间,每一个消息的过期时间是不同的,进入死信队列被消息的时间也是不同的。

运行produceByMessageTTL结果如下:


spring event和消息队列的区别 消息队列和socket_消息队列实现socket 消息同步_05

produceByMessageTTL结果


给每一条消息设置一个固定TTL为20秒的过期时间,该20秒过期时间小于队列的TTL(60秒)。每一条消息的过期时间都是相同的,在20秒过期时间后统一被消费者消费掉。

运行produceByMessageDiffTTL结果如下:


spring event和消息队列的区别 消息队列和socket_spring_06

produceByMessageDiffTTL结果


给每一条消息随机的设置一个TTL,有的消息TTL大于队列的TTL(60秒),有的消息TTL小于队列的TTL(60秒),在61秒的时候所有进入延时队列的消息全部过期,过期的消息被转发到死信队列,消费者监听死信队列,消息掉了所有过期的消息。



spring event和消息队列的区别 消息队列和socket_消息队列实现socket 消息同步_07


通过以上演示,我们实现了基于RabbitMQ的延时消息队列功能,同时验证了当队列和消息都设置了TTL,那么消息的过期时间将取决于较小的TTL时间。

查看RabbitMQ的Manage界面我们发现创建了两个交换机和两个队列:


spring event和消息队列的区别 消息队列和socket_spring_08

RabbitMQ交换机


spring event和消息队列的区别 消息队列和socket_Time_09

RabbitMQ队列