系统中有几万个任务需要在各自的特定时刻触发执行,怎么做?

82 阅读3分钟

在一个系统中,有 几万个任务 需要在各自的特定时间点触发执行,常见的解决方案包括 时间轮(TimeWheel)、任务队列(DelayQueue)、分布式调度系统 等。下面是几种主要的设计思路:

方案 1:Redis + ZSet(有序集合)

核心思路

• 任务按照触发时间存入 Redis ZSet,以 时间戳作为 score,任务 ID 作为 value。

• 定时任务扫描 ZSet,取出当前时间之前的任务并执行。

实现步骤

  1. 任务入队
import redis
import time

client = redis.StrictRedis(host='localhost', port=6379, decode_responses=True)

# 添加任务,score 为触发时间戳
task_id = "task_123"
execute_time = int(time.time()) + 60  # 60 秒后执行
client.zadd("task_queue", {task_id: execute_time})

2. 任务调度

while True:
    now = int(time.time())
    tasks = client.zrangebyscore("task_queue", 0, now)  # 获取到期任务
    
    for task in tasks:
        print(f"执行任务: {task}")
        client.zrem("task_queue", task)  # 移除已执行任务
    
    time.sleep(1)  # 轮询间隔

优点

高效:Redis ZSet 按 score 排序,O(logN) 查询最近任务

支持分布式:多个调度器可以并行消费任务。

缺点

不适用于严格毫秒级调度(轮询方式有一定延迟)。

Redis 容量有限,任务量特别大时需要分片存储。

适用场景

中等规模(几万任务),执行时间要求秒级的任务调度。

方案 2:DelayQueue(基于 Java 延迟队列)

核心思路

• 使用 Java 的 DelayQueue,任务按照执行时间排序,任务到期后才能被消费

实现

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

class Task implements Delayed {
    private final long executeTime;
    private final String taskId;

    public Task(String taskId, long delayMillis) {
        this.taskId = taskId;
        this.executeTime = System.currentTimeMillis() + delayMillis;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        return Long.compare(this.executeTime, ((Task) o).executeTime);
    }

    public void execute() {
        System.out.println("执行任务: " + taskId);
    }
}

public class TaskScheduler {
    private static final DelayQueue<Task> queue = new DelayQueue<>();

    public static void main(String[] args) throws InterruptedException {
        queue.put(new Task("task_1", 5000));  // 5 秒后执行

        while (true) {
            Task task = queue.take();  // 任务到期才能取出
            task.execute();
        }
    }
}

优点

精准触发,任务不会提前执行。

性能较好,Java 原生 DelayQueue 使用最小堆存储,O(logN) 复杂度。

缺点

单点运行,无法分布式扩展(适合单机)。

任务量过大时,JVM 内存压力大

适用场景

小规模任务调度(单机 10w 任务以内) ,精准毫秒级调度。

方案 3:时间轮(TimeWheel)

核心思路

时间轮算法 使用环形结构,把任务分布到不同时间槽中,时间槽依次轮询触发任务,减少遍历成本。

实现

可以使用 Netty HashedWheelTimer

import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;

import java.util.concurrent.TimeUnit;

public class TimeWheelScheduler {
    public static void main(String[] args) {
        HashedWheelTimer timer = new HashedWheelTimer(100, TimeUnit.MILLISECONDS, 512);
        
        timer.newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) {
                System.out.println("执行任务: task_1");
            }
        }, 5, TimeUnit.SECONDS);
    }
}

优点

减少 CPU 轮询(相比 DelayQueue)。

适合大规模定时任务,支持 10w+ 任务调度。

缺点

• 任务执行时间不精确,存在一定延迟。

适用场景

大规模任务(10w+),对精确度要求不高

方案 4:分布式任务调度(Quartz / XXL-JOB)

如果任务调度需要分布式处理,可以使用 Quartz / XXL-JOB

核心思路

任务存数据库,多个 Worker 分布式执行

基于 Zookeeper / Redis 实现任务协调

XXL-JOB 示例

@XxlJob("taskJobHandler")
public void execute() {
    System.out.println("执行任务: " + System.currentTimeMillis());
}

优点

支持集群调度,避免单点故障。

任务支持失败重试,提高可靠性。

缺点

性能较低,比 Redis / DelayQueue 慢。

适用场景

大规模分布式任务调度(10w 以上任务)

最佳方案选择

推荐:

中等规模(几万任务):Redis + ZSet

超大规模(10w+ 任务):分布式任务调度

对毫秒级精准度要求高:DelayQueue

允许少量延迟(如 100ms 级别):时间轮