时间轮(Timing Wheel)是一种高效的定时任务调度算法,广泛应用于网络编程、分布式系统等需要处理大量定时任务的场景。它的核心思想是将时间划分为固定大小的槽位(Bucket),每个槽位对应一个时间区间,任务根据其过期时间被分配到对应的槽位中。
在传统的定时任务实现中,通常使用优先级队列(如Java的DelayQueue)来管理任务。这种方式虽然简单,但在处理大量任务时存在性能瓶颈:
时间轮通过以下方式解决了这些问题:
一个基本的时间轮实现需要定义三个关键参数:
例如,一个tickMs=100ms,wheelSize=512的时间轮:
java复制/**
* 定时任务接口
*/
public interface TimerTask {
/**
* 任务执行方法
* @param timeout 超时对象,包含任务状态信息
*/
void run(Timeout timeout);
}
这个接口定义了定时任务的基本契约,任何需要被调度的任务都需要实现这个接口。run方法接收一个Timeout参数,允许任务在执行时访问自己的状态信息。
java复制/**
* 超时接口,表示一个定时任务的状态
*/
public interface Timeout {
/**
* 获取关联的定时任务
*/
TimerTask task();
/**
* 检查任务是否已过期
*/
boolean isExpired();
/**
* 检查任务是否已取消
*/
boolean isCancelled();
/**
* 取消任务
* @return 取消是否成功
*/
boolean cancel();
}
Timeout接口提供了任务状态管理的功能,允许外部查询和控制任务的生命周期。
java复制/**
* 定时任务条目,维护任务链表关系
*/
class TimerTaskEntry implements Comparable<TimerTaskEntry> {
// 任务取消标志
private volatile boolean cancelled = false;
// 链表指针
private TimerTaskEntry next;
private TimerTaskEntry prev;
// 关联的定时任务
private final TimerTask timerTask;
// 过期时间戳(毫秒)
private final long expirationMs;
public TimerTaskEntry(TimerTask timerTask, long expirationMs) {
this.timerTask = timerTask;
this.expirationMs = expirationMs;
}
/**
* 从链表中移除当前节点
*/
public void remove() {
synchronized (this) {
if (next != null) {
next.prev = prev;
}
if (prev != null) {
prev.next = next;
}
next = null;
prev = null;
}
}
}
TimerTaskEntry是时间轮中的核心数据结构,它:
注意:这里使用了synchronized进行同步,确保链表操作的线程安全。在高并发场景下,可以考虑使用更高效的并发控制机制。
java复制/**
* 定时任务链表,管理同一时间槽中的多个任务
*/
class TimerTaskList {
// 使用原子计数器记录任务数量
private final AtomicInteger taskCounter = new AtomicInteger(0);
// 哨兵节点,简化链表操作
private final TimerTaskEntry sentinal = new TimerTaskEntry(null, -1);
public TimerTaskList() {
// 初始化空链表
sentinal.next = sentinal;
sentinal.prev = sentinal;
}
/**
* 添加任务到链表头部
*/
public boolean add(TimerTaskEntry timerTaskEntry) {
boolean done = false;
while (!done) {
timerTaskEntry.remove();
synchronized (this) {
if (!timerTaskEntry.isCancelled()) {
// 标准双向链表插入操作
timerTaskEntry.next = sentinal.next;
timerTaskEntry.prev = sentinal;
sentinal.next.prev = timerTaskEntry;
sentinal.next = timerTaskEntry;
taskCounter.incrementAndGet();
done = true;
}
}
}
return true;
}
}
TimerTaskList的特点:
java复制/**
* 时间槽,存储特定时间范围内的任务
*/
class Bucket {
private final TimerTaskList taskList = new TimerTaskList();
// 使用原子变量记录槽位过期时间
private final AtomicLong expiration = new AtomicLong(-1L);
/**
* 添加任务到时间槽
*/
public void addTask(TimerTaskEntry timeout) {
if (taskList.add(timeout)) {
// 更新槽位过期时间为最早的任务时间
long bucketExpiration = expiration.get();
if (timeout.expirationMs() < bucketExpiration || bucketExpiration == -1L) {
expiration.set(timeout.expirationMs());
}
}
}
}
Bucket类的关键点:
java复制/**
* 时间轮核心实现
*/
public class TimingWheel {
private final long tickMs; // 每个槽位的时间跨度
private final int wheelSize; // 槽位数量
private final long interval; // 总时间跨度(tickMs * wheelSize)
private final AtomicLong currentTime; // 当前时间指针
private final List<Bucket> buckets; // 槽位数组
private final TimingWheel overflowWheel; // 上层时间轮
public TimingWheel(long tickMs, int wheelSize, long startTime) {
this(tickMs, wheelSize, startTime, null);
}
/**
* 添加定时任务
*/
public boolean add(TimerTaskEntry timerTaskEntry) {
long expiration = timerTaskEntry.expirationMs();
if (timerTaskEntry.isCancelled()) {
return false;
}
long calculatedExpiration = expiration - currentTime.get();
if (calculatedExpiration < tickMs) {
// 任务即将过期,立即执行
return false;
} else if (calculatedExpiration < interval) {
// 计算槽位索引
long virtualId = expiration / tickMs;
int index = (int) (virtualId % wheelSize);
Bucket bucket = buckets.get(index);
bucket.addTask(timerTaskEntry);
return true;
} else {
// 任务超出当前时间轮范围,交给上层时间轮
if (overflowWheel == null) {
// 创建上层时间轮
long newTickMs = interval;
int newWheelSize = wheelSize;
overflowWheel = new TimingWheel(newTickMs, newWheelSize, currentTime.get());
}
return overflowWheel.add(timerTaskEntry);
}
}
}
多层时间轮的关键设计:
java复制/**
* 哈希时间轮定时器
*/
public class HashedWheelTimer implements Timer {
private final TimingWheel timingWheel;
private final BlockingQueue<HashedWheelTimeout> timeouts = new LinkedBlockingQueue<>();
private final ExecutorService taskExecutor;
private final Thread workerThread;
public HashedWheelTimer(long tickDuration, int ticksPerWheel, long startTime) {
this.timingWheel = new TimingWheel(tickDuration, ticksPerWheel, startTime);
this.taskExecutor = Executors.newCachedThreadPool();
this.workerThread = new Thread(new Worker(), "HashedWheelTimerWorker");
workerThread.setDaemon(true);
workerThread.start();
}
@Override
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
long deadline = System.currentTimeMillis() + unit.toMillis(delay);
HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
timeouts.offer(timeout);
return timeout;
}
private class Worker implements Runnable {
@Override
public void run() {
while (!shutdown.get()) {
// 1. 处理新任务
fetchFromBucket();
// 2. 推进时间轮
timingWheel.advanceClock(System.currentTimeMillis());
// 3. 处理过期任务
processExpiredTimeouts();
// 4. 短暂休眠
try {
Thread.sleep(1);
} catch (InterruptedException ignored) {}
}
}
}
}
java复制public class TimerExample {
public static void main(String[] args) {
// 创建时间轮定时器
// tickDuration=10ms, 512个槽位
Timer timer = new HashedWheelTimer(10, 512);
// 创建定时任务
TimerTask task = timeout -> {
System.out.println("任务执行时间: " + System.currentTimeMillis());
};
// 3秒后执行
timer.newTimeout(task, 3, TimeUnit.SECONDS);
// 10秒后执行
timer.newTimeout(task, 10, TimeUnit.SECONDS);
}
}
参数调优:
异常处理:
监控指标:
现象:任务实际执行时间晚于预期
排查:
解决:
现象:内存持续增长不释放
排查:
解决:
| 特性 | HashedWheelTimer | ScheduledThreadPoolExecutor |
|---|---|---|
| 时间复杂度 | O(1)添加/取消 | O(log n)添加/取消 |
| 内存占用 | 固定大小 | 随任务数量增长 |
| 精度 | 受限于tickMs | 更高精度 |
| 适用场景 | 大量短周期任务 | 少量高精度任务 |
Kafka的实现进行了以下优化:
在实际项目中,可以根据需求选择直接使用Kafka的时间轮实现(kafka.utils.timer)。
在分布式系统中,可以通过以下方式扩展时间轮:
在流处理系统中,时间轮可用于:
例如Flink的WindowOperator内部就使用了类似时间轮的机制来管理窗口触发。
在实现自己的时间轮后,建议进行以下测试:
可以使用JMH(Java Microbenchmark Harness)编写基准测试,例如:
java复制@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public class TimerBenchmark {
private Timer timer;
private AtomicLong counter;
@Setup
public void setup() {
timer = new HashedWheelTimer(10, 512);
counter = new AtomicLong();
}
@Benchmark
public void testScheduleTask() {
timer.newTimeout(timeout -> counter.incrementAndGet(),
10, TimeUnit.MILLISECONDS);
}
}