一、时间轮介绍
之前公司内部搭建的延迟队列服务有用到时间轮,但是一直没有了解过它的实现原理。
最近有个和支付宝对接的项目,支付宝接口有流量控制,一定的时间内只允许 N 次接口调用,针对一些业务我们需要频繁调用支付宝开放平台接口,如果不对请求做限制,很容易触发流控告警。
为了避免这个问题,我们按照一定延迟规则将任务加载进时间轮内,通过时间轮的调度来实现接口异步调用。
很多开源框架都实现了时间轮算法,这里以 Netty 为例,看下 Netty 中时间轮是怎么实现的。
1.1 快速入门
下面是一个 API 使用例子。
public class WheelTimerSamples {
private static final HashedWheelTimerInstance INSTANCE = HashedWheelTimerInstance.INSTANCE;
public static void main(String[] args) throws IOException {
INSTANCE.getWheelTimer().newTimeout(new PrintTimerTask(), 3, TimeUnit.SECONDS);
System.in.read();
}
static class PrintTimerTask implements TimerTask {
@Override
public void run(Timeout timeout) {
System.out.println("Hello world");
}
}
enum HashedWheelTimerInstance {
INSTANCE;
private final HashedWheelTimer wheelTimer;
HashedWheelTimerInstance() {
wheelTimer = new HashedWheelTimer(r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((t1, e) -> System.out.println(t1.getName() + e.getMessage()));
t.setName("-HashedTimerWheelInstance-");
return t;
}, 100, TimeUnit.MILLISECONDS, 64);
}
public HashedWheelTimer getWheelTimer() {
return wheelTimer;
}
}
}
上面的例子中我们自定义了一个 HashedWheelTimer
,然后自定义了一个 TimerTask
,将一个任务加载进时间轮,3s 后执行这个任务,怎么样是不是很简单。
在定义时间轮时建议按照业务类型进行区分,将时间轮定义为多个单例对象。
PS:因为时间轮是异步执行的,在任务执行之前 JVM 不能退出,所以 System.in.read();
这一行代码不能删除。
1.2 原理图解
二、原理分析
2.1 时间轮状态
时间轮有以下三种状态:
- WORKER_STATE_INIT:初始化状态,此时时间轮内的工作线程还没有开启
- WORKER_STATE_STARTED:运行状态,时间轮内的工作线程已经开启
- WORKER_STATE_SHUTDOWN:终止状态,时间轮停止工作
状态转换如下,转换原理会在下面讲到:
2.2 构造函数
public HashedWheelTimer(
ThreadFactory threadFactory,
long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection,
long maxPendingTimeouts) {
if (threadFactory == null) {
throw new NullPointerException("threadFactory");
}
if (unit == null) {
throw new NullPointerException("unit");
}
if (tickDuration <= 0) {
throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);
}
if (ticksPerWheel <= 0) {
throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
}
// 初始化时间轮数组,时间轮大小为大于等于 ticksPerWheel 的第一个 2 的幂,和 HashMap 类似
wheel = createWheel(ticksPerWheel);
// 取模用,用来定位数组中的槽
mask = wheel.length - 1;
// 为了保证精度,时间轮内的时间单位为纳秒
long duration = unit.toNanos(tickDuration);
// 时间轮内的时钟拨动频率不宜太大也不宜太小
if (duration >= Long.MAX_VALUE / wheel.length) {
throw new IllegalArgumentException(String.format(
"tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
tickDuration, Long.MAX_VALUE / wheel.length));
}
if (duration < MILLISECOND_NANOS) {
logger.warn("Configured tickDuration {} smaller then {}, using 1ms.",
tickDuration, MILLISECOND_NANOS);
this.tickDuration = MILLISECOND_NANOS;
} else {
this.tickDuration = duration;
}
// 创建工作线程
workerThread = threadFactory.newThread(worker);
// 非守护线程且 leakDetection 为 true 时检测内存是否泄漏
leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null;