背景数据库
在本身接触到的业务系统中,不少地方会有定时任务的需求,好比支付的交易超时自动关闭、链接超时、支付异步通知等等。常见的作法有:数组
1.考虑使用JDK中的Timer定时任务来实现缓存
2.经过封装quartz搭建专门的调度平台来管理数据结构
目前项目中运用的是第2种。异步
看到netty中hashedwheeltimer原理,本身能够仿造一种数据结构,用来实现延时消息触发。spa
首先分析项目中哪些运用场景,经过延时的过程当中数据的是否须要检测最终是否触发来划分静态的延时和动态的延时。线程
1、静态延时:不须要在延时的过程当中判断是否触发定时任务,只是单纯地到指定时间触发任务便可,例如:交易成功通知业务系统。设计
2、动态延时:在延时的过程当中并非每一个任务都须要执行,是有前提条件才能触发执行;例如:心跳检测,链接超时等。3d
场景一分析:支付成功异步通知指针
支付模块有一笔订单支付成功通知业务系统的定时任务,具体是支付流水交易若是支付成功了,那么由调度平台根据cron定义的时间来触发通知的任务。
假设支付流水表的结构为:t_jnl(jnl_no, pay_status,notify_status …),定时任务每一分钟执行一次:目前的场景能够简化为:
1.查询出支付成功的流水记录:select jnl_no from t_jnl where pay_status = 1 and notify_status =0;
2.调用业务系统接口,通知支付结果;
存在的问题:
①若是支付记录数很大,那么去查找知足条件的记录会形成数据库很大的压力。仅仅根据2个状态来查询的效率是很低的。
每次查询表数据,已经被执行过记录,仍然会被扫描(只是不会出如今结果集中),有重复计算的嫌疑。
②若是知足条件的支付流水足够多的话,至少每次不能一次性读取。须要分页查询,这将会是一个for循环。目前作法是定时任务触发时一次读取100条数据。
若是记录数超时定时任务中设定的数量(100),那么在后面的记录不会再本次中获得执行。
③假如一条记录刚好在刚执行任务后0.1s知足条件了(pay_status = 1),那么几乎要等待下一个周期被执行,时效性很差。偏差时间有可能就是cron的设置时间t。
场景一改造:(静态延时)
为了解决上述场景存在的问题,引入下面的设计:右侧是经过一个数组进行封装的环形队列,相似一个时钟。根据cron来设置环形队列的segment,理解为一个独立的任务单元。左侧是每一个任务单元的结构实现:set<Task>
以当前场景为例,cron设置时间t=60s,n=60,后台启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的segment。
那么改造的场景变为:
1.在支付成功后根据current Index所在位置和cron设置周期确认在环形队列上的segment下标和cyclenum后将数据插入环形队列中。
假设 current Index = 3,想要在60s后执行,数据插入第3+60=63个节点,可是环形队列最大长度为60,因此cyclenum=63/60=1,segment=3
2.task function是具体执行延时任务的方法
假设异步通知业务系统的方法为syncOrder(jnl_no) ,通知业务系统这笔流水支付成功了。
3.后台一致启动一个Timer,每隔t/n时间段,current index移动一个segment,当移动到当前的segment时候,渠道set<Task>中的cyclenum,
判断是否为0,若是cyclenum=0,当即执行task function(jnlno)(能够用单独的线程来执行Task),并把这个Task从Set<Task>中删除,不然cyclenum -1。等待下个周期。
结论分析:
(1)无需与数据库进行交互,不用再轮询所有订单,效率高
(2)时效性好,精确到秒(设置timer的移动频率t和segment数量n能够控制精度)
(3)可是须要考虑数据量大的时候内存吃紧的状况(能够经过t/n的频率来减小内存中缓存的数据)。
场景二分析:支付成功但通知失败后进行重复通知策略
在上面的"支付成功通知"场景中会去异步通知业务系统,根据业务系统响应后修改通知状态.有时候会出现业务系统宕机或者超时的状况,遇到此种问题须要再次发起通知。
1.系统目前的解决办法是:查询出支付成功但通知失败的流水记录:select jnl_no from t_jnl where pay_status = 1 and notify_status =2;
2.再次调用业务系统接口,通知支付结果;
3.修改对应的通知状态,若是通知成功后续不会再通知,失败还会发起通知。
存在的问题:
①若是“支付成功但通知失败”记录不多,那么去查找的时候已经通知成功的记录仍然会被扫描,只为查询少许数据但须要全盘扫描其实资源就被浪费了。
②假如一条记录刚好在刚执行任务后0.1s知足条件了,那么几乎要等待下一个周期t=5min被执行,时效性很差。偏差时间有可能就是周期t。
场景二改造:(动态延时)
之因此是动态延迟是由于并非每次通知的结果都须要延迟执行任务,只有通知失败才会有后续的延时任务。
以当前场景为例,首先在场景一中调用定时任务中的异步通知方法,若是通知失败后将syncOrder(jnl_no)的流水号jnl_no存入Map数据中,将对应的环形队列的下标存入Map的值。
cron设置时间t=300s,n=60,后台启动一个timer,这个timer每隔5s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的segment。
那么改造的场景变为:
1.假设current index指向segment=3的时候,执行通知但结果失败,先确认该流水下次在队列上的index = current index -1 = 2 ,到下一次被执行恰好300s.
因此map.put(jnl_no,2),同时把curent index指向的节点从数据删除。
2.隔了300s后上一步的segment会被current index读取,执行通知任务,若是执行成功,把map中的数据删除掉,执行失败继续按照上一步步骤进行。
哪些元素是通知失败的呢?
Current Index每秒种移动一个segment,这个segment对应的Set<jnl_no>中全部jnl_no都应该被执行!若是最近500s有通知失败的,必定被放到Current Index的前一个segment了,Current Index所在的segment对应Set中全部元素,都是通知失败的。因此,当没有通知失败时,Current Index扫到的每个segment的Set中应该都没有元素。
结论分析:
相对项目中目前的优点:
(1)只须要1个timer便可,无需数据库交互,全局搜索。
(2)批量通知,Current Index扫到的segment,Set中全部元素都应该被从新发起通知。
除开上面目前项目中运用的方法,还有其余的一些办法,来进行比较下。
“轮询扫描法”
1)用一个Map<jnl_no, last_notify_time>来记录每个jnl_no最近一次通知时间last_notify_time
2)当某个用户jnl_no通知失败时,实时更新这个last_notify_time
3)启动一个timer,当Map中不为空时,轮询扫描这个Map,看每一个jnl_no的last_notify_time是否超过500s,若是超过500s进行超时再次通知。
“多timer触发法”
1)用一个Map<jnl_no, last_notify_time>来记录每个jnl_no最近一次请求时间last_notify_time
2)当某个jnl_no有通知失败,实时更新这个Map,并同时对这个jnl_no启动一个timer,500s以后触发
3)每一个jnl_no对应的timer触发后,看Map中,查看这个jnl_no的last_notify_time是否超过500s,若是超过则进行通知处理
方案一:只启动一个timer,但须要轮询,效率较低
方案二:不须要轮询,但每一个请求包要启动一个timer,比较耗资源