最近业务上有个需求,背景以下:有一个养殖类游戏,经过给养的宠物喂食来升级,一次喂食后,宠物须要花4个小时吃完。如今有个新需求,可使用道具卡来丰富玩法。道具卡有两种,一种是加速卡,一种是自动喂食卡。加速卡会使吃食的时间缩短两个小时,自动喂食卡能够在宠物吃完当前喂食的狗粮后系统帮助其自动喂食一次。redis
业务需求里的自动喂食就是一种典型的延时任务。延时任务是指须要在指定的将来的某个时间点自动触发。与之相似的场景还有:数据库
对于延时任务,常见的方案就是扫表。扫表就是用一个后台进程,每隔一段时间扫描数据库的整张数据表,判断每一个任务是否达到触发的条件。若是达到条件就执行相应的业务。扫描全表对数据库压力较大,因此通常选择扫从库。扫表的最大优点是实现起来比较简单,并且数据自己存在DB里,所以也不用担忧任务数据会丢失,失败的任务能够下次扫描时再重入。可是扫表存在如下问题:网络
扫表最大的问题就是会有延迟,不能再指定的时间里触发,对于时效性高的场景,这种方案是不能知足需求的。数据结构
目前,有些MQ消息队列能够支持延时消息,如kafka。延时消息就是消息发送后,能够指定在多少时间以后才会发送到消费者那里。这个方案,开发成本也很小,不过须要使用的中间件能支持延时消息。并且该方案也存在一个瓶颈就是若是,延时任务须要从新更新时间就作不到了,由于消息已经发出去了,收不回了。架构
用环形队列作成时间片,环形队列的每一个格子里维护一个链表。每一个时刻有一个当前指针指向环形队列某个格子,定时器每超时一次,就把当前指针指向下环形队列的下一个格子。而后处理这个格子保存的链表里的任务。若是只是这样维护,若是要作到秒级的粒度,时间长度最长一天,那么这个环形队列就会很是大。所以,有人又有人改进了一下,当存在任务进入队列时,就用时间长度除以环形队列的长度,记为圈数。这样每次遍历到该元素时,将圈数减一,若是减一后为0就执行改任务,否者不执行。并发
kafka的延时消息的内部实现就是采用时间片轮询的方式来实现的。异步
对于时间跨度很是大的场景,若是使用这种方法会致使链表上的元素很是多,遍历链表的开销也不小,甚至在一个时间片内遍历不完。所以,又有了进一步的改进,将时间片分为不一样粒度的。好比,粒度为小时的时间轮,粒度为分钟的时间轮,粒度为秒钟的时间轮。小时里的时间轮达到触发的条件后会放到分钟的时间轮里,分钟的时间轮到达触发的条件后会放到秒的时间轮里。(图片来自网络,侵删)分布式
该方案时间片存放在内存,所以轮询起来效率很是高,也能够根据不一样的粒度调整时间片,所以也很是灵活。可是该方案须要本身实现持久化与高可用,以及对储存的管理,若是没有现成的轮子开发耗时会比较长。高并发
Redis实现延时任务,是经过其数据结构ZSET来实现的。ZSET会储存一个score和一个value,能够将value按照score进行排序,而SET是无序的。ui
延时任务的实现分为如下几步来实现:
(1) 将任务的执行时间做为score,要执行的任务数据做为value,存放在zset中; (2) 用一个进程定时查询zset的score分数最小的元素,能够用ZRANGEBYSCORE key -inf +inf limit 0 1 withscores命令来实现; (3) 若是最小的分数小于等于当前时间戳,就将该任务取出来执行,不然休眠一段时间后再查询
redis的ZSET是经过跳跃表来实现的,复杂度为O(logN),N是存放在ZSET中元素的个数。用redis来实现能够依赖于redis自身的持久化来实现持久化,redis的集群来支持高并发和高可用。所以开发成本很小,能够作到很实时。
扫表的方法延时过高不能知足实时的需求,团队目前使用的消息队列还不支持延时消息队列,时间轮的方法开发起来很耗时,所以最终选择了Redis来实现。
前面介绍了Redis实现延时任务的原理,为了实现更高的并发还须要在原理的基础上进行设计。接下来将详细阐述具体的实现。架构设计图以下:
说明:
在延时任务的基础上,本次业务还有一个需求,就是延时任务若是尚未到达执行时间,那么该延时任务的时间是能够被更改的。为了实现这个需求,咱们另外给每一个用户维护一个ZSET,这个ZSET中存放该用户全部的延时任务。为了便于描述,咱们将这个ZSET称为ZSET-USER。若是用户须要修改其延时任务,若是没有办法从总体的延时任务的ZSET中找到这个任务,而是即便能找到,也只能遍历这个ZSET,显然这种方法太慢,太耗资源。咱们采起的方法是从ZSET-USER中取出这个用户的延时任务,而后修改score,最后从新ZADD到延时任务ZSET和ZSET-USER中,ZADD会覆盖原来的任务,而score则发生了更新。这样看来,这个需求还只能经过Redis来实现。
本篇文章,借着业务需求的背景首先探讨了延时任务的业界实现方案,而后详细阐述了经过redis来实现延时任务方法,并分析了高并发,高可用的设计思路。