糊涂啊!這個需求居然沒想到用時間輪來解決。
你好呀,我是歪歪。
上周不是發布了這篇文章嘛:《也是出息了,業務代碼里面也用上算法了。》
里面聊到一個場景,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 誰都可以執行。
整個思路就是這樣的。
思路清晰了,代碼不就是“呼大模型”而出了嘛:
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<String, Platform> 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 < 1000) {
return;
}
// 查找最早到期的平臺
Platform earliestPlatform = null;
long minNextTime = Long.MAX_VALUE;
for (Platform platform : platforms.values()) {
if (platform.nextAllowedTime <= now && platform.nextAllowedTime < minNextTime) {
earliestPlatform = platform;
minNextTime = platform.nextAllowedTime;
}
}
// 執行符合條件的平臺請求
if (earliestPlatform != null) {
executePlatformRequest(earliestPlatform, now);
}
}
private void executePlatformRequest(Platform platform, long now) {
// 執行請求
System.out.printf("[%tT.%tL] %s平臺執行 | 設定間隔: %ds | 實際間隔: %.3fs%n",
now, now, platform.name, platform.interval / 1000,
(now - platform.nextAllowedTime + platform.interval) / 1000.0);
// 更新平臺狀態
platform.nextAllowedTime = now + platform.interval;
// 更新全局狀態
lastExecutionTime = now;
}
public static void main(String[] args) throws InterruptedException {
PlatformLaneScheduler laneScheduler = new PlatformLaneScheduler();
laneScheduler.start();
// 運行60秒
TimeUnit.SECONDS.sleep(60);
laneScheduler.stop();
}
}
從代碼執行結果來看,第 6 秒,它確實選擇了 C 平臺執行:

老實說,這個解決方案,比我那個劍走偏鋒的加權輪詢負載均衡的方案好多了。
可以支持任意多個平臺,每個平臺都可以配置個性化的時間間隔。
而且這個方案的底層邏輯理解起來的成本也非常低。
但是,你看看這章的小標題,叫做“時間輪”。
上面這個方案并不是真正意義上的時間輪。
真正的時間輪
那么什么是時間輪呢?
首先時間輪最基本的結構其實就是一個數組,比如下面這個長度為 8 的數組:

怎么變成一個輪呢?
首尾相接就可以了:

假如每個元素代表一秒鐘,那么這個數組一圈能表達的時間就是 8 秒,就是這樣的:

注意我前面強調的是一圈,為 8 秒。
那么 2 圈就是 16 秒, 3 圈就是 24 秒,100 圈就是 800 秒。
這個能理解吧?
我再給你配個圖:

雖然數組長度只有 8,但是它可以在上疊加一圈又一圈,那么能表示的數據就多了。
比如我把上面的圖的前三圈改成這樣畫:

希望你能看明白,如果你看不明白,不要懷疑自己,肯定是垃圾作者畫得不行,和你自己沒關系。

記住,全文重點:與其反思自己,不如指責別人。
我畫上面的圖主要是要你知道這里面有一個“第幾圈”的概念。
好了,我現在把前面的這個數組美化一下,從視覺上也把它變成一個輪子。
輪子怎么說?
輪子的英文是 wheel,所以我們現在有了一個叫做 wheel 的數組:

然后,把前面的數據給填進去大概是長這樣的。
為了方便示意,我只填了下標為 0 和 3 的位置,其他地方也是一個意思:

那么問題就來了。假設這個時候我有一個需要在 800 秒之后執行的任務,應該是怎么樣的呢?
800 mod 8 =0,說明應該掛在下標為 0 的地方:

假設又來一個 400 秒之后需要執行的任務呢?
同樣的道理,繼續往后追加即可:

不要誤以為下標對應的鏈表中的圈數必須按照從小到大的順序來,這個是沒有必要的。
好,現在又來一個 403 秒后需要執行的任務,應該掛在哪兒?
403 mod 8 = 3,那么就是這樣的:

我為什么要不厭其煩的給你說怎么計算,怎么掛到對應的下標中去呢?
因為我還需要引出一個東西:待分配任務的隊列。
上面畫 800 秒、 400 秒和 403 秒的任務的時候,我還省略了一步。
其實應該是這樣的:

你看這個玩意,是不是就和我們討論的場景很像了。
還是這一套老參數:
A 平臺的時間間隔為 5s B 平臺的時間間隔為 3s C 平臺的時間間隔為 1s
假設我們的時間輪一圈是 8s,這個時間你可以自定義,你要是喜歡 10s 也不是不可以。
那么第一個八秒,即第一圈,應該是這樣的:
第 1 秒,A 平臺執行。放在第 1 圈,第 1 個位置 第 2 秒,B 平臺執行。放在第 1 圈,第 2 個位置 第 3 秒,C 平臺執行。放在第 1 圈,第 3 個位置 第 4 秒,C 平臺執行。放在第 1 圈,第 4 個位置 第 5 秒,B 平臺執行。放在第 1 圈,第 5 個位置 第 6 秒,C 平臺執行。放在第 1 圈,第 6 個位置 第 7 秒,A 平臺執行。放在第 1 圈,第 7 個位置 第 8 秒,C 平臺執行。放在第 2 圈,第 0 個位置

第二個八秒,即第二圈,是這樣的,我用不同的顏色來表示:
第 9 秒,B 平臺執行。放在第 2 圈,第 1 個位置 第 10 秒,C 平臺執行。放在第 2 圈,第 2 個位置 第 11 秒,C 平臺執行。放在第 2 圈,第 3 個位置 第 12 秒,A 平臺執行。放在第 2 圈,第 4 個位置 第 13 秒,B 平臺執行。放在第 2 圈,第 5 個位置 第 14 秒,C 平臺執行。放在第 2 圈,第 6 個位置 第 15 秒,C 平臺執行。放在第 2 圈,第 7 個位置 第 16 秒,B 平臺執行。放在第 3 圈,第 0 個位置

這玩意,才叫真正的時間輪。
以時間間隔為桶,根本不關心桶里面裝的是哪個平臺的數據。
反正時間一到,就把桶里面的數據往外扔就完事了。
你理解了真正的時間輪,你就知道為什么我說前面讀者給出來的方案中的“時間輪”這個引號加的很巧妙。
他只是借用了時間輪中"定期檢查"的思想,但從數據結構、調度機制和實現方式上,都與真正的時間輪有本質區別。
有點那種就是:我也不想解釋我這個方案不是真正的時間輪,我只是聯想到了真正的時間輪,借鑒了里面的一些思路。但是我又懶得給你解釋這么多,我就加個引號在這里,你自己去悟。能不能悟出來,就看個人道行深淺了。

扔掉時間輪
所以,你以為到這里就結束了嗎?
這篇文章下面還有一個評論。
這個評論寥寥數語,就給出了一個我認為是最佳方案的方案:

為什么不像上面說的用時間輪?
因為平臺數比包數小很多,不需要時間輪這種復雜結構。
所以,還有高手?

前面我們說了,如果用真正的時間輪的話,里面放的是什么?
一圈又一圈的,放的是每個平臺的數據。
但是換個視角,如果我們只關注平臺呢?

把所有的平臺都放到二叉堆里面去,利用二叉堆這個數據結構,幫我們實現平臺維度的排序。
數據結構一換,整個解題思路又不一樣了。
而這位讀者的思路,和 DeepSeek 的思路是一致的。

直接給出了可運行的代碼。
核心是基于 Java 的 PriorityQueue 實現:

不一樣的是 DeepSeek 還給出了“錦上添花”的部分:

DeepSeek 怎么說
我還追問了 DeepSeek,讓其用時間輪來解題。
它也解了:

隨便給我對比了一下兩個方案的優劣勢:

然后給出了推薦的方案:


所以,這里也是在回答前面那篇文章中的這個留言:

從實踐來看,AI 不會給出我最開始想到的“加權負載均衡算法”。
它會直接給出“基于優先級隊列”這個最符合實際場景,也是最簡單有效的實現方案。
寫在最后
所以,你以為到這里就結束了嗎?
要我說,其實前面的這些東西其實都不重要。
你看懂了,更好。看不懂,就算求了。
通過這個事情,我想要表達的還是我寫文章以來一直堅持的一個觀點:鼓勵分享。
誰說寫文章給出的方案就一定是要十全十美的?
我前面寫的這篇文章,里面就帶著“技術債”。
在分享上一篇文章后,通過和讀者的思維碰撞,我得到了兩種更有效、更優雅的解決方案。
這就是分享后的獎勵。
它不只是單向輸出,而是一個相互學習的過程。
別怕寫得不夠好。
寫的不好,最壞的結果是你默默修正了認知。
而最好的結果是什么?
你收獲的可能不只是兩個更好的方案,而是更多人的智慧增量,以及下次能寫出更“抗打”文章的底氣。
甚至,進一步來說,拋開技術層面,大膽寫下你的任何思考,哪怕它帶著毛刺和縫隙。
因為正是這些縫隙,讓光得以照進,讓其他思想得以注入。
一個人類的思想與另外一個或者一群人的思想進行碰撞,才會產生智慧的花火。


浙公網安備 33010602011771號