javaNetty面试题-HashedTimerWheel
2024-05-23 13:23:20
定时任务三种形式:
1、按固定周期定时执行
2、延迟一定时间后执行
3、指定某个时刻执行
定时任务的三个关键方法:
Schedule 新增任务至任务集合;
Cancel 取消某个任务;
Run 执行到期的任务
JDK自带的三种定时器:Timer、DelayedQueue 和 ScheduledThreadPoolExecutor
不论有多少任务被加入数组,始终由 异步线程TimerThread 负责处理。TimerThread 会定时轮询 TaskQueue 中的任务,如果堆顶的任务的 deadline 已到,那么执行任务;如果是周期性任务,执行完成后重新计算下一次任务的 deadline,并再次放入小根堆;如果是单次执行的任务,执行结束后会从 TaskQueue 中删除。
DelayQueue 提供了 put() 和 take() 的阻塞方法,可以向队列中添加对象和取出对象。对象被添加到 DelayQueue 后,会根据 compareTo() 方法进行优先级排序。getDelay() 方法用于计算消息延迟的剩余时间,只有 getDelay <=0 时,该对象才能从 DelayQueue 中取出。
DelayQueue 在日常开发中最常用的场景就是实现重试机制。例如,接口调用失败或者请求超时后,可以将当前请求对象放入 DelayQueue,通过一个异步线程 take() 取出对象然后继续进行重试。如果还是请求失败,继续放回 DelayQueue。可以设置重试的最大次数以及采用指数退避算法设置对象的 deadline,如 2s、4s、8s、16s ……以此类推。DelayQueue的时间复杂度和Timer基本一致。
TimerTask 如果执行出现异常,Timer 并不会捕获,会导致线程终止,其他任务永远不会执行。
时间轮原理分析
根据任务的到期时间进行取余和取模,然后根据取余结果将任务分布到不同的 slot 中,每个slot中根据round值决定是否操作,每次轮询到指定slot时,总时遍历最少round的对象进行执行,这样新增、执行两个操作的时间复杂度都近似O(1)。如果冲突较大可以增加数组长度,或者采用多级时间轮的方式处理。
时间轮空推进问题
Netty 中的时间轮是通过固定的时间间隔 tickDuration 进行推动的,如果长时间没有到期任务,那么会存在时间轮空推进的现象,从而造成一定的性能损耗。此外,如果任务的到期时间跨度很大,例如 A 任务 1s 后执行,B 任务 6 小时之后执行,也会造成空推进的问题。
Kafka解决方案
为了解决空推进的问题,Kafka 借助 JDK 的 DelayQueue 来负责推进时间轮。DelayQueue 保存了时间轮中的每个 Bucket,并且根据 Bucket 的到期时间进行排序,最近的到期时间被放在 DelayQueue 的队头。Kafka 中会有一个线程来读取 DelayQueue 中的任务列表,如果时间没有到,那么 DelayQueue 会一直处于阻塞状态,从而解决空推进的问题。虽然DelayQueue 插入和删除的性能不是很好,但这其实就是一种权衡的策略,但是DelayQueue 只存放了 Bucket,Bucket 的数量并不多,相比空推进带来的影响是利大于弊的。
为了解决任务时间跨度很大的问题,Kafka 引入了层级时间轮,如下图所示。当任务的 deadline 超出当前所在层的时间轮表示范围时,就会尝试将任务添加到上一层时间轮中,跟钟表的时针、分针、秒针的转动规则是同一个道理。