rabbitmq 死信队列实现延时消息

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天

前言

在电商开发中我们经常会遇到这样的一些功能需求。例如,当一个订单在30分钟后未支付就需要关闭这个订单;当一个订单7天后用户没有点击确认收货,就系统自动确认。我们可以把这类任务归纳为是一个系统事件,通过触发某个时间条件来执行该事件。我们可以先抛开rabbitmq这个中间件,思考下应该如何设计这个任务。

首先这些事件的触发不应该影响我们的主流程,比如用户生成了订单,但是即使用户不支付,他依然可以去做别的事情,比如浏览商品或者参与其它促销活动。也就是这类事件相对于我们的用户主线程它是独立的,非阻塞的,异步的。那么我们是否可以通过另起一个线程来实现这个事件呢?我们可以设计一个线程池同时使用延时的线程来执行事件。

但是这样似乎有一些问题。例如如果我们任务触发的事件时间是比较长的,那么每一个小的任务都需要一个线程资源来维持,那么我们的线程池就需要去考虑线程资源的占用和拒绝的策略。另外由于这种设计我们的线程是和java服务完全绑定的,在一般条件下宕机或者重启,这些在内存中的线程都会丢失销毁。

因此我们需要一个类似异步线程的设计,它最好能够独立于我们的Java服务单独运行,这样可以降低宕机和重启带来的危害。所以我们想到了消息队列rabbitmq,将一段时间后需要执行的事件投递到队列中,然后一段时间后再接受的队列的消息。

正文

  rabbitmq中一般是使用死信队列来实现延时队列的功能

​ 

首先介绍什么是死信队列,这里我们需要先理解rabbitmq中的TTL的概念。

TTL通俗说就是过期时间和redis中一个缓存的生命周期一样。在rabbitmq中允许我们对消息和队列设置TTL,主要是通过设置x-message-ttl来实现的。应用在队列上则是对队列中的所有消息都生效

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 60000);
channel.queueDeclare("myqueue", false, false, false, args);
复制代码

上面就代表的是一个设置消息过期时间为60s的队列,那么所有这个队列的消息都只能在队列中存储最多60s,超过则被丢弃。 我们也可以对单独的消息设置过期时间

byte[] messageBodyBytes = "Hello, world!".getBytes();
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                                   .expiration("60000")
                                   .build();
channel.basicPublish("my-exchange", "routing-key", properties, messageBodyBytes);
复制代码

上面表示这条消息在队列中最多存储60秒,如果没有消费者消费,则会被丢弃。 如果把过期参数设置在队列上,那么超过过期时间,这个队列就会自动删除。

扫描二维码关注公众号,回复: 13788585 查看本文章
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-expires", 1800000);
channel.queueDeclare("myqueue", false, false, false, args);
复制代码

上面表示这个队列在30m后会被过期删除。

下面接着死信队列来讲,在rabbitmq中超过ttl的消息叫做死信,那么一个都是死信消息的队列则被叫做死信队列。所以延时队列就是利用了这个特性。首先声明一个死信队列,这个队列不能有消费者监听,这样可以保证一到过期时间,消息会被丢弃。接下来我们要做的就是接受这些死信重新把它们投递到队列中消费。

rabbitmq提供了一种Dead Letter Exchanges,死信交换,它的作用就是“死信”的重发布。也就是当消息过期时,消息并不是简单丢弃,而是重新投递到一个交换器中进行新的消费。rabbitmq给我们提供了两个参数用于重发布。分别是x-dead-letter-exchange这个是重新投递的交换器,x-dead-letter-routing-key这个是重新投递的路由,可以指向到具体的队列,方便我们做监听。

下面是延时消息的代码设计。

/*
 * @param
 * @return org.springframework.amqp.core.TopicExchange
 * @description 定义一个死信交换器
 * @author zhou
 * @create 2021/7/12 12:27
 **/
@Bean
public TopicExchange deadExchange() {
    return new TopicExchange("dead_exchange", false, false);
}

/**
 * @param
 * @return org.springframework.amqp.core.Queue
 * @description 死信队列
 * @author zhou
 * @create 2021/7/12 12:31
 **/
@Bean
public Queue deadQueue() {
    HashMap<String, Object> args = new HashMap<>();
    //要发送的交换器
    args.put("x-dead-letter-exchange", "dlx_exchange");
    //路由键
    args.put("x-dead-letter-routing-key", "web.expire");
    return new Queue("dead_queue", false, false, false, args);
}

/**
 * @param
 * @return org.springframework.amqp.core.Binding
 * @description 建立绑定关系
 * @author zhou
 * @create 2021/7/12 12:38
 **/
@Bean
public Binding deadBinding(TopicExchange deadExchange, Queue deadQueue) {
    return BindingBuilder
            .bind(deadQueue)
            .to(deadExchange)
            .with("dlx");
}
复制代码

首先我们声明一个普通的交换器,一个死信队列(记住这个队列没有消费者监听),队列中声明了死信交换出去的指定交换器和指定的路由。然后将这个普通交换器和死信队列通过一个路由绑定。

public void delayInfo(String message) {
    LocalDateTime now = LocalDateTime.now();
    rabbitTemplate.convertAndSend("dead_exchange", "dlx", message,
            m -> {
                m.getMessageProperties().setExpiration(String.valueOf(5000));
                return m;
            }
    );
    log.info("发送延时时间:[{}]", now.toString());
}
复制代码

这里投递了一条标记TTL的消息,我们设置为5S。同时指定它投递到我们之前声明的普通交换器和死信队列中。

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "dlx_queue", durable = "false", autoDelete = "true", exclusive = "false"),
        exchange = @Exchange(value = "dlx_exchange", type = ExchangeTypes.TOPIC),
        key = {"web.expire"}
))
public void delayListener(String message) {
    log.info("delay info:[{}]", message);
}
复制代码

这里是我们的消费者部分。首先我们需要声明一个交换器,它的名称要和之前x-dead-letter-exchange中写的一致。然后我们要声明一个队列,队列的名字可自定义,但是队列和交换器之间绑定的路由需要和x-dead-letter-routing-key中写的一致。

下面我们发送一条消息,可以看到下面的日志输出,之间间隔的时间差不多是5s。

1649860471(1).png

这样一个简单的通过死信队列实现的rabbitmq延时队列就完成了。

猜你喜欢

转载自juejin.im/post/7086098519450189854