docs: change to relative path
@ -1,6 +1,7 @@
|
|||||||
# 前言
|
# 前言
|
||||||
|
|
||||||
前段时间在给自己的玩具项目设计的时候就遇到了一个场景需要定时任务,于是就趁机了解了目前主流的一些定时任务方案,比如下面这些:
|
前段时间在给自己的玩具项目设计的时候就遇到了一个场景需要定时任务,于是就趁机了解了目前主流的一些定时任务方案,比如下面这些:
|
||||||
|
|
||||||
- Timer(halo 博客源码中用到了)
|
- Timer(halo 博客源码中用到了)
|
||||||
- ScheduledExecutorService
|
- ScheduledExecutorService
|
||||||
- ThreadPoolTaskScheduler(基于 ScheduledExecutorService)
|
- ThreadPoolTaskScheduler(基于 ScheduledExecutorService)
|
||||||
@ -9,6 +10,7 @@
|
|||||||
- Kafka 的 TimingWheel(层级时间轮)
|
- Kafka 的 TimingWheel(层级时间轮)
|
||||||
|
|
||||||
还有一些分布式的定时任务:
|
还有一些分布式的定时任务:
|
||||||
|
|
||||||
- Quartz
|
- Quartz
|
||||||
- xxl-job(我实习公司就在用这个)
|
- xxl-job(我实习公司就在用这个)
|
||||||
- ...
|
- ...
|
||||||
@ -19,13 +21,14 @@
|
|||||||
|
|
||||||
# HashedWheelTimer 实现图示
|
# HashedWheelTimer 实现图示
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
大致有个理解就行,关于蓝色格子中的数字,其实就是剩余时钟轮数,这里听不懂也没关系,等后面看到源码解释就懂了~~(大概)~~。
|
大致有个理解就行,关于蓝色格子中的数字,其实就是剩余时钟轮数,这里听不懂也没关系,等后面看到源码解释就懂了~~(大概)~~。
|
||||||
|
|
||||||
# HashedWheelTimer 简答使用例子
|
# HashedWheelTimer 简答使用例子
|
||||||
|
|
||||||
这里顺便列出 schedule 的使用方式,下面是某个 Handler 中的代码:
|
这里顺便列出 schedule 的使用方式,下面是某个 Handler 中的代码:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Override
|
@Override
|
||||||
public void handlerAdded(final ChannelHandlerContext ctx) {
|
public void handlerAdded(final ChannelHandlerContext ctx) {
|
||||||
@ -43,17 +46,17 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# HashedWheelTimer 源码
|
# HashedWheelTimer 源码
|
||||||
|
|
||||||
### 继承关系、方法
|
### 继承关系、方法
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 构造函数、属性
|
### 构造函数、属性
|
||||||
|
|
||||||
请记住这些属性的是干啥用的,后面会频繁遇到:
|
请记住这些属性的是干啥用的,后面会频繁遇到:
|
||||||
`io.netty.util.HashedWheelTimer#HashedWheelTimer(java.util.concurrent.ThreadFactory, long, java.util.concurrent.TimeUnit, int, boolean, long)`
|
`io.netty.util.HashedWheelTimer#HashedWheelTimer(java.util.concurrent.ThreadFactory, long, java.util.concurrent.TimeUnit, int, boolean, long)`
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public HashedWheelTimer(
|
public HashedWheelTimer(
|
||||||
ThreadFactory threadFactory,
|
ThreadFactory threadFactory,
|
||||||
@ -123,6 +126,7 @@
|
|||||||
|
|
||||||
添加定时任务其实就是 Timer 接口的 newTimeOut 方法:
|
添加定时任务其实就是 Timer 接口的 newTimeOut 方法:
|
||||||
`io.netty.util.HashedWheelTimer#newTimeout`
|
`io.netty.util.HashedWheelTimer#newTimeout`
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Override
|
@Override
|
||||||
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
|
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
|
||||||
@ -172,6 +176,7 @@
|
|||||||
|
|
||||||
这里我们再跟进 start 方法看看:
|
这里我们再跟进 start 方法看看:
|
||||||
`io.netty.util.HashedWheelTimer#start`
|
`io.netty.util.HashedWheelTimer#start`
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public void start() {
|
public void start() {
|
||||||
switch (WORKER_STATE_UPDATER.get(this)) {
|
switch (WORKER_STATE_UPDATER.get(this)) {
|
||||||
@ -205,6 +210,7 @@
|
|||||||
|
|
||||||
定时任务的执行逻辑其实就在 Worker 的 run 方法中:
|
定时任务的执行逻辑其实就在 Worker 的 run 方法中:
|
||||||
`io.netty.util.HashedWheelTimer.Worker#run`
|
`io.netty.util.HashedWheelTimer.Worker#run`
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// 用于处理取消的任务
|
// 用于处理取消的任务
|
||||||
private final Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();
|
private final Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();
|
||||||
@ -266,10 +272,12 @@
|
|||||||
processCancelledTasks();
|
processCancelledTasks();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- 取消任务的逻辑这里就不展开看了,也比较简单,有兴趣自行补充即可。
|
- 取消任务的逻辑这里就不展开看了,也比较简单,有兴趣自行补充即可。
|
||||||
|
|
||||||
看看上面的 transferTimeoutsToBuckets 方法,如果你看不懂上面图中蓝色格子数字是什么意思,那就认真看看这个方法:
|
看看上面的 transferTimeoutsToBuckets 方法,如果你看不懂上面图中蓝色格子数字是什么意思,那就认真看看这个方法:
|
||||||
`io.netty.util.HashedWheelTimer.Worker#transferTimeoutsToBuckets`
|
`io.netty.util.HashedWheelTimer.Worker#transferTimeoutsToBuckets`
|
||||||
|
|
||||||
```java
|
```java
|
||||||
private void transferTimeoutsToBuckets() {
|
private void transferTimeoutsToBuckets() {
|
||||||
// transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just
|
// transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just
|
||||||
@ -309,6 +317,7 @@
|
|||||||
|
|
||||||
继续看看上面 run 方法中的 bucket.expireTimeouts(deadline);,这里面就是拿出任务并执行的逻辑:
|
继续看看上面 run 方法中的 bucket.expireTimeouts(deadline);,这里面就是拿出任务并执行的逻辑:
|
||||||
`io.netty.util.HashedWheelTimer.HashedWheelBucket#expireTimeouts`
|
`io.netty.util.HashedWheelTimer.HashedWheelBucket#expireTimeouts`
|
||||||
|
|
||||||
```java
|
```java
|
||||||
/**
|
/**
|
||||||
* Expire all {@link HashedWheelTimeout}s for the given {@code deadline}.
|
* Expire all {@link HashedWheelTimeout}s for the given {@code deadline}.
|
||||||
@ -356,6 +365,7 @@ schedule方法也是Netty的定时任务实现之一,但是底层的数据结
|
|||||||
|
|
||||||
首先来到如下代码:
|
首先来到如下代码:
|
||||||
`io.netty.util.concurrent.AbstractScheduledEventExecutor#schedule(java.lang.Runnable, long, java.util.concurrent.TimeUnit)`
|
`io.netty.util.concurrent.AbstractScheduledEventExecutor#schedule(java.lang.Runnable, long, java.util.concurrent.TimeUnit)`
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Override
|
@Override
|
||||||
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
|
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
|
||||||
@ -373,6 +383,7 @@ schedule方法也是Netty的定时任务实现之一,但是底层的数据结
|
|||||||
|
|
||||||
继续跟进 schedule 方法看看:
|
继续跟进 schedule 方法看看:
|
||||||
`io.netty.util.concurrent.AbstractScheduledEventExecutor#schedule(io.netty.util.concurrent.ScheduledFutureTask<V>)`
|
`io.netty.util.concurrent.AbstractScheduledEventExecutor#schedule(io.netty.util.concurrent.ScheduledFutureTask<V>)`
|
||||||
|
|
||||||
```java
|
```java
|
||||||
private <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
|
private <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
|
||||||
if (inEventLoop()) {
|
if (inEventLoop()) {
|
||||||
@ -392,6 +403,7 @@ schedule方法也是Netty的定时任务实现之一,但是底层的数据结
|
|||||||
|
|
||||||
继续跟进 scheduledTaskQueue()方法:
|
继续跟进 scheduledTaskQueue()方法:
|
||||||
`io.netty.util.concurrent.AbstractScheduledEventExecutor#scheduledTaskQueue`
|
`io.netty.util.concurrent.AbstractScheduledEventExecutor#scheduledTaskQueue`
|
||||||
|
|
||||||
```java
|
```java
|
||||||
PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue() {
|
PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue() {
|
||||||
if (scheduledTaskQueue == null) {
|
if (scheduledTaskQueue == null) {
|
||||||
@ -413,29 +425,33 @@ schedule方法也是Netty的定时任务实现之一,但是底层的数据结
|
|||||||
这里我就直接贴下网上大佬给出的解释:
|
这里我就直接贴下网上大佬给出的解释:
|
||||||
|
|
||||||
如果使用最小堆实现的优先级队列:
|
如果使用最小堆实现的优先级队列:
|
||||||

|

|
||||||
|
|
||||||
- 大致意思就是你的任务如果插入到堆顶,时间复杂度为 O(log(n))。
|
- 大致意思就是你的任务如果插入到堆顶,时间复杂度为 O(log(n))。
|
||||||
|
|
||||||
如果使用链表(既然有说道,那就扩展下):
|
如果使用链表(既然有说道,那就扩展下):
|
||||||

|

|
||||||
|
|
||||||
- 中间插入后的事件复杂度为 O(n)
|
- 中间插入后的事件复杂度为 O(n)
|
||||||
|
|
||||||
单个时间轮:
|
单个时间轮:
|
||||||

|

|
||||||
|
|
||||||
- 复杂度可以降至 O(1)。
|
- 复杂度可以降至 O(1)。
|
||||||
|
|
||||||
记录轮数的时间轮(其实就是文章开头的那个):
|
记录轮数的时间轮(其实就是文章开头的那个):
|
||||||

|

|
||||||
|
|
||||||
层级时间轮:
|
层级时间轮:
|
||||||

|

|
||||||
|
|
||||||
- 时间复杂度是 O(n),n 是轮子的数量,除此之外还要计算一个轮子上的 bucket。
|
- 时间复杂度是 O(n),n 是轮子的数量,除此之外还要计算一个轮子上的 bucket。
|
||||||
|
|
||||||
### 单时间轮缺点
|
### 单时间轮缺点
|
||||||
|
|
||||||
根据上面的图其实不难理解,如果任务是很久之后才执行的、同时要保证任务低延迟,那么单个时间轮所需的 bucket 数就会变得非常多,从而导致内存占用持续升高(CPU 空转时间还是不变的,仅仅是内存需求变高了),如下图:
|
根据上面的图其实不难理解,如果任务是很久之后才执行的、同时要保证任务低延迟,那么单个时间轮所需的 bucket 数就会变得非常多,从而导致内存占用持续升高(CPU 空转时间还是不变的,仅仅是内存需求变高了),如下图:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Netty 对于单个时间轮的优化方式就是记录下 remainingRounds,从而减少 bucket 过多的内存占用。
|
Netty 对于单个时间轮的优化方式就是记录下 remainingRounds,从而减少 bucket 过多的内存占用。
|
||||||
|
|
||||||
@ -449,11 +465,13 @@ Netty对于单个时间轮的优化方式就是记录下remainingRounds,从而
|
|||||||
- A:而 ScheduledExecutorService 不会有这个问题。
|
- A:而 ScheduledExecutorService 不会有这个问题。
|
||||||
|
|
||||||
另外,Netty 时间轮的实现模型抽象出来是大概这个样子的:
|
另外,Netty 时间轮的实现模型抽象出来是大概这个样子的:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
for(Tasks task : tasks) {
|
for(Tasks task : tasks) {
|
||||||
task.doXxx();
|
task.doXxx();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这个抽象是个什么意思呢?你要注意一个点,这里的任务循环执行是同步的,**这意味着你第一个任务执行很慢延迟很高,那么后面的任务全都会被堵住**,所以你加进时间轮的任务不可以是耗时任务,比如一些延迟很高的数据库查询,如果有这种耗时任务,最好再嵌入线程池处理,不要让任务阻塞在这一层。
|
这个抽象是个什么意思呢?你要注意一个点,这里的任务循环执行是同步的,**这意味着你第一个任务执行很慢延迟很高,那么后面的任务全都会被堵住**,所以你加进时间轮的任务不可以是耗时任务,比如一些延迟很高的数据库查询,如果有这种耗时任务,最好再嵌入线程池处理,不要让任务阻塞在这一层。
|
||||||
|
|
||||||
> 原文链接:https://wenjie.store/archives/netty-hashedwheeltimer-and-schedule
|
> 原文链接:https://wenjie.store/archives/netty-hashedwheeltimer-and-schedule
|
||||||
|
BIN
images/Netty/image_1595751597062.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
images/Netty/image_1595752125587.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
images/Netty/image_1595756711656.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
images/Netty/image_1595756928493.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
images/Netty/image_1595757035360.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
images/Netty/image_1595757110003.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
images/Netty/image_1595757328715.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
images/Netty/image_1595758329809.png
Normal file
After Width: | Height: | Size: 93 KiB |