你好呀,我是歪歪。
上周不是发布了这篇文章嘛:《也是出息了,业务代码里面也用上算法了。》
里面聊到一个场景,A、B、C 三个平台需要调用下游系统的接口查询数据。
当时下游对该查询接口做了限流,只支持一秒最多一个请求。
其中 A 平台要求每个请求间隔 6s 或者以上。
B,C 平台可以接受一秒一次请求。
如何实现时间的最大化利用?
也就是把 A 平台中的间隔时间利用起来:
把中间用其他平台的数据塞满:
我当时灵机一动,想到一个骚操作:加权轮询负载均衡算法。
最终也算是实现了这个需求。
你知道的,在业务代码里面能用到算法还是一件很稀奇的事情,所以我写了上面那篇文章。
文章发布之后,经过读者提醒,我又打开了新思路。
找到了比我这个加权轮询负载均衡算法实现更加优雅的实现方案。
时间轮
其实当这个读者提到“一个平台一条时间轮”这句话的时候,我的思路一下就打开了。
时间轮,这个玩意我也会啊。
但是时间轮这个玩意,我们先按下不表,先分析一下这个读者的思路。
他在描述的时候,对时间轮加了引号,等我理解他的意思之后才知道,这个引号加的很巧妙。
因为他用的并不是真正的时间轮,而是借鉴了时间轮的“时间间隔”思路。
为了我画图方便,我先做一个预设:
- A 平台的时间间隔为 5s
- B 平台的时间间隔为 3s
- C 平台的时间间隔为 1s
然后我们需要一个初始化的时间,假设为 10 点整,这个 10 点整你可以理解为程序跑起来的时间点。
那么整个核心思路大概是这样的。
首先,在程序刚刚启动的时候,A、B、C 3 个平台都是满足条件的。
所以理论上任何一个平台都可以。
对平台进行循环,最先循环到的是 A:
然后,关键的地方就来了。
平台被选中后,按照专属的时间间隔更新下次执行时间:
A 平台下次执行时间就变成了 10:00:05。
按照这个逻辑往下推。
前三秒就是这样的:
继续往下,到第六秒的时候,是这样的:
到这里,我必须把 10:00:05 这个时间切片单独拿出来给你看看,这个点非常关键:
你说,这个时候应该是 A 执行还是 C 执行呢,毕竟它们都满足“当前时间大于等于下次执行时间”这个判断条件。
按理来说,代码循环的时候,最新取到的肯定是 A,所以 A 先执行,没毛病。
但是实际上大概率是 C 执行。
为啥呢?
回到 A 的下次执行时间被更新时的这个时间点:
你仔细分析图中的这句话:
更新平台 A 的下次执行时间:System.currentTimeMillis()+5s
那平台 A 的下次执行时间会正正好好是 10:00:05 吗?
有 System.currentTimeMillis() 的存在,是不是 10:00:05:123 这样带着点毫秒数的时间更加合理点?
所以,当我把毫秒数带着,这样你再看,是不是大概率是 C 执行了:
那么问题又来了:为什么是大概率 C 执行呢?
在什么情况下可能会把 A 选出来执行呢?
在 GC 抖动的情况下,当前时间可能也会往前偏移一点,可能会把 A 选出来执行。
但是,A、C 谁先谁后,根本不重要,因为在当前的情况下,A、C 谁都可以执行。
整个思路就是这样的。
思路清晰了,代码不就是“呼大模型”而出了嘛:
[code]public class PlatformLaneScheduler {
// 平台配置类
static class Platform {
final String name;
final long interval; // 执行间隔(毫秒)
long nextAllowedTime; // 下次允许执行时间
public Platform(String name, long intervalSeconds) {
this.name = name;
this.interval = intervalSeconds * 1000;
this.nextAllowedTime = System.currentTimeMillis(); // 初始可立即执行
}
}
// 全局状态
private final Map platforms = new HashMap();
private long lastExecutionTime = 0; // 上次执行时间
private final ScheduledExecutorService scheduler;
public PlatformLaneScheduler() {
// 初始化调度器(单线程)
scheduler = Executors.newSingleThreadScheduledExecutor();
// 添加平台配置
addPlatform("A", 5);
addPlatform("B", 3);
addPlatform("C", 1);
}
public void addPlatform(String name, long intervalSeconds) {
platforms.put(name, new Platform(name, intervalSeconds));
}
public void start() {
// 每100ms巡检一次
scheduler.scheduleAtFixedRate(this::checkPlatforms, 0, 100, TimeUnit.MILLISECONDS);
}
public void stop() {
scheduler.shutdown();
}
private void checkPlatforms() {
long now = System.currentTimeMillis();
// 检查全局限流:1秒内只能执行一次
if (now - lastExecutionTime |