延时任务
概述
在工作中,我们可能会接到一些需求,需要我们做一些延时的处理,用户使用一个功能,处理业务逻辑时,不是立即执行的,可能是用户设置一个时间,过了这个时间后才执行相应的业务逻辑。
举例
case 1
在医院中某床位安装了一个智能看护床垫设备,当床垫监测到床上的人离开床后,会向服务器发送一条离床消息,当离床的时间超过了用户设定的阈值时,需要产生一条报警数据。
case 2
订单超时的关闭,当用户下单后,超过一定的时长未付款,则自动关闭该订单
分析
处理这些业务功能时,都需要流程开始时进行计时,到时间后,执行后续业务逻辑
实现方案
Redis的Key失效机制
当流程开始时,我们向Redis中放入一个key,并设置失效的时间,设置key失效监听器,当监听到key失效时,通过key的名称区分,执行相应的业务逻辑。
在之前实现类似功能时,我采用该种方法去实现,发现了一些问题:
缺点:失效时间有一定的延迟
像case1中,可能需要对失效的时间精确度有很高的要求,因为报警的时间需要越精确越好,但是实际测试中出现10s,甚至20s的延迟
然后查找Redis官方对expire的说明:
过期精度
在 Redis 2.4 及以前版本,过期期时间可能不是十分准确,有0-1秒的误差。
从 Redis 2.6 起,过期时间误差缩小到0-1毫秒。
失效机制的说明:
Redis如何淘汰过期的keys
Redis keys过期有两种方式:被动和主动方式。
主动:
当一些客户端尝试访问它时,key会被发现并主动的过期。
被动:
当然,这样是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间。所有这些过期的keys将会从密钥空间删除。
具体就是Redis每秒10次做的事情:
- 测试随机的20个keys进行相关过期检测。
- 删除所有已经过期的keys。
- 如果有多于25%的keys过期,重复步奏
这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的keys的百分百低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys。
根据官方的说明,Redis每秒做10次,去随机检测20个key是否过期,也即是说100毫秒检测20个key,然后我就不太能理解,这样如何能将过期时间缩小到0-1毫秒呢?根据这种方式,1s失效200个key,假如我有2000个key的情况,怎么看也不能把误差缩小到0-1毫秒吧。。。
既然被动失效机制无法满足需求,那就采用主动失效的机制
在后台创建一个静态的ConcurrentHashMap(普通的HashMap可能会出现并发修改异常),每次放入一个expire key时,将这个key也放入这个map中,然后开启一个线程,每秒去遍历这个map中的key值,遍历一个就去Redis中查询这个key,根据主动失效的机制,可以将误差降到非常低的状态
性能分析
根据百度百科,Redis读的速度是110000次/s,所以在key不多的情况下,每秒去遍历似乎造成不了太大的性能消耗,但是可能会造成key集中的失效,这个对Redis性能的消耗情况有待确认,但是大量的key失效时,根据业务的需求,可能会需要向数据库写入大量数据,需要注意。
环形队列处理
该种方案是看云析学院的公开课时学到的,这种方案首先需要一个环形队列,队列长度为3600,理论上误差在1s以内。
环形队列长度在3600,那么我们设置一个指针,每秒钟挪动一格,移动3600格需要1h
然后在每格中放入一个Set集合,存储我们定义的延时任务对象
public class MyTask{
private int hours;
void do(){
//执行业务逻辑
}
}
存储任务:
其中的hours代表延迟的小时数,假如一个任务需要延时的时间超过了一个小时,那么我们就将小时数存入hours,不满一个小时的时间,查询指针当前的位置,通过计算,放入队列中
执行任务
当指针指向某一格时,遍历该Set集合中的全部对象,当hours不为0时,hours-1,hours为0时,执行其中的do方法
优点
(1)无需再轮询全部任务,效率高
(2)时效性好,精确到秒(控制timer移动频率可以控制精度)
思考
是否能将任务设置为线程,执行时,使用线程池执行