An introduction to the time wheel
1.1 Why use the time wheel
In normal development, we often deal with timed tasks. Here are a few examples of timed task processing.
1) Heartbeat detection . In Dubbo, a heartbeat mechanism is required to maintain a long connection between the Consumer and the Provider. The default heartbeat interval is 60s. When the provider does not receive a heartbeat response within 3 heartbeats, it will close the connection channel. When the Consumer does not receive a heartbeat response within 3 heartbeats, it will reconnect. The heartbeat detection mechanisms on the provider side and the consumer side are implemented by timed tasks, and are processed by the time wheel HashedWheelTimer to be analyzed in this article.
2) Timeout processing . When initiating an RPC call in Dubbo, a timeout period is usually configured. When the consumer calls the service provider and the timeout occurs, a certain logic processing is performed. So how to detect the task call timeout? We can use timed tasks to create a Future each time, record the creation time and timeout time of this Future, and there is a timed task in the background for detection. When the Future reaches the timeout time and is not processed, we need to perform the timeout logic processing on the Future. .
3) Redisson distributed lock renewal . In distributed lock processing, the timeout period of the distributed lock is usually specified, and the distributed lock is also released in the finally block. However, when there is a problem, it is usually difficult to judge the timeout period of distributed locks. If the setting is short but the business is not completed, the lock is released, or the timeout period is set too long, there will also be some problems. Redisson provides a watchdog mechanism to renew distributed locks through time rounds, that is, to extend the timeout period of distributed locks.
It can be seen that the above examples are all related to timed tasks, so what are the disadvantages of traditional timed tasks? Why use a time wheel for this?
If the normal timing task processing mechanism is used to handle the timeout situation in example 2):
1) Simply, you can create a thread for each request, then Sleep until the timeout period, and then perform timeout logic processing if the timeout is determined. The problem is that if faced with high concurrent requests, threads must be created for each request, which is too resource-intensive.
2) In view of the shortcomings of scheme 1, it can be changed to a thread to process all timing tasks. For example, this thread can scan all the time-out tasks that need to be processed every 50ms, and if any time-out tasks are found, they will be processed. However, there is also a problem in this way. There may be no tasks that reach the timeout period for a period of time, so the CPU will have a lot of useless polling and traversal operations.
In view of the deficiencies of the above solutions, a time wheel can be used to deal with them. Let's briefly introduce the concept of time wheel.
1.2 Single layer time wheel
Let's take a single-layer time wheel as an example. Suppose the period of the time wheel is 1 second, and there are 10 slots in the time wheel, and each slot represents 100ms. Suppose we now have 3 tasks, namely task A (execute after 220ms), B (run after 410ms), and C (run after 1930ms). The slots where these three tasks are located in the time wheel are as shown in the figure below. You can see that task A is placed in slot 2, task B is placed in slot 4, and task C is placed in slot 9.
When the time wheel rotates to the corresponding slot, the task will be taken out from the slot to determine whether it needs to be executed. At the same time, it can be found that there is a concept of remaining cycle. This is because the execution time of task C is 1930ms, which exceeds the cycle of the time wheel by 1 second, so its remaining cycle can be marked as 1. When the time wheel rotates for the first time to its When it is in position, it is found that its remaining cycle is 1, indicating that it has not yet reached the time to be processed, the remaining cycle is reduced by 1, and the time wheel continues to rotate. Process the scheduled task. This single-layer time wheel mechanism is used in Dubbo.
1.3 Multi-layer time wheel
Since there is a single-layer time wheel, it is natural to think of using a multi-layer time wheel to solve the situation that the above-mentioned task execution time exceeds the time wheel period. The following takes two layers of time wheels as an example. The first layer of time wheels has a period of 1 second, and the second layer of time wheels has a period of 10 seconds.
Taking the above three tasks as an example, it can be seen that tasks A and B are distributed on the first layer of time wheels, while task C is distributed at slot 1 of the second layer of time wheels. When the first layer of time wheel rotates, task A and task B will be executed successively. After 1 second, the first layer of time wheel completed a cycle of rotation. The 0th jump starts from a new one. At this time, the second layer time wheel jumps from slot 0 to slot 1, and the task in slot 1, that is, task C, is taken out and put into slot 9 of the first layer time wheel. A layer of time wheel rotates to slot 9, and task C will be executed. This kind of taking out the tasks of the second layer and putting them into the first layer is called downgrading, which is to ensure the time accuracy of the tasks being processed. This multi-layer time wheel mechanism is used internally in Kafka.
The principle of time wheel
Let's take a look at the structure of the time wheel in Dubbo. You can see that it is very similar to a clock. It is divided into buckets. Each bucket has a head pointer and a tail pointer, which respectively point to the head node of the doubly linked list. And the tail node, the doubly linked list stores the tasks to be processed. The time wheel keeps turning, and when it points to the doubly linked list maintained by Bucket0, it traverses and retrieves the tasks stored in it for processing.
Let's first introduce some core concepts involved in the time wheel HashedWheelTimer in Dubbo. After explaining these core concepts, we will analyze the source code of the time wheel.
2.1 TimerTask
In Dubbo, TimerTask encapsulates the task to be executed, which is the task encapsulated by the node in the doubly linked list above. All timing tasks need to inherit the TimerTask interface. As shown in the figure below, you can see that the heartbeat task HeartBeatTask in Dubbo, the registration failure retry task FailRegisteredTask, etc. all implement the TimerTask interface.
public interface TimerTask {
void run(Timeout timeout) throws Exception;
}
2.2 Timeout
The input parameter of the run method in TimerTask is Timeout, and Timeout corresponds to TimerTask one by one. The only implementation class of Timeout, HashedWheelTimeout, encapsulates the TimerTask attribute. It can be understood that HashedWheelTimeout is a node of the above-mentioned doubly linked list, so it also contains two types of HashedWheelTimeout. The pointers point to the previous node and the next node of the current node, respectively.
public interface Timeout {
// Timer就是定时器, 也就是Dubbo中的时间轮
Timer timer();
// 获取该节点要执行的任务
TimerTask task();
// 判断该节点封装的任务有没有过期、被取消
boolean isExpired();
boolean isCancelled();
// 取消该节点的任务
boolean cancel();
}
HashedWheelTimeout is the only implementation of Timeout, it does two things:
- It is the node of the doubly linked list maintained by the time wheel slot, which encapsulates the actual task TimerTask to be executed.
- Through it, you can view the status of timed tasks, cancel timed tasks, and remove them from the doubly linked list.
Let's take a look at the core fields and implementation of Timeout's implementation class HashedWheelTimeout.
1) int ST_INIT = 0、int ST_CANCELLED = 1、int ST_EXPIRED = 2
HashedWheelTimeout里定义了三种状态,分别表示任务的初始化状态、被取消状态、已过期状态
2) STATE_UPDATER
用于更新定时任务的状态
3) HashedWheelTimer timer
指向时间轮对象
4) TimerTask task
实际要执行的任务
5) long deadline
指定时任务执行的时间,这个时间是在创建 HashedWheelTimeout 时指定的
计算公式是: currentTime(创建 HashedWheelTimeout 的时间) + delay(任务延迟时间)
- startTime(HashedWheelTimer 的启动时间),时间单位为纳秒
6) int state = ST_INIT
任务初始状态
7) long remainingRounds
指当前任务剩余的时钟周期数. 时间轮所能表示的时间长度是有限的, 在任务到期时间与当前时刻
的时间差超过时间轮单圈能表示的时长,就出现了套圈的情况,需要该字段值表示剩余的时钟周期
8) HashedWheelTimeout next、HashedWheelTimeout prev
分别对应当前定时任务在链表中的前驱节点和后继节点,这也验证了时间轮中每个槽所对应的任务链表是
一个双链表
9) HashedWheelBucket bucket
时间轮中的一个槽,对应时间轮圆圈的一个个小格子,每个槽维护一个双向链表,当时间轮指针转到当前
槽时,就会从槽所负责的双向链表中取出任务进行处理
HashedWheelTimeout provides the remove operation, which can remove the current self node from the doubly linked list and reduce the number of timed tasks maintained by the current time wheel by one.
void remove() {
// 获取当前任务属于哪个槽
HashedWheelBucket bucket = this.bucket;
if (bucket != null) {
// 从槽中移除自己,也就是从双向链表中移除节点,
// 分析bucket的方法时会分析
bucket.remove(this);
} else {
// pendingTimeouts表示当前时间轮所维护的定时任务的数量
timer.pendingTimeouts.decrementAndGet();
}
}
HashedWheelTimeout provides the cancel operation, which can cancel the timed task in the time wheel. When a scheduled task is canceled, it is first temporarily stored in the canceledTimeouts queue. cancel will be called before the time wheel rotates to the slot for task processing and when the time wheel exits, and cancel will call remove to clean up the canceled scheduled tasks in the queue.
@Override
public boolean cancel() {
// 通过CAS进行状态变更
if (!compareAndSetState(ST_INIT, ST_CANCELLED)) {
return false;
}
// 任务被取消时,时间轮会将它暂存到时间轮所维护的canceledTimeouts队列中.
// 在时间轮转动到槽进行任务处理之前和时间轮退出运行时都会调用cancel,而
// cancel会调用remove,从而清理该队列中被取消的定时任务
timer.cancelledTimeouts.add(this);
return true;
}
HashedWheelTimeout provides the expire operation. When the time wheel pointer rotates to a slot, it will traverse the doubly linked list maintained by the slot to judge the status of the node. If the task is found to have expired, it will be removed by the remove method, and then the expire method will be called. Execute the scheduled task.
public void expire() {
// 修改定时任务状态为已过期
if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {
return;
}
try {
// 真正的执行定时任务所要代表的逻辑
task.run(this);
} catch (Throwable t) {
// 打印日志,可以看到当时间轮中定时任务执行异常时,
// 不会抛出异常,影响到时间轮中其他定时任务执行
}
}
2.3 HashedWheelBucket
As mentioned earlier, it is a slot in the time wheel, which internally maintains the head and tail pointers of the doubly linked list. Let's take a look at its internal core resources and implementation.
1) HashedWheelTimeout head、HashedWheelTimeout tail
指向该槽所维护的双向链表的首节点和尾节点
HashedWheelBucket provides the addTimeout method for adding tasks to the tail node of the doubly linked list.
void addTimeout(HashedWheelTimeout timeout) {
// 添加之前判断一下该任务当前没有被被关联到一个槽上
assert timeout.bucket == null;
timeout.bucket = this;
if (head == null) {
head = tail = timeout;
} else {
tail.next = timeout;
timeout.prev = tail;
tail = timeout;
}
}
HashedWheelBucket provides the remove method to remove the specified node from the doubly linked list. The core logic is shown in the figure below, find its pre-node and post-node according to the node to be deleted, and then adjust the next pointer of the pre-node and the prev pointer of the post-node respectively. There are some edge cases that need to be considered in the deletion process. After deletion, the pendingTimeouts, that is, the number of pending tasks in the current time round, is decremented by one. The logic of the remove code is relatively simple, so the code will not be posted here.
HashedWheelBucket provides the expireTimeouts method. When the time wheel pointer rotates to a certain slot, the timing task of the doubly linked list on the slot is processed by this method, which is divided into 3 cases:
- When the timed task has expired, it will be taken out through the remove method, and its expire method will be called to execute the task logic.
- If the timed task has been canceled, it is taken out through the remove method and discarded directly.
- If the scheduled task has not expired, the remainingRounds (remaining clock cycles) will be decremented by one.
void expireTimeouts(long deadline) {
HashedWheelTimeout timeout = head;
// 时间轮指针转到某个槽时从双向链表头节点开始遍历
while (timeout != null) {
HashedWheelTimeout next = timeout.next;
// remainingRounds <= 0表示到期了
if (timeout.remainingRounds <= 0) {
// 从链表中移除该节点
next = remove(timeout);
// 判断该定时任务确实是到期了
if (timeout.deadline <= deadline) {
// 执行该任务
timeout.expire();
} else {
// 抛异常
}
} else if (timeout.isCancelled()) {
// 任务被取消,移除后直接丢弃
next = remove(timeout);
} else {
// 剩余时钟周期减一
timeout.remainingRounds--;
}
// 继续判断下一个任务节点
timeout = next;
}
}
HashedWheelBucket also provides the clearTimeouts method, which will be used when the time wheel stops, it will traverse and remove all nodes in the doubly linked list, and return all tasks that have not timed out and have not been canceled.
2.4 Worker
The Worker implements the Runnable interface, and the Time Wheel uses the Worker thread to process the timing tasks placed in the Time Wheel. Let's take a look at its core fields and run method logic first.
1) Set<Timeout> unprocessedTimeouts
当时间轮停止时,用于存放时间轮中未过期的和未被取消的任务
2) long tick
时间轮指针,指向时间轮中某个槽,当时间轮转动时该tick会自增
public void run() {
// 初始化startTime, 所有任务的的deadline都是相对于这个时间点
startTime = System.nanoTime();
// 唤醒阻塞在start()的线程
startTimeInitialized.countDown();
// 只要时间轮的状态为WORKER_STATE_STARTED, 就循环的转动tick,
// 处理槽中的定时任务
do {
// 判断是否到了处理槽的时间了,还没到则sleep一会
final long deadline = waitForNextTick();
if (deadline > 0) {
// 获取tick对应的槽索引
int idx = (int) (tick & mask);
// 清理用户主动取消的定时任务, 这些定时任务在用户取消时,
// 会记录到 cancelledTimeouts 队列中. 在每次指针转动
// 的时候,时间轮都会清理该队列
processCancelledTasks();
// 根据当前指针定位对应槽
HashedWheelBucket bucket = wheel[idx];
// 将缓存在 timeouts 队列中的定时任务转移到时间轮中对应的槽中
transferTimeoutsToBuckets();
// 处理该槽位的双向链表中的定时任务
bucket.expireTimeouts(deadline);
tick++;
}
// 检测时间轮的状态, 如果时间轮处于运行状态, 则循环执行上述步骤,
// 不断执行定时任务
} while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this)
== WORKER_STATE_STARTED);
// 这里应该是时间轮停止了, 清除所有槽中的任务, 并加入到未处理任务列表,
// 以供stop()方法返回
for (HashedWheelBucket bucket : wheel) {
bucket.clearTimeouts(unprocessedTimeouts);
}
// 将还没有加入到槽中的待处理定时任务队列中的任务取出, 如果是未取消
// 的任务, 则加入到未处理任务队列中, 以供stop()方法返回
for (; ; ) {
HashedWheelTimeout timeout = timeouts.poll();
if (timeout == null) {
break;
}
if (!timeout.isCancelled()) {
unprocessedTimeouts.add(timeout);
}
}
// 最后再次清理 cancelledTimeouts 队列中用户主动取消的定时任务
processCancelledTasks();
}
The following is an introduction to some of the methods involved in the run method:
1)waitForNextTick
The logic is relatively simple. It will determine whether it is time to process the next slot task. If it has not arrived, it will sleep for a while.
2)processCancelledTasks
Traverse canceledTimeouts, get canceled tasks and remove them from the doubly linked list.
private void processCancelledTasks() {
for (; ; ) {
HashedWheelTimeout timeout = cancelledTimeouts.poll();
if (timeout == null) {
// all processed
break;
}
timeout.remove();
}
}
3)transferTimeoutsToBuckets
When the newTimeout method is called, the tasks to be processed will be cached in the timeouts queue first, and the transferTimeoutsToBuckets method will be called uniformly when the time wheel pointer rotates, and the tasks will be transferred to the doubly linked list corresponding to the specified slot, 100,000 each time. So as not to block the time wheel thread.
private void transferTimeoutsToBuckets() {
// 每次tick只处理10w个任务, 以免阻塞worker线程
for (int i = 0; i < 100000; i++) {
HashedWheelTimeout timeout = timeouts.poll();
// 没有任务了直接跳出循环
if (timeout == null) {
// all processed
break;
}
// 还没有放入到槽中就取消了, 直接略过
if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
continue;
}
// 计算任务需要经过多少个tick
long calculated = timeout.deadline / tickDuration;
// 计算任务的轮数
timeout.remainingRounds = (calculated - tick) / wheel.length;
// 如果任务在timeouts队列里面放久了, 以至于已经过了执行时间, 这个时候
// 就使用当前tick, 也就是放到当前bucket, 此方法调用完后就会被执行.
final long ticks = Math.max(calculated, tick);
int stopIndex = (int) (ticks & mask);
// 将任务加入到相应的槽中
HashedWheelBucket bucket = wheel[stopIndex];
bucket.addTimeout(timeout);
}
}
2.5 HashedWheelTimer
Finally, let's analyze the time wheel HashedWheelTimer, which implements the Timer interface and provides the newTimeout method to add timed tasks to the time wheel. The task will be temporarily stored in the timeouts queue, and when the time wheel rotates to a certain slot, The tasks in the timeouts queue will be transferred to the doubly linked list that a slot is responsible for. It also provides the stop method for terminating the time wheel, which returns unprocessed tasks in the time wheel. It also provides the isStop method to determine whether the time wheel has terminated.
Let's first look at the core fields of HashedWheelTimer.
1) HashedWheelBucket[] wheel
该数组就是时间轮的环形队列,数组每个元素都是一个槽,一个槽负责维护一个双向链表,用于存储定时
任务。它会被在构造函数中初始化,当指定为n时,它实际上会取最靠近n的且为2的幂次方值。
2) Queue<HashedWheelTimeout> timeouts
timeouts用于缓存外部向时间轮提交的定时任务
3) Queue<HashedWheelTimeout> cancelledTimeouts
cancelledTimeouts用于暂存被取消的定时任务,时间轮会在处理槽负责的双向链表之前,先处理这两
个队列中的数据。
4) Worker worker
时间轮处理定时任务的逻辑
5) Thread workerThread
时间轮处理定时任务的线程
6) AtomicLong pendingTimeouts
时间轮剩余的待处理的定时任务数量
7) long tickDuration
时间轮每个槽所代表的时间长度
8) int workerState
时间轮状态,可选值有init、started、shut down
Let's take a look at the constructor of the time wheel, which is used to initialize a time wheel. First, it will convert the incoming parameter ticksPerWheel, and return a power of 2 greater than this value, which indicates how many slots there are on the time wheel, and the default is 512. Then create a HashedWheelBucket[] array of size that value. Then assign the tickDuration of the time wheel through the incoming tickDuration, the default is 100ms. Section creates a workerThread worker thread through threadFactory, which is the thread responsible for processing the timing tasks in the time wheel.
public HashedWheelTimer(ThreadFactory threadFactory,
long tickDuration, TimeUnit unit,
int ticksPerWheel,
long maxPendingTimeouts) {
// 圆环上一共有多少个时间间隔, HashedWheelTimer对其正则化
// 将其换算为大于等于该值的2^n
wheel = createWheel(ticksPerWheel);
// 这用来快速计算任务应该呆的槽
mask = wheel.length - 1;
// 时间轮每个槽的时间间隔
this.tickDuration = unit.toNanos(tickDuration);
// threadFactory是创建线程的线程工厂对象
workerThread = threadFactory.newThread(worker);
// 最多允许多少个任务等待执行
this.maxPendingTimeouts = maxPendingTimeouts;
}
private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
// 计算真正应当创建多少个槽
ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
// 初始化时间轮数组
HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
for (int i = 0; i < wheel.length; i++) {
wheel[i] = new HashedWheelBucket();
}
return wheel;
}
After initializing the time wheel, you can submit timed tasks to it, which can be done through the newTimeout method provided by the time wheel. First, increase the number of tasks to be processed by 1, and then start the time wheel thread. At this time, the run method of the worker will be scheduled to run by the system. Then encapsulate the timing task as HashedWheelTimeout and add it to the timeouts queue. After start, the time wheel starts to run until the outside world calls the stop method to terminate the exit.
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
// 待处理的任务数量加1
long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();
// 启动时间轮
start();
// 计算该定时任务的deadline
long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
// 创建一个HashedWheelTimeout对象,它首先会被暂存到timeouts队列中
HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
timeouts.add(timeout);
return timeout;
}
public void start() {
/**
* 判断当前时间轮的状态
* 1) 如果是初始化, 则启动worker线程, 启动整个时间轮
* 2) 如果已经启动则略过
* 3) 如果是已经停止,则报错
*/
switch (WORKER_STATE_UPDATER.get(this)) {
case WORKER_STATE_INIT:
// 使用cas来判断启动时间轮
if (WORKER_STATE_UPDATER.compareAndSet(this,
WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
workerThread.start();
}
break;
case WORKER_STATE_STARTED:
break;
case WORKER_STATE_SHUTDOWN:
// 抛异常
default:
throw new Error("Invalid WorkerState");
}
// 等待worker线程初始化时间轮的启动时间
while (startTime == 0) {
try {
// 这里使用countDownLatch来确保调度的线程已经被启动
startTimeInitialized.await();
} catch (InterruptedException ignore) {
// Ignore - it will be ready very soon.
}
}
}
3. Time Wheel Application
At this point, the time wheel principle in Dubbo is analyzed. Next, echo the three examples at the beginning of this article and combine them to analyze how the time wheel is used in Dubbo or Redisson.
1)HeartbeatTimerTask
In Dubbo's HeaderExchangeClient class, the heartbeat task is submitted to the time wheel.
private void startHeartBeatTask(URL url) {
// Client的具体实现决定是否启动该心跳任务
if (!client.canHandleIdle()) {
AbstractTimerTask.ChannelProvider cp =
() -> Collections.singletonList(HeaderExchangeClient.this);
// 计算心跳间隔, 最小间隔不能低于1s
int heartbeat = getHeartbeat(url);
long heartbeatTick = calculateLeastDuration(heartbeat);
// 创建心跳任务
this.heartBeatTimerTask =
new HeartbeatTimerTask(cp, heartbeatTick, heartbeat);
// 提交到IDLE_CHECK_TIMER这个时间轮中等待执行, 等时间到了时间轮就会去取出该任务进行调度执行
IDLE_CHECK_TIMER.newTimeout(heartBeatTimerTask, heartbeatTick, TimeUnit.MILLISECONDS);
}
}
// 上面用到的IDLE_CHECK_TIMER就是我们本文的分析的时间轮
private static final HashedWheelTimer IDLE_CHECK_TIMER =
new HashedWheelTimer(new NamedThreadFactory("dubbo-client-idleCheck", true), 1, TimeUnit.SECONDS, TICKS_PER_WHEEL);
// 上述创建心跳任务时, 创建了一个HeartbeatTimerTask对象, 可以看下该任务具体要做什么
@Override
protected void doTask(Channel channel) {
try {
// 获取最后一次读写时间
Long lastRead = lastRead(channel);
Long lastWrite = lastWrite(channel);
if ((lastRead != null && now() - lastRead > heartbeat)
|| (lastWrite != null && now() - lastWrite > heartbeat)) {
// 最后一次读写时间超过心跳时间, 就会发送心跳请求
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
// 表明它是一个心跳请求
req.setEvent(HEARTBEAT_EVENT);
channel.send(req);
}
} catch (Throwable t) {
}
}
2) Redisson lock renewal mechanism
When the lock is acquired successfully, Redisson will encapsulate a lock renewal task and put it into the time wheel. The default 10s check is used to renew the acquired lock and prolong the holding time of the lock. If the business machine is down, the scheduled task that should be renewed will not be able to run, and the renewal will not be possible. The lock will be automatically released when the lock time is up. The logic is encapsulated in the renewExpiration() method in RedissonLock.
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 这边newTimeout点进去发现就是往时间轮中提交了一个任务
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// 续期成功后继续调度, 又往时间轮中放一个续期任务
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
// 通过lua脚本对锁进行续期
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
3) Timeout retry
The usage method is similar to the HeartbeatTimerTask method, and readers can analyze where it was introduced by themselves.
4. Summary
In this article, three examples are given to discuss why the time wheel is needed and the advantages of using the time wheel. At the end of the article, the use of these three examples in Dubbo or Redisson is also introduced. Then, the single-layer time wheel and multi-layer time wheel mechanisms are explained by drawing, so that readers have a simple understanding of the time wheel algorithm. In the second part, the TimerTask, Timeout, HashedWheelBucket, Worker, and HashedWheelTimer involved in the Dubbo time wheel are explained in turn, and their principles and source code implementations are analyzed.
Author: vivo Internet Server Team - Li Wanghong
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。