五一期间,原本计划写两篇文章,同时阅读一本技术类书籍,然而由于自律性不足,我未能打开电脑,计划以失败告终。与大佬之间的差距显而易见,他们勤奋工作,比我只优秀,这让我深感惭愧。
知耻而后勇,我决心再次出发。我对实践类的东西比较感兴趣,既能够学习到知识,又能让技术落地,如果能搞出个demo就更好不过了。原本不知道应该分享什么主题,幸运的是,最近项目正在紧急招人,我有幸成为面试官,那么我就给大家分享一道面:“如何实现延时队列?”
接下来,我会介绍多种实现延时队列的思路,并在文末提供几种实现方式的GitHub地址。其实并没有哪种方式绝对的好与坏,只看它适用于什么业务场景,技术这东西没有最好的只有最合适的。
那么,什么是延时队列呢?顾名思义,它首先要具有队列的特性,然后再给它附加一个延迟消费队列消息的功能,也就是说可以指定队列中的消息在哪个时间点被消费。
延时队列在项目中的应用相当广泛,尤其在电商类平台:
1. 订单成功后,若在30分钟内没有支付,自动取消订单。
2. 外卖平台发送订餐通知,下单成功后60秒内给用户推送短信。
3. 如果订单一直处于某一个未完结状态时,及时处理关单,并退还库存。
4. 淘宝新建商户一个月内还没上传商品信息,将冻结商铺等。
以上这些场景都可以应用延时队列解决。
我的观点是:工作上能用JDK自带API实现的功能,就不要轻易自己重复造,或者引入三方中间件。一方面自己封装很容易出问题(大佬除外),再加上调试验证会产生许多不必要的工作量;另一方面一旦接入三方的中间件就会让系统复杂度成倍的增加,维护成本也大大的增加。
JDK中提供了一组实现延迟队列的API,位于Java.util.concurrent包下的DelayQueue。DelayQueue是一个BlockingQueue(阻塞)队列,它本质就是封装了一个PriorityQueue(优先队列),PriorityQueue内部使用完全二叉堆来实现队列元素排序。我们在向DelayQueue队列中添加元素时,会给元素一个Delay(延迟时间)作为排序条件,队列中最小的元素会优先放在队首。队列中的元素只有到了Delay时间才允许从队列中取出。
我们先来简单实现一下看看效果,添加三个订单进DelayQueue,分别设置订单在当前时间的5秒、10秒、15秒后取消。要实现DelayQueue延时队列,队中元素需要实现Delayed接口,这个接口里只有一个getDelay方法,用于设置延期时间。订单类中的compareTo方法负责对队列中的元素进行排序。DelayQueue的put方法是线程安全的,因为put方法内部使用了ReentrantLock锁进行线程同步。DelayQueue还提供了两种出队的方法poll()和take(),poll()为非阻塞获取,没有到期的元素直接返回null;take()为阻塞方式获取,没有到期的元素线程将会等待。
上面只是简单的实现入队与出队的操作,实际开发中会有专门的线程负责消息的入队与消费。执行后看到结果如下,Order1、Order2、Order3分别在5秒、10秒、15秒后被执行,至此就用DelayQueue实现了延时队列。
除了DelayQueue,还有其他几种实现延时队列的方式。比如Quartz是一款非常经典的任务调度框架,在Redis、RabbitMQ还未广泛应用时,超时未支付取消订单功能都是由定时任务实现的。再如Redis的数据结构Zset,利用它的score属性也可以实现延迟队列的效果。还有RabbitMQ也可以通过其TTL和DXL两个属性间接实现延迟队列。每种方式都有其适用的场景和优缺点。