一个简单的基于 Redis 的分布式任务调度器 —— Java 语言实现

使用 Java 实现一个简单且高效的任务调度框架 本文系统地介绍了如何使用 Java 设计与实现一个高吞吐、高可用、轻量级的任务调度框架。内容涵盖任务状态管理、线程池并发执行、阻塞队列解耦、失败重试机制及分布式扩展方案(如分片与 Redis 中心化调度)。通过逐步深入的架构演进与代码示例,帮助读者理解调度系统的核心设计原则与工程实现,为构建可扩展的任务处理平台提供实用参考。 阅读详情

折腾了一周的 Java Quartz 集群任务调度,很遗憾没能搞定,网上的相关文章也少得可怜,在多节点(多进程)环境下 Quartz 似乎无法动态增减任务,恼火。无奈之下自己撸了一个简单的任务调度器,结果只花了不到 2天时间,而且感觉非常简单好用,代码量也不多,扩展性很好。

640?wx_fmt=png

实现一个分布式的任务调度器有几个关键的考虑点

  1. 单次任务和循环任务好做,难的是 cron 表达式的解析和时间计算怎么做?

  2. 多进程同一时间如何保证一个任务的互斥性?

  3. 如何动态变更增加和减少任务?

代码实例

在深入讲解实现方法之前,我们先来看看这个调度器是如何使用的

class Demo {
    public static void main(String[] args) {
        var redis = new RedisStore();
        // sample 为任务分组名称
        var store = new RedisTaskStore(redis, "sample");
        // 5s 为任务锁寿命
        var scheduler = new DistributedScheduler(store, 5);
        // 注册一个单次任务
        scheduler.register(Trigger.onceOfDelay(5), Task.of("once1", () -> {
            System.out.println("once1");
        }));
        // 注册一个循环任务
        scheduler.register(Trigger.periodOfDelay(5, 5), Task.of("period2", () -> {
            System.out.println("period2");
        }));
        // 注册一个 CRON 任务
        scheduler.register(Trigger.cronOfMinutes(1), Task.of("cron3", () -> {
            System.out.println("cron3");
        }));
        // 设置全局版本号
        scheduler.version(1);
        // 注册监听器
        scheduler.listener(ctx -> {
            System.out.println(ctx.task().name() + " is complete");
        });
        // 启动调度器
        scheduler.start();
    }
}

当代码升级任务需要增加减少时(或者变更调度时间),只需要递增全局版本号,现有的进程中的任务会自动被重新调度,那些没有被注册的任务(任务减少)会自动清除。新增的任务(新任务)在老代码的进程里是不会被调度的(没有新任务的代码无法调度),被清除的任务(老任务)在老代码的进程里会被取消调度。

比如我们要取消 period2 任务,增加 period4 任务

class Demo {
    public static void main(String[] args) {
        var redis = new RedisStore();
        // sample 为任务分组名称
        var store = new RedisTaskStore(redis, "sample");
        // 5s 为任务锁寿命
        var scheduler = new DistributedScheduler(store, 5);
        // 注册一个单次任务
        scheduler.register(Trigger.onceOfDelay(5), Task.of("once1", () -> {
            System.out.println("once1");
        }));
        // 注册一个 CRON 任务
        scheduler.register(Trigger.cronOfMinutes(1), Task.of("cron3", () -> {
            System.out.println("cron3");
        }));
        // 注册一个循环任务
        scheduler.register(Trigger.periodOfDelay(5, 10), Task.of("period4", () -> {
            System.out.println("period4");
        }));
        // 递增全局版本号
        scheduler.version(2);
        // 注册监听器
        scheduler.listener(ctx -> {
            System.out.println(ctx.task().name() + " is complete");
        });
        // 启动调度器
        scheduler.start();
    }
}

cron4j

<dependency>
    <groupId>it.sauronsoftware.cron4j</groupId>
    <artifactId>cron4j</artifactId>
    <version>2.2.5</version>
</dependency>

这个开源的 library 包含了基础的 cron 表达式解析功能,它还提供了任务的调度功能,不过这里并不需要使用它的调度器。我只会用到它的表达式解析功能,以及一个简单的方法用来判断当前的时间是否匹配表达式(是否该运行任务了)。

我们对 cron 的时间精度要求很低,1 分钟判断一次当前的时间是否到了该运行任务的时候就可以了。

class SchedulingPattern {
    // 表达式是否有效
    boolean validate(String cronExpr);
    // 是否应该运行任务了(一分钟判断一次)
    boolean match(long nowTs);
}

任务的互斥性

因为是分布式任务调度器,多进程环境下要控制同一个任务在调度的时间点只能有一个进程运行。使用 Redis 分布式锁很容易就可以搞定。锁需要保持一定的时间(比如默认 5s)。

所有的进程都会在同一时间调度这个任务,但是只有一个进程可以抢到锁。因为分布式环境下时间的不一致性,不同机器上的进程会有较小的时间差异窗口,锁必须保持一个窗口时间,这里我默认设置为 5s(可定制),这就要求不同机器的时间差不能超过 5s,超出了这个值就会出现重复调度。

public boolean grabTask(String name) {
    var holder = new Holder<Boolean>();
    redis.execute(jedis -> {
        var lockKey = keyFor("task_lock", name);
        var ok = jedis.set(lockKey, "true", SetParams.setParams().nx().ex(lockAge));
        holder.value(ok != null);
    });
    return holder.value();
}

全局版本号

我们给任务列表附上一个全局的版本号,当业务上需要增加或者减少调度任务时,通过变更版本号来触发进程的任务重加载。这个重加载的过程包含轮询全局版本号(Redis 的一个key),如果发现版本号变动,立即重新加载任务列表配置并重新调度所有的任务。

private void scheduleReload() {
    // 1s 对比一次
    this.scheduler.scheduleWithFixedDelay(() -> {
        try {
            if (this.reloadIfChanged()) {
                this.rescheduleTasks();
            }
        } catch (Exception e) {
            LOG.error("reloading tasks error", e);
        }
    }, 0, 1, TimeUnit.SECONDS);
}

重新调度任务先要取消当前所有正在调度的任务,然后调度刚刚加载的所有任务。

private void rescheduleTasks() {
    this.cancelAllTasks();
    this.scheduleTasks();
}

private void cancelAllTasks() {
    this.futures.forEach((name, future) -> {
        LOG.warn("cancelling task {}", name);
        future.cancel(false);
    });
    this.futures.clear();
}

因为需要将任务持久化,所以设计了一套任务的序列化格式,这个也很简单,使用文本符号分割任务配置属性就行。

// 一次性任务(startTime)
ONCE@2019-04-29T15:26:29.946+0800
// 循环任务,(startTime,endTime,period),这里任务的结束时间是天荒地老
PERIOD@2019-04-29T15:26:29.949+0800|292278994-08-17T15:12:55.807+0800|5
// cron 任务,一分钟一次
CRON@*/1 * * * *

$ redis-cli
127.0.0.1:6379> hgetall sample_triggers
1) "task3"
2) "CRON@*/1 * * * *"
3) "task2"
4) "PERIOD@2019-04-29T15:26:29.949+0800|292278994-08-17T15:12:55.807+0800|5"
5) "task1"
6) "ONCE@2019-04-29T15:26:29.946+0800"
7) "task4"
8) "PERIOD@2019-04-29T15:26:29.957+0800|292278994-08-17T15:12:55.807+0800|10"

线程池

时间调度会有一个单独的线程(单线程线程池),任务的运行由另外一个线程池来完成(数量可定制)。

class DistributedScheduler {
    private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private ExecutorService executor = Executors.newFixedThreadPool(threads);
}

之所以要将线程池分开,是为了避免任务的执行(IO)影响了时间的精确调度。

支持无互斥任务

互斥任务要求任务的单进程运行,无互斥任务就是没有加分布式锁的任务,可以多进程同时运行。默认需要互斥。

class Task {
    /**
     * 是否需要考虑多进程互斥(true表示不互斥,多进程能同时跑)
     */
    private boolean concurrent;
    private String name;
    private Runnable runner;
    ...
    public static Task of(String name, Runnable runner) {
        return new Task(name, false, runner);
    }

    public static Task concurrent(String name, Runnable runner) {
        return new Task(name, true, runner);
    }
}

增加回调接口

考虑到调度器的使用者可能需要对任务运行状态进行监控,这里增加了一个简单的回调接口,目前功能比较简单。能汇报运行结果(成功还是异常)和运行的耗时

class TaskContext {
    private Task task;
    private long cost;  // 运行时间
    private boolean ok;
    private Throwable e;
}

interface ISchedulerListener {
    public void onComplete(TaskContext ctx);
}

支持存储扩展

目前只实现了 Redis 和 Memory 形式的任务存储,扩展到 zk、etcd、关系数据库也是可行的,实现下面的接口即可。

interface ITaskStore {
  public long getRemoteVersion();
  public Map<String, String> getAllTriggers();
  public void saveAllTriggers(long version, Map<String, String> triggers);
  public boolean grabTask(String name);
}

代码地址

https://github.com/pyloque/taskino

640?wx_fmt=png

Python自动化车载测试:5分钟搞定CANoe COM接口调用(附完整代码) 本文详细介绍了如何使用Python快速实现CANoe COM接口调用,完成车载测试自动化。通过实战案例展示ECU信号测试框架搭建,包括环境配置、核心架构解析、信号读写封装及多线程安全方案,帮助开发者5分钟内掌握高效测试技巧。 阅读详情

相关推荐

PotPlayer终极画质调校:深入MadVR渲染器设置,让你的显示器发挥100%潜力

本文深入探讨了如何通过MadVR渲染器优化PotPlayer的画质设置,充分发挥高端显卡和显示器的潜力。从色彩空间匹配、色深优化到HDR元数据处理,提供了一套完整的配置指南,帮助用户实现影院级视觉体验。特别适合拥有NVIDIA RTX或AMD RX系列显卡的用户进行精细调校。

weixin_42530458的博客 603

Java中redis的简单使用

Java中redis的简单使用 作者:阳旭网络 环境依赖:需下载安装redis 一、在项目的pom.xml中引入jar包 一、在项目的pom.xml中引入jar包 &lt;!-- redis --&gt; &lt;dependency&gt; &lt;groupId&gt;redis.clients&lt;/groupId&gt; &lt;arti...

yangxuwang888的博客 1349

Informer复现 -- 完整模型

有了数据和核心的ProbAttention,接下来就可以复现完整的Informer(图1)了。图1:Informer模型结构[1]如果对图1还不太熟悉的同学,可以参考图2。由图2可知,informer与Transformer之间的差异并不大。主要区别是ProbAttention和可能的encoder最后一层的conv(源代码中用来distil)。图2:参考Transformer画的Informer模型结构。

AI_daily的博客 3493

java读取redis数据_Java连接redis操作数据

选择2.9.0 jar 版本下载:jedis-2.9.0.jarpackage com.hao.redis;import org.junit.Before;import org.junit.Test;import redis.clients.jedis.Jedis;import java.util.*;public class TestRedis {private Jedis jedis;@Be...

weixin_39528697的博客 1563

MCGS 和 200SMART 时间同步

在实际应用中,PLC经常会因为断电原因,导致时间不准确。假设我们设置了一个投加泵,每天8-18点投加。屏幕上显示的时间和PLC内部的时间,很可能不一样。所以需要在触摸屏上读取PLC的时间,并且提供设置功能。

mmxxoo_xxmmp的博客 1522

【上海大学计算机组成原理实验报告】六、内存系统实验

学习内存访问机制//理解代码和数据的分区存放原理和技术//手动方式把立即数`33H`写入内存`D1H`单元//手动方式把`D1H`单元的内容读出,再送入`ST`单元//在`CP226`汇编语言程序集成开发环境下编写程序//既然有`ORG`微指令,为什么集成开发环境下载到实验箱的目标程序的第一条执行语句最好存放在0号地址?//上海大学计算机组成原理实验//内存系统实验

HackerKevon的博客 3129

java调度器cron4j的使用

关于cron4j的使用 cron4j的主要实体是调度程序。使用it.sauronsoftware.cron4j.scheduler实例,您可以在一年中的固定时间执行任务。调度程序可以每分钟执行一次任务,每五分钟执行一次,星期五10:00,2月16日12:30,但仅限于星期六,依此类推。可惜没有设置到秒的操作。 使用cron4j调度程序是一个四步操作: 创建调度程序实例。 安排你的行动。要计...

jiangbb8686的博客 1351

XC95144XL-10TQG144I Xilinx XC9500XL 高性能 CPLD

XC95144XL-10TQG144I 属于 Xilinx 的 XC9500XL 高性能 CPLD 家族,是一颗面向 3.3V 低电压系统的可编程逻辑器件。该器件以小封装、高速响应和良好的系统兼容性为设计目标,常用作系统控制/ glue-logic、I/O 缓冲、状态机、总线桥接与接口协议前端等场景中的可重编程逻辑单元。产品支持现场(in-system)编程与 IEEE-1149.1 JTAG 边界扫描,能在系统板上直接更新逻辑。

2403_90082736的博客 1060

关于Jfinal自带Cron4j的定时器使用实例

cron4j是Java平台的一个调度器,使用cron4j你只需要制定一些简单的规则,就可以在规定好的时间内执行你所指定的任务。本篇我们主要介绍cron4j在JFinal中的使用。 cron4j基本概念 主要步骤: cron4j的主要对象为scheduler(调度器)。 1.我们在使用的时候需要先new一个scheduler实例。 2.通过schedule方法添加实现了java.lang.Runnable接口的实例对象作为要调度的任务(或是用cron4j提供的it.sauronsoftware...

gaochenglong1的博客 1238

AI 赋能测试全生命周期:多 Agent 协同的自动化提效方案

本文提出了一种基于Coze平台的多Agent协同测试解决方案,旨在解决软件测试中PRD解析不清晰、测试点覆盖不全、用例编写效率低、代码生成耗时等痛点。通过PRD优化、测试点生成、测试用例生成和单元测试框架生成四个Agent的协同工作,结合Cursor工具实现从需求到代码的全链路自动化。以电商平台登录模块为例,展示了从模糊PRD到标准化测试用例和可执行Java单元测试代码的完整转化流程。该方案通过AI赋能显著提升测试效率,减少人工干预,实现测试覆盖率≥95%,为快速迭代的软件研发提供了高效的测试自动化支持。

QINJIEEE的博客 1106

【uniapp踩坑记】App Store上传失败:Invalid Swift Support错误怎么解决?

作为一个前端出身的开发者,用 uniapp 开发原生 App 已经够挑战了,打包 iOS 上传 App Store 更是处处暗雷。这次的坑虽然小,但确实容易忽略。如果你也遇到了类似的报错,希望这篇文章能帮你少踩一次坑!

代码简单说 Vue、JAVA、PHP、Node.js 熟练运用,接口、架构、性能全搞定。 1143

redis java 分布式_一个简单的基于 Redis 的分布式任务调度器 —— Java 语言实现...

折腾了一周的 Java Quartz 集群任务调度,很遗憾没能搞定,网上的相关文章也少得可怜,在多节点(多进程)环境下 Quartz 似乎无法动态增减任务,恼火。无奈之下自己撸了一个简单的任务调度器,结果只花了不到 2天时间,而且感觉非常简单好用,代码量也不多,扩展性很好。实现一个分布式的任务调度器有几个关键的考虑点单次任务和循环任务好做,难的是 cron 表达式的解析和时间计算怎么做?多进程同一...

weixin_42513602的博客 206

Android 14.0 Launcher3定制化之最近任务的全部清除由左边移到下边显示

在最近14.0的系统rom产品开发中,在Launcher3的定制化开发中,在最近任务列表中,发现点击recents最近任务键后 显示的全部清除按键在左边 由于是横屏的产品显示在左边不太合理 所以要求显示在下边比较合理,所以要从Launcher3的显示流程来解决这个问题

安卓兼职framework和app工程师的博客 765

java实现语法分析器_一个简单的基于 Redis 的分布式任务调度器 —— Java 语言实现...

折腾了一周的 Java Quartz 集群任务调度,很遗憾没能搞定,网上的相关文章也少得可怜,在多节点(多进程)环境下 Quartz 似乎无法动态增减任务,恼火。无奈之下自己撸了一个简单的任务调度器,结果只花了不到 2天时间,而且感觉非常简单好用,代码量也不多,扩展性很好。实现一个分布式的任务调度器有几个关键的考虑点单次任务和循环任务好做,难的是 cron 表达式的解析和时间计算怎么做?多进程同一...

weixin_39641450的博客 189

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

java 分布式任务_一个简单的基于 Redis 的分布式任务调度器 —— Java 语言实现...

折腾了一周的 Java Quartz 集群任务调度,很遗憾没能搞定,网上的相关文章也少得可怜,在多节点(多进程)环境下 Quartz 似乎无法动态增减任务,恼火。无奈之下自己撸了一个简单的任务调度器,结果只花了不到 2天时间,而且感觉非常简单好用,代码量也不多,扩展性很好。实现一个分布式的任务调度器有几个关键的考虑点单次任务和循环任务好做,难的是 cron 表达式的解析和时间计算怎么做?多进程同一...

weixin_35705382的博客 260

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

java 调度器成功结束_一个简单的基于 Redis 的分布式任务调度器 —— Java 语言实现...

折腾了一周的 Java Quartz 集群任务调度,很遗憾没能搞定,网上的相关文章也少得可怜,在多节点(多进程)环境下 Quartz 似乎无法动态增减任务,恼火。无奈之下自己撸了一个简单的任务调度器,结果只花了不到 2天时间,而且感觉非常简单好用,代码量也不多,扩展性很好。实现一个分布式的任务调度器有几个关键的考虑点单次任务和循环任务好做,难的是 cron 表达式的解析和时间计算怎么做?多进程同一...

weixin_36081109的博客 116

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

Java实现分布式任务调度系统(基于Quartz和Redis)

Java实现分布式任务调度系统(基于Quartz和Redis)

安 1092

使用Java实现分布式任务调度器

分布式任务调度器是现代应用开发中常见的需求,特别是在微服务架构中,各个服务可能需要定时执行任务或者异步处理。本文将介绍如何使用Java实现一个简单的分布式任务调度器,以便于管理和调度分布式环境中的任务执行。这种设计能够在分布式环境中确保任务的唯一性和可靠性,适用于需要定时执行或异步处理的各种场景。启动Spring Boot应用程序,任务调度器将会周期性地执行定义的任务,并通过Redis实现分布式锁保证任务的唯一执行。大家好,我是微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!

聚娃科技开发者博客 481

SpringCloud天机学堂:分布式任务调度

官网地址:XXL-JOB的运行原理和架构如图:执行器:我们的服务引入一个XXL-JOB的依赖,就可以通过配置创建一个执行器。负责与XXL-JOB调度中心交互,执行本地任务。调度中心:一个独立服务,负责管理执行器、管理任务、任务执行的调度、任务结果和日志收集。接下来,把之前的SpringTask任务改成XXL-JOB的任务。我们修改tj-learning模块下的,将原本的@Scheduled注解替换为@XXLJob注解:其中,@XxlJob注解中定义的就是当前任务的名称。

小林的博客 2141

Redis实战:分布式队列与任务调度

1.背景介绍 Redis是一个开源的高性能的key-value存储系统,它支持数据的持久化,不仅仅支持简单的key-value类型的数据,同时还提供list,set,hash和排序等数据结构的存储。Redis还支持publish/subscribe消息通信模式,通过Lua脚本,可以对Redis的值进行操作。Redis是一个非关系型数据库,并非所有关系型数据库的替代方案。 Redis的核心特性有...

AI天才研究院 1337

Java面试-如何设计一个分布式任务调度系统?

《Java面试200问》系列文章《分布式任务调度系统设计全解析》深入探讨了分布式任务系统的核心架构与实现方案。文章从单机定时任务的局限性出发,系统分析了分布式环境下的四大核心挑战(高可用、任务重复执行、任务分片、故障转移)及应对策略,并给出了完整的架构设计图。 作者详细讲解了调度中心、执行器、任务模型等关键组件的设计要点,包括任务状态机、分片机制、负载均衡等核心技术。文章还提供了Java+Spring Boot的简易实现代码,对比了Quartz、Elastic-Job等主流框架,并针对面试常见问题给出了专业

千淘万漉虽辛苦,吹尽狂沙始到金 2万+
上一篇: RabbitMQ 消息可靠性、延时队列以及高可用集群
下一篇: 掘金 MySQL 小册的艰辛创作历程
码洞
博客等级 码龄10年 41粉丝 137原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值