Bootstrap

单机环境下定时任务的基本原理和常见解决方案(二)之时间轮原理分析

时间轮

生活中的时钟想必大家都不陌生,而时间轮的设计思想就是来源于生活中的时钟,这个从其命名就可以看出。

时间轮是一种环形的数据结构,我们可以将其想象成时钟的样子,时间轮上有许多格子(bucket),每个格子代表一段时间,时间轮的精度取决于一个格子的代表的时间,比如时间轮的格子是一秒跳一次,那么其调度任务的精度就是1秒,小于一秒的任务无法被时间轮调度。

时间轮上的bucket数量是有限的,而任务的数量是可以无限大的(理论上),所以时间轮使用一个链表来存储放在某个格子上的定时任务。

如下图所示 :
假设一个格子是1秒,整个时间轮有10个格子,那走一圈就表示10s,假如当前指针指向1,此时需要调度一个12s后执的任务,应该等指针走完一圈+2格再执行,因此应放入序号为3的格子,同时将round(1)保存到任务中。

指针随着时间一格格转动,走到每个格子,则检查格子中是否有可以执行的任务。此时时间轮指将链表里round=0的任务取出来执行,其他任务的round都减1。
在这里插入图片描述

简单总结一下,时间轮通过数组+链表的形式来存储定时任务,每个任务存放的bucket的计算公式:
(预计时间-时间轮开始时间)/(每一格的时间*时间轮的bucket数) 对应的商就是round,余数就是bucket的下标(本质还是取模)

Netty 需要管理大量的连接,每一个连接都会有很多检测超时任务,比如发送超时、心跳检测间隔等。
它提供了工具类 HashedWheelTimer 来实现延迟任务。该工具类就是采用时间轮原理来实现的。

Netty时间轮使用

后续的源码分析都是基于4.1.80版本的源码

  <dependency>
     <groupId>io.netty</groupId>
     <artifactId>netty-common</artifactId>
     <version>4.1.80.Final</version>
  </dependency>
    public static void main(String[] args) {
        //创建一个HashedWheelTimer时间轮,有16个格的轮子,每一秒走一个格子
        Timer timer = new HashedWheelTimer(1, TimeUnit.SECONDS, 16);
        System.out.println(Calendar.getInstance().getTime() + "开始执行任务...");
        //添加任务到时间轮中
        timer.newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) {
                System.out.println(Calendar.getInstance().getTime() + ":执行任务1");
            }
        }, 5, TimeUnit.SECONDS);
        timer.newTimeout(timeout ->
                        System.out.println(Calendar.getInstance().getTime() + ":执行任务2"), 8,
                TimeUnit.SECONDS);
    }

在这里插入图片描述

Netty 时间轮 HashedWheelTimer 源码分析

构造方法的三个参数分别代表

  • tickDuration 每一tick的时间,走一格是多久
  • timeUnit tickDuration的时间单位
  • ticksPerWheel 时间轮一共有多个格子,即一圈表示多少个tick。
  public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection, long maxPendingTimeouts) {
        this.worker = new Worker();
        //一个CountDownLatch
        this.startTimeInitialized = new CountDownLatch(1);
        //mpsc队列
        this.timeouts = PlatformDependent.newMpscQueue();
        //mpsc队列
        this.cancelledTimeouts = PlatformDependent.newMpscQueue();
        this.pendingTimeouts = new AtomicLong(0L);
        ObjectUtil.checkNotNull(threadFactory, "threadFactory");
        ObjectUtil.checkNotNull(unit, "unit");
        ObjectUtil.checkPositive(tickDuration, "tickDuration");
        ObjectUtil.checkPositive(ticksPerWheel, "ticksPerWheel");
        //创建时间轮,默认创建512个轮 就是创建一个长度为512的HashedWheelBucket数组
        this.wheel = createWheel(ticksPerWheel);
        this.mask = this.wheel.length - 1;
        //默认tickDuration=100ms 
        long duration = unit.toNanos(tickDuration);
        if (duration >= Long.MAX_VALUE / (long)this.wheel.length) {
            throw new IllegalArgumentException(String.format("tickDuration: %d (expected: 0 < tickDuration in nanos < %d", tickDuration, Long.MAX_VALUE / (long)this.wheel.length));
        } else {
            if (duration < MILLISECOND_NANOS) {
                logger.warn("Configured tickDuration {} smaller then {}, using 1ms.", tickDuration, MILLISECOND_NANOS);
                this.tickDuration = MILLISECOND_NANOS;
            } else {
                this.tickDuration = duration;
            }
            //创建一个线程workerThread,此时未启动(延迟启动,当有任务添加后再启动)
            this.workerThread = threadFactory.newThread(this.worker);
            this.leak = !leakDetection && this.workerThread.isDaemon() ? null : leakDetector.track(this);
            this.maxPendingTimeouts = maxPendingTimeouts;
            if (INSTANCE_COUNTER.incrementAndGet() > 64 && WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
                reportTooManyInstances();
            }

        }
    }

向时间轮里添加task

  public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
        ObjectUtil.checkNotNull(task, "task");
        ObjectUtil.checkNotNull(unit, "unit");
        long pendingTimeoutsCount = this.pendingTimeouts.incrementAndGet();
        //如果maxPendingTimeouts>0,则表示对于存储的任务有上限,默认无限制
        if (this.maxPendingTimeouts > 0L && pendingTimeoutsCount > this.maxPendingTimeouts) {
            this.pendingTimeouts.decrementAndGet();
            throw new RejectedExecutionException("Number of pending timeouts (" + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending timeouts (" + this.maxPendingTimeouts + ")");
        } else {
            //启动workerThread线程
            this.start();
            //判断当前任务还要多长时间执行(这里的startTime就是workerThread的启动时间,执行到这的时候startTime一定有值,否则this.start()会阻塞)
            long deadline = System.nanoTime() + unit.toNanos(delay) - this.startTime;
            if (delay > 0L && deadline < 0L) {
                deadline = Long.MAX_VALUE;
            }
            //封装成HashedWheelTimeout,并将其加入到MpscQueue(timeouts队列)
            //MPSC: Multi producer, Single consumer FIFO
            HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
            this.timeouts.add(timeout);
            return timeout;
        }
    }

workerThread线程延迟启动

    public void start() {
        switch (WORKER_STATE_UPDATER.get(this)) {
            case 0:
                //通过CAS保证线程安全,workThread线程只会启动一次
                if (WORKER_STATE_UPDATER.compareAndSet(this, 0, 1)) {
                    this.workerThread.start();
                }
            case 1:
                break;
            case 2:
                throw new IllegalStateException("cannot be started once stopped");
            default:
                throw new Error("Invalid WorkerState");
        }
        //当startTime==0L,表示workThread线程还没有启动,通过CountDownLatch阻塞在这,直到workThread线程启动
        while(this.startTime == 0L) {
            try {
                this.startTimeInitialized.await();
            } catch (InterruptedException var2) {
            }
        }

    }

WorkerThread线程执行时间轮任务

  public void run() {
            //设置startTime
            HashedWheelTimer.this.startTime = System.nanoTime();
            if (HashedWheelTimer.this.startTime == 0L) {
                HashedWheelTimer.this.startTime = 1L;
            }
            HashedWheelTimer.this.startTimeInitialized.countDown();

            int idx;
            HashedWheelBucket bucket;
            //自旋直到wheelTimer被关闭
            do {
                //计算时间轮走到下一个tick的时间点(如果没有到时间则通过sleep休眠等待),这里返回的deadline是当前时间距离时间轮启动经过的时间(deadline小于0说明数据异常,不执行操作)
                long deadline = this.waitForNextTick();
                if (deadline > 0L) {
                    //根据tick与轮的大小取模 得到当前tick所在的bucket的下标
                    idx = (int)(this.tick & (long)HashedWheelTimer.this.mask);
                    //处理已经取消的task(将取消队列里的任务从bucket丢弃,如果已经放入到bucket里的话)
                    this.processCancelledTasks();
                    bucket = HashedWheelTimer.this.wheel[idx];
                    //将timeouts队列中缓存的任务取出加入到时间轮中
                    this.transferTimeoutsToBuckets();
                    //处理当前bucket所有的到期任务
                    bucket.expireTimeouts(deadline);
                    ++this.tick;
                }
            } while(HashedWheelTimer.WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == 1);
            //wheelTimer被关闭后的处理,取出每一个bucket里还没被执行的任务,放到unprocessedTimeouts中
            HashedWheelBucket[] var5 = HashedWheelTimer.this.wheel;
            int var2 = var5.length;

            for(idx = 0; idx < var2; ++idx) {
                bucket = var5[idx];
                bucket.clearTimeouts(this.unprocessedTimeouts);
            }

            while(true) {
                HashedWheelTimeout timeout = (HashedWheelTimeout)HashedWheelTimer.this.timeouts.poll();
                //处理所有被取消的任务
                if (timeout == null) {
                    this.processCancelledTasks();
                    return;
                }
                //将加到timeouts队列里的任务添加到把未处理的unprocessedTimeouts队列中
                if (!timeout.isCancelled()) {
                    this.unprocessedTimeouts.add(timeout);
                }
            }
        }

主要流程:

1.如果HashedWheelTimer未关闭,则等待到达下一个tick的时间(未到则sleep)
2.到达下一tick时间后

  • 1)将已取消的任务丢弃
  • 2)然后将timeouts队列里的任务迁移到bucket对应的位置上
  • 3)获取当前tick对应的bucket,执行其中已经到达执行时间的任务

3.如果HashedWheelTimer已关闭,则将bucket里还没被执行的任务和timeouts队列里未取消的任务,统一放到unprocessedTimeouts队列中。
然后统一处理取消队列里的任务(processCancelledTasks) 也就是说已取消的任务在取消操作时只是放入到取消队列里,并没有从timeouts队列或者bucket里移除

  private long waitForNextTick() {
            //计算下一个tick的时间点,该时间是相对时间轮启动时间的相对时间
            long deadline = HashedWheelTimer.this.tickDuration * (this.tick + 1L);
            //自旋
            while(true) {
                //计算时间轮启动后经过的时间
                long currentTime = System.nanoTime() - HashedWheelTimer.this.startTime;
                //判断需要休眠的时间
                long sleepTimeMs = (deadline - currentTime + 999999L) / 1000000L;
                if (sleepTimeMs <= 0L) {
                    if (currentTime == Long.MIN_VALUE) {
                        return -9223372036854775807L;
                    }
                    //如果当前时间大于下一个tick的时间,则直接返回(说明到执行任务的时间了),否则sleep
                    return currentTime;
                }

                if (PlatformDependent.isWindows()) {
                    sleepTimeMs = sleepTimeMs / 10L * 10L;
                    if (sleepTimeMs == 0L) {
                        sleepTimeMs = 1L;
                    }
                }

                try {
                    //休眠对应的时间
                    Thread.sleep(sleepTimeMs);
                } catch (InterruptedException var8) {
                    if (HashedWheelTimer.WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == 2) {
                        return Long.MIN_VALUE;
                    }
                }
            }
        }
 private void transferTimeoutsToBuckets() {
     //从timeouts队列中获取任务,每次最多只能获取10万个任务
            for(int i = 0; i < 100000; ++i) {
                HashedWheelTimeout timeout = (HashedWheelTimeout)HashedWheelTimer.this.timeouts.poll();
                //timeout==null说明timeouts队列已经空了
                if (timeout == null) {
                    break;
                }
                //计算执行该任务需要放到哪个bucket下并且对应的round为多少
                if (timeout.state() != 1) {
                    //计算任务的执行时间
                    //这里的deadline是任务执行时间相对时间轮开始时间的时间,也就是计算从时间轮的开始时间算起,需要经过多少次tick
                    long calculated = timeout.deadline / HashedWheelTimer.this.tickDuration;
                    timeout.remainingRounds = (calculated - this.tick) / (long)HashedWheelTimer.this.wheel.length;
                    //calculated和tick去较大者,就是说如果当前任务的执行时间已过期,则让其在当前tick执行
                    long ticks = Math.max(calculated, this.tick);
                    //计算该任务要在哪个bucket下执行
                    int stopIndex = (int)(ticks & (long)HashedWheelTimer.this.mask);
                    HashedWheelBucket bucket = HashedWheelTimer.this.wheel[stopIndex];
                    //HashedWheelBucket底层是一个HashedWheelTimeout的链表 
                    bucket.addTimeout(timeout);
                }
            }

        }

处理已到执行时间的任务

// 这里的deadline是当前时间距离时间轮启动经过的时间
public void expireTimeouts(long deadline) {
            HashedWheelTimeout next;
            //从头遍历HashedWheelTimeout链表
            for(HashedWheelTimeout timeout = this.head; timeout != null; timeout = next) {
                next = timeout.next;
                if (timeout.remainingRounds <= 0L) {
                    next = this.remove(timeout);
                    //说明当前任务的执行时间大于deadline,中间可能哪里出现故障,抛出异常
                    if (timeout.deadline > deadline) {
                        throw new IllegalStateException(String.format("timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
                    }
                    //调用timeout.expire方法,执行task,通过ImmediateExecutor线程池执行任务,实际就是调用task的run方法
                    timeout.expire();
                } else if (timeout.isCancelled()) {
                    next = this.remove(timeout);
                } else {
                    //未到执行时间,remainingRounds-1
                    --timeout.remainingRounds;
                }
            }

        }

多层时间轮

当时间跨度很大时,提升单层时间轮的 tickDuration 可以减少空转次数,但会导致时间精度变低,使用多层时间轮既可以避免精度降低,也能减少空转次数。

如果有时间跨度较长的定时任务,则可以交给多层级时间轮去调度。

假设有一个设置为5天14 小时40 分30 秒后执行的定时任务,在 tickDuration = 1s 的单层时间轮中,需要经过:5x24x60x60+14x60x60+40x60+30 数十万次tick才能被执行。
但在 wheel1 tickDuration = 1 天,wheel2 tickDuration = 1 小时,wheel3 tickDuration = 1 分,wheel4 tickDuration = 1 秒 的四层时间轮中,只需要经过 5+14+40+30 次tick就可以了。

总结

while+sleepTimerScheduledThreadPoolExecutorHashedWheelTimer
实现方式while+sleep最小堆最小堆基于时间轮
写入效率-O(logN)O(logN)类HashMap,近似O(1)
查询效率-O(1)O(1)近似O(1)
优点实现简单 O(1)可以对大量定时任务进行统一调度线程池执行,有异常捕获机制写入性能高
缺点对于大量定时任务不便于管理单线程执行;没有异常捕获机制写入效率较低,在需要大量添加定时任务的场景下会影响性能单线程执行;没有异常捕捉机制

注意下,HashedWheelTimer 的写入和查询效率都是近似O(1),由于链表的存在,如果要执行任务的存放在长链表的末尾,那他的查询性能可能会很差,HashMap通过扰动函数来将减少hash冲突,时间轮也可以通过设置合适的时间精度,来减少hash冲突

Netty对时间轮的实现是基于他的使用场景,我们可以根据不同的业务场景对时间轮进行优化

  1. 将所有的任务交给线程池执行,避免单个任务的执行耗时较长影响下一个任务的执行
  2. 可以给每个bucket设置一个线程池来执行这个bucket的任务
  3. 假设需要在同一时刻,执行大量比较耗时的任务,那么可以通过MQ解耦,然后使用消费者并发执行任务,提高性能
    。。。。

选择哪一种方式来实现定时/延迟任务取决于各自的业务场景。

;