redis之二十二 -- 实现延迟队列的两种方法

延迟队列是指把当前要做的事情,往后推迟一段时间再做。

延迟队列在实际工作中和面试中都比较常见,它的实现方式有很多种,然而每种实现方式也都有它的优缺点,接下来我们来看。

延迟队列的使用场景

延迟队列的常见使用场景有以下几种:

  1. 超过 30 分钟未支付的订单,将会被取消
  2. 外卖商家超过 5 分钟未接单的订单,将会被取消
  3. 在平台注册但 30 天内未登录的用户,发短信提醒

等类似的应用场景,都可以使用延迟队列来实现。

常见实现方式

Redis 延迟队列实现的思路、优点:目前市面上延迟队列的实现方式基本分为三类,第一类是通过程序的方式实现,例如 JDK 自带的延迟队列 DelayQueue,第二类是通过 MQ 框架来实现,例如 RabbitMQ 可以通过 rabbitmq-delayed-message-exchange 插件来实现延迟队列,第三类就是通过 Redis 的方式来实现延迟队列。

程序实现方式

JDK 自带的 DelayQueue 实现延迟队列,代码如下:

public class DelayTest {
    public static void main(String[] args) throws InterruptedException {
        DelayQueue delayQueue = new DelayQueue();
        delayQueue.put(new DelayElement(1000));
        delayQueue.put(new DelayElement(3000));
        delayQueue.put(new DelayElement(5000));
        System.out.println("开始时间:" +  DateFormat.getDateTimeInstance().format(new Date()));
        while (!delayQueue.isEmpty()){
            System.out.println(delayQueue.take());
        }
        System.out.println("结束时间:" +  DateFormat.getDateTimeInstance().format(new Date()));
    }

    static class DelayElement implements Delayed {
        // 延迟截止时间(单面:毫秒)
        long delayTime = System.currentTimeMillis();
        public DelayElement(long delayTime) {
            this.delayTime = (this.delayTime + delayTime);
        }
        @Override
        // 获取剩余时间
        public long getDelay(TimeUnit unit) {
            return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }
        @Override
        // 队列里元素的排序依据
        public int compareTo(Delayed o) {
            if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
                return 1;
            } else if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
                return -1;
            } else {
                return 0;
            }
        }
        @Override
        public String toString() {
            return DateFormat.getDateTimeInstance().format(new Date(delayTime));
        }
    }
}

程序执行结果:

开始时间:2019-6-13 20:40:38
2019-6-13 20:40:39
2019-6-13 20:40:41
2019-6-13 20:40:43
结束时间:2019-6-13 20:40:43

优点

  1. 开发比较方便,可以直接在代码中使用
  2. 代码实现比较简单

缺点

  1. 不支持持久化保存
  2. 不支持分布式系统

MQ 实现方式

RabbitMQ 本身并不支持延迟队列,但可以通过添加插件 rabbitmq-delayed-message-exchange 来实现延迟队列。

优点

  1. 支持分布式
  2. 支持持久化

缺点

框架比较重,需要搭建和配置 MQ。

Redis 实现方式

Redis 是通过有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。

优点

  1. 灵活方便,Redis 是互联网公司的标配,无序额外搭建相关环境;
  2. 可进行消息持久化,大大提高了延迟队列的可靠性;
  3. 分布式支持,不像 JDK 自身的 DelayQueue;
  4. 高可用性,利用 Redis 本身高可用方案,增加了系统健壮性。

缺点

需要使用无限循环的方式来执行任务检查,会消耗少量的系统资源。

结合以上优缺点,我们决定使用 Redis 来实现延迟队列,具体实现代码如下。

代码实战

本文我们使用 Python 语言来实现延迟队列,延迟队列的实现有两种方式:第一种是利用 zrangebyscore 查询符合条件的所有待处理任务,循环执行队列任务。第二种实现方式是每次查询最早的一条消息,判断这条信息的执行时间是否小于等于此刻的时间,如果是则执行此任务,否则继续循环检测。

方式一

一次性查询所有满足条件的任务,循环执行,代码如下:

from redis import StrictRedis
redis_cli = StrictRedis(host="xxxxx", port=xx, password="xx", db=xx, decode_responses=True)
import time


class DelayQueue:
    """
    一次性查出所有符合条件的进行消费
    """

    def __init__(self):
        self.queue_key = "delay_all"

    def do_publish(self):
        now = time.time()
        redis_cli.zadd(self.queue_key, {"key30": now + 30})
        redis_cli.zadd(self.queue_key, {"key10": now + 10})
        redis_cli.zadd(self.queue_key, {"key3": now + 3})
        redis_cli.zadd(self.queue_key, {"key5": now + 5})
        redis_cli.zadd(self.queue_key, {"key8": now + 8})
        data = redis_cli.zrangebyscore(self.queue_key, now, now + 50)
        print("add success, data is: >>>>>> %s" % data)
        self.consume()

    # 延迟队列消费
    def consume(self):
        while True:
            # 上一秒
            last_second = time.time() - 1
            now_second = time.time()
            data = redis_cli.zrangebyscore(self.queue_key, last_second, now_second)
            # print("data in delay queue is >>>>>>>: %s" % data)
            if data:
                # 消费
                for each in data:
                    print("消费的是:%s" % each)
            # 删除已经消费的
            redis_cli.zremrangebyscore(self.queue_key, last_second, now_second)
            time.sleep(1)

if __name__ == '__main__':
    d = DelayQueue()
    d.do_publish()

结果

add success, data is: >>>>>> ['key3', 'key5', 'key8', 'key10', 'key30']
消费的是:key3
消费的是:key5
消费的是:key8
消费的是:key10
消费的是:key30

方式二

每次查询最早的一条任务,与当前时间判断,决定是否需要执行,实现代码如下:

class DelayFirst:
    """
    每次只查最早的一条任务
    """

    def __init__(self):
        self.queue_key = "delay_first"

    def publish(self):
        now = time.time()
        redis_cli.zadd(self.queue_key, {"key30": now + 30})
        redis_cli.zadd(self.queue_key, {"key10": now + 10})
        redis_cli.zadd(self.queue_key, {"key3": now + 3})
        redis_cli.zadd(self.queue_key, {"key5": now + 5})
        redis_cli.zadd(self.queue_key, {"key8": now + 8})
        data = redis_cli.zrangebyscore(self.queue_key, now, now + 50)
        print("add success, data is: >>>>>> %s" % data)
        self.do_consume()

    # 延迟队列消费
    def do_consume(self):
        while True:
            data = redis_cli.zrange(self.queue_key, 0, 0)
            print("data in delay queue is >>>>>>>: %s" % data)
            if data:
                # 消费
                for each in data:
                    # 获取时间,判断时间是否比当前时间小
                    cur_score = redis_cli.zscore(self.queue_key, each)
                    if cur_score <= time.time():
                        print("消费的是:%s" % each)
                        # 删除已经消费的
                        redis_cli.zrem(self.queue_key, each)
            time.sleep(1)


if __name__ == '__main__':
    # d = DelayQueue()
    # d.do_publish()

    d = DelayFirst()
    d.publish()

 

以上程序执行结果和实现方式一相同,结果如下:

add success, data is: >>>>>> ['key3', 'key5', 'key8', 'key10', 'key30']
data in delay queue is >>>>>>>: ['key3']
data in delay queue is >>>>>>>: ['key3']
data in delay queue is >>>>>>>: ['key3']
data in delay queue is >>>>>>>: ['key3']
消费的是:key3
data in delay queue is >>>>>>>: ['key5']
消费的是:key5
data in delay queue is >>>>>>>: ['key8']
data in delay queue is >>>>>>>: ['key8']
data in delay queue is >>>>>>>: ['key8']
消费的是:key8
data in delay queue is >>>>>>>: ['key10']
data in delay queue is >>>>>>>: ['key10']
消费的是:key10
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
data in delay queue is >>>>>>>: ['key30']
消费的是:key30

其中,执行间隔代码 time.sleep(1) 可根据实际的业务情况删减或配置。

小结

本文我们介绍了延迟队列的使用场景以及各种实现方案,其中 Redis 的方式是最符合我们需求的,它主要是利用有序集合的 score 属性来存储延迟执行时间,再开启一个无限循环来判断是否有符合要求的任务,如果有的话执行相关逻辑,没有的话继续循环检测。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值