哎,我糊涂啊!这个需求居然没想到用时间轮来解决。

你好呀,我是歪歪。

上周不是发布了这篇文章嘛:《也是出息了,业务代码里面也用上算法了。》

里面聊到一个场景,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 谁都可以执行。

整个思路就是这样的。

思路清晰了,代码不就是"呼大模型"而出了嘛:

arduino 复制代码
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 不会给出我最开始想到的"加权负载均衡算法"。

它会直接给出"基于优先级队列"这个最符合实际场景,也是最简单有效的实现方案。

写在最后

所以,你以为到这里就结束了吗?

要我说,其实前面的这些东西其实都不重要。

你看懂了,更好。看不懂,就算求了。

通过这个事情,我想要表达的还是我写文章以来一直坚持的一个观点:鼓励分享。

谁说写文章给出的方案就一定是要十全十美的?

我前面写的这篇文章,里面就带着"技术债"。

在分享上一篇文章后,通过和读者的思维碰撞,我得到了两种更有效、更优雅的解决方案。

这就是分享后的奖励。

它不只是单向输出,而是一个相互学习的过程。

别怕写得不够好。

写的不好,最坏的结果是你默默修正了认知。

而最好的结果是什么?

你收获的可能不只是两个更好的方案,而是更多人的智慧增量,以及下次能写出更"抗打"文章的底气。

甚至,进一步来说,抛开技术层面,大胆写下你的任何思考,哪怕它带着毛刺和缝隙。

因为正是这些缝隙,让光得以照进,让其他思想得以注入。

一个人类的思想与另外一个或者一群人的思想进行碰撞,才会产生智慧的花火。

相关推荐
龙茶清欢9 小时前
5、urbane-commerce 微服务统一依赖版本管理规范
java·运维·微服务
零千叶10 小时前
【面试】Kafka / RabbitMQ / ActiveMQ
面试·kafka·rabbitmq
Tony Bai11 小时前
【Go开发者的数据库设计之道】05 落地篇:Go 语言四种数据访问方案深度对比
开发语言·数据库·后端·golang
eqwaak011 小时前
Flask实战指南:从基础到高阶的完整开发流程
开发语言·后端·python·学习·flask
麻雀202512 小时前
一键面试prompt
面试·职场和发展·prompt
海琴烟Sunshine12 小时前
Leetcode 26. 删除有序数组中的重复项
java·算法·leetcode
RoboWizard12 小时前
移动固态硬盘连接手机无法读取是什么原因?
java·spring·智能手机·电脑·金士顿
PAK向日葵12 小时前
【算法导论】NMWQ 0913笔试题
算法·面试
PAK向日葵12 小时前
【算法导论】DJ 0830笔试题题解
算法·面试
PAK向日葵12 小时前
【算法导论】LXHY 0830 笔试题题解
算法·面试