I. Introduction of time round

1.1 Why time wheel

In normal development, you often have to deal with scheduled 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 Provider. The default heartbeat interval is 60 seconds. If the Provider does not receive any heartbeat response within three heartbeats, it closes the connection channel. If the Consumer does not receive a heartbeat response within 3 heartbeats, a reconnection is performed. The heartbeat detection mechanism on the Provider side and the Consumer side is realized through scheduled tasks, which are processed by HashedWheelTimer to be analyzed in this paper.

2) Timeout processing. When an RPC call is made in Dubbo, a timeout is typically configured for some logical processing when the consumer invokes the service provider for a timeout. So how to detect the task call timeout? We can use scheduled tasks to create a Future each time and record the creation time and timeout time of the Future. There is a scheduled task in the background to detect the Future. When the Future reaches the timeout time and has not been processed, it needs to perform timeout logic processing for the Future.

3) Redisson distributed lock renewal. In distributed lock processing, it is common to specify a timeout for the distributed lock and also to release the distributed lock ina finally block. However, when there is a problem, it is usually difficult to determine the timeout period of distributed locks. If the service is set short but not completed, the lock is released, or the timeout period is set very long, there will also be some problems. Redisson provides a watchdog mechanism to renew distributed locks through a time wheel, that is, to extend the timeout period of distributed locks.

As you can see, these examples are all related to timed tasks. What are the disadvantages of traditional timed tasks? Why use a time wheel?

If you use a normal scheduled task processing mechanism to handle the timeout case in Example 2) :

1) Simply, you can create a thread for each request, Sleep to the timeout, and then perform the timeout logic if it determines a timeout. The problem is that if you have a high number of concurrent requests, creating threads for each request is too resource-intensive.

2) In view of the deficiency of scheme 1, a thread can be changed to handle all scheduled tasks. For example, this thread can scan all the timeout tasks that need to be processed every 50ms, and process the timeout tasks if any are found. The problem with this, however, is that the timeout may not be reached for a period of time and the CPU will be overloaded with useless polling traversals.

In view of the shortcomings of the above scheme, time wheel can be used to deal with. Here is a brief introduction to the concept of time wheel.

1.2 Single-layer time wheel

Let’s take the single-layer time wheel as an example. Assume that the period of the time wheel is 1 second and there are 10 slots in the time wheel, then each slot represents 100ms. Suppose we now have three tasks: task A (220ms later), task B (410ms later), and task C (1930ms later). The slots of the three tasks in the time wheel are shown in the figure below. It can be seen that task A is placed in slot 2, task B in slot 4, and task C in slot 9.

When the time wheel rotates to the corresponding slot, the task will be removed from the slot to determine whether it needs to be executed. Can also be found to have a rest period, the concept of this is because the task execution time C for 1930 ms, over a period of 1 second time round, so I can mark it the rest of the cycle is 1, when the time wheel rotation to its position for the first time, found the remaining period is 1, said not to have to deal with time, the remaining period minus 1, continue to turn the time round, When the next rotation to task C, the remaining period is 0, indicating that it is time to process the scheduled task. Dubbo uses this single-layer time wheel mechanism.

1.3 Multi-layer time wheel

Given the existence of a single time wheel, it is natural to use multiple time wheels to solve the situation where the task execution time exceeds the cycle of the time wheel. The following takes the two-layer time cycle as an example. The first time cycle is 1 second, and the second time cycle is 10 seconds.

Taking the above three tasks as an example, it can be seen that task A and B are distributed in the first time round, while task C is distributed in slot 1 of the second time round. When the layer 1 time wheel rotates, task A and task B are executed in sequence. One second later, the first time wheel completes a cycle. At this time, the second layer time wheel jumps from slot 0 to slot 1, and the task in slot 1, namely task C, is taken out and put into slot 9 of the first layer time wheel. When the first layer time wheel rotates to slot 9, task C will be executed. This removal of layer 2 tasks into layer 1 is called demotion to ensure that tasks are processed with time precision. Kafka uses this kind of multi-layer time wheel mechanism internally.

Time wheel principle

It is divided into buckets. Each Bucket has a head pointer to the head node and a tail pointer to the tail node of the bidirectional linked list. The bidirectional linked list stores the tasks to be processed. The time wheel spins, and as it points to a two-way linked list maintained by Bucket0, it loops through the tasks it stores for processing.

Below, we will first introduce some core concepts involved in HashedWheelTimer in Dubbo. After explaining these core concepts, we will analyze the source code of the time wheel.

2.1 TimerTask

In Dubbo, the TimerTask encapsulates the task to be performed, which is the task encapsulated by the nodes in the bidirectional list above. All scheduled tasks need to inherit the TimerTask interface. As shown in the following figure, the TimerTask interface is implemented in Dubbo HeartBeatTask HeartBeatTask and registration failure retry task FailRegisteredTask.

public interface TimerTask {
    void run(Timeout timeout) throws Exception;
}
Copy the code

2.2 the Timeout

The entry parameter of the run method in TimerTask is Timeout, which corresponds to TimerTask one by one. The only implementation class of Timeout, HashedWheelTimeout, encapsulates the attribute of TimerTask. It can be understood that HashedWheelTimeout is a node of the above bidirectional linked list, so it also contains two Pointers of the type HashedWheelTimeout, respectively pointing to the previous node and the next node of the current node.

public interface Timeout {
 
    // Timer is a Dubbo time wheel
    Timer timer(a);
 
    // Gets the task to be performed by the node
    TimerTask task(a);
 
    // Check whether the task encapsulated by this node has expired or been cancelled
    boolean isExpired(a);
    boolean isCancelled(a);
 
    // Cancel the task for this node
    boolean cancel(a);
}
Copy the code

HashedWheelTimeout is the only implementation of Timeout, which has two functions:

  • It is the node of the bidirectional linked list maintained by the Time wheel slot, which encapsulates the actual task to be executed, the TimerTask.

  • You can view the status of scheduled tasks, cancel scheduled tasks, and remove them from the bidirectional 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 = 2HashedWheelTimeout defines three states, which respectively represent the initialization state, canceled state and expired state of the task2) STATE_UPDATER Used to update the status of scheduled tasks3HashedWheelTimer Timer points to a time wheel object4TimerTask Indicates the actual task to be performed5) longDeadline specifies the task execution time, which was specified when HashedWheelTimeout was created. The calculation formula is as follows: CurrentTime (time when HashedWheelTimeout is created) + delay(task delay) - startTime(startTime of HashedWheelTimer), in nanosecond6) intState = ST_INIT Initial task status7) longRemainingRounds Refers to the number of clock cycles remaining in the current task. The length of time represented by the time wheel is limited. When the time difference between the task expiration time and the current time exceeds the time represented by the single turn of the time wheel, a ring occurs, and the value of this field is required to represent the remaining clock cycle8HashedWheelTimeout Next and HashedWheelTimeout Prev correspond to the precursor node and the successor node of the current scheduled task in the linked list respectively, which also verifies that the task linked list corresponding to each slot in the time wheel is a double linked list9A slot in a bucket time wheel corresponds to small cells in the time wheel circle. Each slot maintains a bidirectional linked list. When the time wheel pointer moves to the current slot, tasks are removed from the bidirectional linked list for processingCopy the code

HashedWheelTimeout provides the Remove operation, which can remove its current node from the bidirectional linked list and reduce the number of scheduled tasks maintained by the current time wheel by one.

void remove(a) {
    // Get the slot to which the current task belongs
    HashedWheelBucket bucket = this.bucket;
    if(bucket ! =null) {
        // Remove itself from the slot, that is, remove the node from the bidirectional list,
        // The bucket method will be parsed
        bucket.remove(this);
    } else {
        PendingTimeouts Indicates the number of scheduled tasks maintained by the current time wheeltimer.pendingTimeouts.decrementAndGet(); }}Copy the code

HashedWheelTimeout provides the Cancel operation to cancel a scheduled task in a time wheel. When a scheduled task is cancelled, it is first temporarily stored in the canceledTimeouts queue. Cancel is called both before the wheel rotates into the slot for task processing and when the wheel exits, and Cancel calls remove to clean up canceled scheduled tasks in the queue.

@Override
public boolean cancel(a) {
    // Use CAS to change the status
    if(! compareAndSetState(ST_INIT, ST_CANCELLED)) {return false;
    }
     
    // When a task is cancelled, the time wheel temporarily stores it to the canceledTimeouts queue maintained by the time wheel.
    // Cancel is called both before the wheel rotates into the slot for task processing and when the wheel exits, while
    // Cancel calls remove to clean up canceled scheduled tasks in the queue
    timer.cancelledTimeouts.add(this);
    return true;
}
Copy the code

HashedWheelTimeout provides the expire operation. When the time wheel pointer rotates to a certain slot, it will traverse the bidirectional linked list maintained by the slot to judge the node status. If the task is found to be expired, it will remove it through the remove method, and then call the EXPIRE method to execute the scheduled task.

public void expire(a) {
    // Change the status of the scheduled task to expired
    if(! compareAndSetState(ST_INIT, ST_EXPIRED)) {return;
    }
 
    try {
        // Actually execute the logic represented by the scheduled task
        task.run(this);
    } catch (Throwable t) {
        // Prints logs. You can see that when the scheduled task fails to execute in the time wheel,
        // Will not throw an exception, which will affect the execution of other scheduled tasks in the time wheel}}Copy the code

2.3 HashedWheelBucket

As described earlier, it is the slot in the time wheel that maintains the beginning and end Pointers to the bidirectional list. Let’s take a look at the core resources and implementation within it.

1) HashedWheelTimeout head and HashedWheelTimeout Tail point to the first and last nodes of the bidirectional linked list maintained by the slotCopy the code

HashedWheelBucket provides the addTimeout method for adding tasks to the tail of a bidirectional linked list.

void addTimeout(HashedWheelTimeout timeout) {
    // Check that the task is not currently associated with a slot before adding it
    assert timeout.bucket == null;
    timeout.bucket = this;
    if (head == null) {
        head = tail = timeout;
    } else{ tail.next = timeout; timeout.prev = tail; tail = timeout; }}Copy the code

HashedWheelBucket provides the Remove method for removing a specified node from a bidirectional linked list. The core logic is shown in the figure below. Locate the front node and the back node according to the node to be deleted, and adjust the next pointer of the front node and the prev pointer of the back node respectively. Some boundary cases need to be considered during deletion. After deletion, subtract one from the pendingTimeouts, the number of pending tasks in the current time round. Remove code logic is simple, I won’t post code here.

HashedWheelBucket provides expireTimeouts method. When the time wheel pointer rotates to a certain slot, the method is used to process the scheduled tasks of the bidirectional linked list on the slot, which can be divided into three situations:

  • If a scheduled task expires, the remove method is used to retrieve it and its EXPIRE method is invoked to execute the task logic.

  • If a scheduled task has been canceled, run the remove method to remove it and discard it.

  • RemainingRounds (remaining clock period) will be subtracted by one if the scheduled task has not expired.

void expireTimeouts(long deadline) {
    HashedWheelTimeout timeout = head;
 
    // The time wheel pointer is traversed from the head of the bidirectional list when it reaches a slot
    while(timeout ! =null) {
        HashedWheelTimeout next = timeout.next;
        // remainingRounds <= 0 indicates maturity
        if (timeout.remainingRounds <= 0) {
            // Remove the node from the list
            next = remove(timeout);
            // Determine that the scheduled task is indeed expired
            if (timeout.deadline <= deadline) {
                // Execute the task
                timeout.expire();
            } else {
                / / thrown exception}}else if (timeout.isCancelled()) {
            // The task is cancelled and discarded after being removed
            next = remove(timeout);
        } else {
            // The remaining clock period is reduced by one
            timeout.remainingRounds--;
        }
        // Proceed to the next task nodetimeout = next; }}Copy the code

HashedWheelBucket also provides the clearTimeouts method, which is used when the time wheel stops, traversing and removing all nodes in the bidirectional linked list and returning all untimed and uncancelled tasks.

2.4 the Worker

Worker implements the Runnable interface, and the time wheel processes the scheduled tasks in the time wheel through Worker threads. Let’s take a look at its core fields and the run method logic.

1) Set<Timeout> unprocessedTimeouts Stores unexpired and uncanceled tasks in the time wheel when it stops2) longTick Indicates a slot in the time wheel. The tick increases when the time wheel rotatesCopy the code

public void run(a) {
    // Initialize startTime, the deadline for all tasks is relative to this point in time
    startTime = System.nanoTime();
 
    // Wake up the thread blocking at start()
    startTimeInitialized.countDown();
 
    // Rotate the tick cycle as long as the time wheel is in WORKER_STATE_STARTED,
    // Processes scheduled tasks in the slot
    do {
        // Determine if it is time to process the slot, and if not, sleep for a while
        final long deadline = waitForNextTick();
        if (deadline > 0) {
            // Obtain the slot index corresponding to the tick
            int idx = (int) (tick & mask);
 
            // Clear the scheduled tasks that are cancelled by users. When the scheduled tasks are cancelled,
            CancelledTimeouts (cancelledTimeouts) Every time the needle turns
            //, the time wheel clears the queue
            processCancelledTasks();
 
            // Locate the slot based on the current pointer
            HashedWheelBucket bucket = wheel[idx];
 
            // Move scheduled tasks cached in timeouts queues to corresponding slots in the time wheel
            transferTimeoutsToBuckets();
 
            // Processes scheduled tasks in the bidirectional linked list for the slot
            bucket.expireTimeouts(deadline);
            tick++;
        }
        // Check the status of the time wheel. If the time wheel is running, repeat the above steps.
        // Perform scheduled tasks continuously
    } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this)
                                    == WORKER_STATE_STARTED);
 
    // Clear all slots and add them to the list of unprocessed tasks.
    // for the stop() method to return
    for (HashedWheelBucket bucket : wheel) {
        bucket.clearTimeouts(unprocessedTimeouts);
    }
 
    // Remove the scheduled tasks that have not been added to the slot
    // is added to the queue of unprocessed tasks for the stop() method to return
    for(; ;) { HashedWheelTimeout timeout = timeouts.poll();if (timeout == null) {
            break;
        }
        if (!timeout.isCancelled()) {
            unprocessedTimeouts.add(timeout);
        }
    }
    CancelledTimeouts cancelledTimeouts cancelledTimeouts cancelledTimeouts cancelledTimeouts cancelledTimeouts cancelledTimeouts cancelledTimeouts
    processCancelledTasks();
}
Copy the code

Here are some of the methods involved in the run method:

1) waitForNextTick

The logic is simple, it will determine whether the time to process the next slot has reached, if not, it will sleep for a while.

2) processCancelledTasks

Iterate over cancelledTimeouts to get the cancelled task and remove it from the bidirectional list.

private void processCancelledTasks(a) {
    for(; ;) { HashedWheelTimeout timeout = cancelledTimeouts.poll();if (timeout == null) {
            // all processed
            break; } timeout.remove(); }}Copy the code

3) transferTimeoutsToBuckets

When call newTimeout method, will be going to deal with the task of the cache to timeouts in the queue, the time wheel Pointers such as unified call transferTimeoutsToBuckets method processing, transfer the task to the slot specified corresponding two-way chain table, every time transfer, 100000, To avoid blocking the time wheel thread.

private void transferTimeoutsToBuckets(a) {
    // Each tick processes 10W tasks to avoid blocking the worker thread
    for (int i = 0; i < 100000; i++) {
        HashedWheelTimeout timeout = timeouts.poll();
        // Jump out of the loop when there are no more tasks
        if (timeout == null) {
            // all processed
            break;
        }
        // Cancel the slot before it is put into the slot
        if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
            continue;
        }
 
        // Calculate how many ticks the task needs to pass
        long calculated = timeout.deadline / tickDuration;
        // Count the number of rounds of the task
        timeout.remainingRounds = (calculated - tick) / wheel.length;
 
        // If a task has been in the timeouts queue for so long that its execution time has passed
        // Just use the current tick, that is, put the current bucket, and this method will be executed after it is called.
        final long ticks = Math.max(calculated, tick);
        int stopIndex = (int) (ticks & mask);
 
        // Add the task to the appropriate slotHashedWheelBucket bucket = wheel[stopIndex]; bucket.addTimeout(timeout); }}Copy the code

2.5 HashedWheelTimer

Finally, we analyze the HashedWheelTimer, which implements the Timer interface and provides the newTimeout method to add a timed task to the time wheel. The task will be temporarily stored in the Timeouts queue. When the time wheel rotates to a certain slot, Tasks in the Timeouts queue are moved to a bidirectional linked list for which a slot is responsible. It also provides a stop method to terminate the time wheel, which returns the unprocessed tasks in the time wheel. It also provides the isStop method to determine if the time wheel has terminated.

Take a look at the core fields of HashedWheelTimer.

1) HashedWheelBucket[] wheel The array is a circular queue of time wheels. Each element of the array is a slot, and one slot maintains a bidirectional linked list for storing scheduled tasks. It is initialized in the constructor, and when specified as n, it actually takes the nearest n and is2To the power of theta.2) Queue<HashedWheelTimeout> timeouts Timeouts is used to cache external scheduled tasks submitted to the time wheel3) Queue<HashedWheelTimeout> cancelledTimeouts cancelledTimeouts cancelledTimeouts is used to hold cancelled scheduled tasks temporarily. The time wheel processes data in the two queues before processing the slot's bidirectional linked list.4) Worker Worker Logic that processes scheduled tasks in a time rotation5Thread workerThread Indicates the Thread that processes scheduled tasks in the time wheel6AtomicLong pendingTimeouts Number of scheduled tasks remaining in a time round7) longTickDuration Indicates the length of time represented by each slot in the time wheel8) intWorkerState Indicates the time wheel status. Possible values are init, started, and shut DownCopy the code

Let’s look at the time wheel constructor, which initializes a time wheel. It first converts the passed parameter ticksPerWheel to a power greater than 2, which indicates how many slots there are on the time wheel. The default is 512. Then create a HashedWheelBucket[] array of that size. TickDuration is then assigned to the tickDuration of the time wheel via the tickDuration passed in. The default is 100ms. Section threadFactory creates the workerThread workerThread, which is the thread responsible for processing the scheduled tasks in the time wheel.

public HashedWheelTimer(ThreadFactory threadFactory,
                        long tickDuration, TimeUnit unit,
                        int ticksPerWheel,
                        long maxPendingTimeouts) {
 
    HashedWheelTimer regularizes the total number of time intervals on the ring
    // Convert this to a value greater than or equal to 2^n
    wheel = createWheel(ticksPerWheel);
 
    // This is used to quickly calculate the slot where the task should stay
    mask = wheel.length - 1;
 
    // The time interval for each slot in the time wheel
    this.tickDuration = unit.toNanos(tickDuration);
 
    // threadFactory is the threadFactory object that creates the thread
    workerThread = threadFactory.newThread(worker);
 
    // What is the maximum number of tasks allowed to wait for execution
    this.maxPendingTimeouts = maxPendingTimeouts;
}
 
private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
    // Calculate how many slots should really be created
    ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
 
    // Initializes the time wheel array
    HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
    for (int i = 0; i < wheel.length; i++) {
        wheel[i] = new HashedWheelBucket();
    }
    return wheel;
}
Copy the code

Once the time wheel is initialized, you can submit a scheduled task to it, which can be done using the newTimeout method provided by the time wheel. Firstly, 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 and run by the system. Then encapsulate the timed task as HashedWheelTimeout and add it to the Timeouts queue. After start, the time wheel starts running until the stop method is called to terminate the exit.

public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
    // The number of tasks to be processed increases by 1
    long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();
 
    // Start the time wheel
    start();
 
    // Calculate the deadline of the scheduled task
    long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
 
    // Create a HashedWheelTimeout object, which will first be temporarily stored in the Timeouts queue
    HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
    timeouts.add(timeout);
    return timeout;
}
Copy the code
public void start(a) {
    /** * Determine the state of the current time wheel * 1) if it is initialized, start the worker thread, start the entire time wheel * 2) if it is started, skip * 3) if it is stopped, an error */
    switch (WORKER_STATE_UPDATER.get(this)) {
        case WORKER_STATE_INIT:
            // Use cas to determine the start time wheel
            if (WORKER_STATE_UPDATER.compareAndSet(this,
                     WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
                workerThread.start();
            }
            break;
        case WORKER_STATE_STARTED:
            break;
        case WORKER_STATE_SHUTDOWN:
            / / thrown exception
        default:
            throw new Error("Invalid WorkerState");
    }
 
    // Waiting for the worker thread to initialize the time wheel to start
    while (startTime == 0) {
        try {
            // countDownLatch is used here to ensure that the scheduled thread is started
            startTimeInitialized.await();
        } catch (InterruptedException ignore) {
            // Ignore - it will be ready very soon.}}}Copy the code

Time round application

This concludes the analysis of Dubbo’s time wheel principle. Let’s take a look at how time wheels are used in Dubbo or Redisson, echoing the three examples at the beginning of this article.

1) HeartbeatTimerTask

The heartbeat task is submitted to the time wheel in Dubbo’s HeaderExchangeClient class.

private void startHeartBeatTask(URL url) {
    // The implementation of the Client determines whether to start the heartbeat task
    if(! client.canHandleIdle()) { AbstractTimerTask.ChannelProvider cp = () -> Collections.singletonList(HeaderExchangeClient.this);
        // Calculate the heartbeat interval. The minimum interval cannot be less than 1s
        int heartbeat = getHeartbeat(url);
        long heartbeatTick = calculateLeastDuration(heartbeat);
        // Create a heartbeat task
        this.heartBeatTimerTask =
               new HeartbeatTimerTask(cp, heartbeatTick, heartbeat);
        // Submit the task to IDLE_CHECK_TIMER to wait for execution. When the time is up to the time wheel, the task will be taken out for scheduling executionIDLE_CHECK_TIMER.newTimeout(heartBeatTimerTask, heartbeatTick, TimeUnit.MILLISECONDS); }}Copy the code
// The IDLE_CHECK_TIMER used above is the time wheel for our analysis in this article
private static final HashedWheelTimer IDLE_CHECK_TIMER =
                              new HashedWheelTimer(new NamedThreadFactory("dubbo-client-idleCheck".true), 1, TimeUnit.SECONDS, TICKS_PER_WHEEL);
Copy the code
// Create a HeartbeatTimerTask
@Override
protected void doTask(Channel channel) {
    try {
        // Get the last read/write time
        Long lastRead = lastRead(channel);
        Long lastWrite = lastWrite(channel);
        if((lastRead ! =null&& now() - lastRead > heartbeat) || (lastWrite ! =null && now() - lastWrite > heartbeat)) {
            // When the last read/write time exceeds the heartbeat time, a heartbeat request is sent
            Request req = new Request();
            req.setVersion(Version.getProtocolVersion());
            req.setTwoWay(true);
            // indicates a heartbeat requestreq.setEvent(HEARTBEAT_EVENT); channel.send(req); }}catch (Throwable t) {
         
    }
}
Copy the code

2) Redisson lock renewal mechanism

After the lock is successfully obtained, Redisson encapsulates a lock renewal task and puts it into the time wheel. By default, Redisson checks for 10 seconds to renew the acquired lock and extend the lock holding time. If the business machine is down, then the scheduled task that should be renewed can not run, can not be renewed, and when the lock time is up, the lock will be automatically released. The logic is encapsulated in the renewExpiration() method in RedissonLock.

private void renewExpiration(a) {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
 
    // newTimeout commits a task to the time wheel
    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) {
                    // After the task is successfully renewed, the schedule is continued and another task is added to the time wheelrenewExpiration(); }}); } }, internalLockLeaseTime /3, TimeUnit.MILLISECONDS);
 
    ee.setTimeout(task);
}
Copy the code
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    // Renew the lock through the Lua script
    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));
}
Copy the code

3) Retry after timeout

It is used in a similar way to HeartbeatTimerTask, so it is up to the reader to analyze where it was introduced.

Four,

In this paper, three examples are given to explain why time wheel is needed and the advantages of using time wheel. At the end of this paper, the application of these three examples in Dubbo or Redisson is introduced respectively. Then it explains the mechanism of single-layer time wheel and multi-layer time wheel by drawing pictures so that readers can have a simple understanding of the time wheel algorithm. In the second part, TimerTask, Timeout, HashedWheelBucket, Worker and HashedWheelTimer involved in Dubbo time wheel are explained successively, and their principles and source code implementation are analyzed.

Author: Li Wanghong, Vivo Internet Server Team