任务调度常见方案

说起任务调度,很多时候我们都在用单机的任务调度器,比如Timer、ScheduledThreadPoolExecutor或者Spring内置的@Scheduled。
还有就是一些可以整合到项目中的任务调度框架,如Quartz。
要么就是分布式任务调度中间件,比如xxl-job等等……
优缺点:
单机任务调度,简单方便,但是在多机部署的环境下,需要考虑并处理任务同时触发的情况。虽然这个问题可以用分布式锁来解决,但还是不够优雅。
也有用单台机器crontab来触发任务的案例,但是毕竟是单点,理论上不是很可靠。
引用Quartz等框架的方案,会把任务调度耦合到项目中,而且配置也很复杂,不推荐。
分布式任务调度中间件的方案相对完美,使用方便,配置灵活。但是想要达到相应的可靠性,中间件也需要集群化部署,所以在无形中也增加了运维、部署成本。
这里推荐另一种方案,就是使用Redisson实现的分布式任务调度服务。毕竟Redis常有,而中间件不常有。

流程图

redisson schedule redisson scheduler_redisson schedule

这里的实例,指的是Redisson实例。既可以是同一JVM进程中的不同实例,也可以是不同JVM进程中的不同实例。
流程上,是由Master实例发布任务,经过Redis中转,Worker实例订阅主题,并且不断拉取任务并执行。
如图所示,同一个任务,是在3个Worker中轮询执行的。如果中途有一个Worker挂掉,则会在剩下的2个Worker中继续轮询。

代码示例

RunnableTask
创建一个任务类,打印日期和Redisson实例ID。
package cn.mrxionge.idemo.redis;

import org.redisson.api.RedissonClient;
import org.redisson.api.annotation.RInject;

import java.io.Serializable;
import java.time.LocalDate;

public class RunnableTask implements Runnable, Serializable {

    @RInject
    private RedissonClient client;

    @Override
    public void run() {
        LocalDate date = LocalDate.now();
        System.out.println("task run ...");
        System.out.println("today is " + date);
        System.out.println("redisson client is " + client.getId());
    }
}
SchedulerWorker
循环创建3个Worker实例。
package cn.mrxionge.idemo.redis;

import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RScheduledExecutorService;
import org.redisson.api.RedissonClient;
import org.redisson.api.WorkerOptions;
import org.redisson.config.Config;

@Slf4j
public class SchedulerWorker {

    public static void main(String[] args) {
        //任务队列名
        String key = "service:task_scheduler";
        //连接配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456").setDatabase(0);
        //模拟多个worker实例
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                log.debug("创建worker实例");
                RedissonClient client = Redisson.create(config);
                RScheduledExecutorService executorService = client.getExecutorService(key);
                log.debug("注册为worker节点");
                executorService.registerWorkers(WorkerOptions.defaults());
            }).start();
        }
    }
}
SchedulerMaster
创建1个Master实例,并且发布一个首次间隔2秒,后续每隔1秒执行一次的任务。且在10秒之后取消任务。(实际执行的任务数是8次)
package cn.mrxionge.idemo.redis;

import cn.hutool.core.thread.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RScheduledExecutorService;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.concurrent.TimeUnit;

@Slf4j
public class SchedulerMaster {

    public static void main(String[] args) {
        //任务队列名
        String key = "service:task_scheduler";
        //连接配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456").setDatabase(0);
        log.debug("创建master实例");
        RedissonClient client = Redisson.create(config);
        RScheduledExecutorService executorService = client.getExecutorService(key);
        log.debug("发布任务");
        executorService.scheduleAtFixedRate(new RunnableTask(), 2, 1, TimeUnit.SECONDS);
        ThreadUtil.sleep(10000);
        log.debug("遍历并取消任务");
        executorService.getTaskIds().forEach(id -> {
            log.debug("取消任务 任务id: {}", id);
            executorService.cancelTask(id);
        });
    }
}
我们先启动Worker,再启动Master,看一下执行结果。
Worker运行结果。
2021-11-22 23:46:41.713 [Thread-2] DEBUG c.m.i.r.SchedulerWorker - 创建worker实例
2021-11-22 23:46:41.713 [Thread-0] DEBUG c.m.i.r.SchedulerWorker - 创建worker实例
2021-11-22 23:46:41.713 [Thread-1] DEBUG c.m.i.r.SchedulerWorker - 创建worker实例
2021-11-22 23:46:42.237 [Thread-1] INFO  org.redisson.Version - Redisson 3.16.4
2021-11-22 23:46:42.237 [Thread-0] INFO  org.redisson.Version - Redisson 3.16.4
2021-11-22 23:46:42.237 [Thread-2] INFO  org.redisson.Version - Redisson 3.16.4
2021-11-22 23:46:42.696 [redisson-netty-2-17] INFO  o.r.c.p.MasterPubSubConnectionPool - 1 connections initialized for 127.0.0.1/127.0.0.1:6379
2021-11-22 23:46:42.696 [redisson-netty-3-16] INFO  o.r.c.p.MasterPubSubConnectionPool - 1 connections initialized for 127.0.0.1/127.0.0.1:6379
2021-11-22 23:46:42.696 [redisson-netty-4-18] INFO  o.r.c.p.MasterPubSubConnectionPool - 1 connections initialized for 127.0.0.1/127.0.0.1:6379
2021-11-22 23:46:42.718 [redisson-netty-2-18] INFO  o.r.c.p.MasterConnectionPool - 24 connections initialized for 127.0.0.1/127.0.0.1:6379
2021-11-22 23:46:42.719 [redisson-netty-4-19] INFO  o.r.c.p.MasterConnectionPool - 24 connections initialized for 127.0.0.1/127.0.0.1:6379
2021-11-22 23:46:42.719 [redisson-netty-3-19] INFO  o.r.c.p.MasterConnectionPool - 24 connections initialized for 127.0.0.1/127.0.0.1:6379
2021-11-22 23:46:42.769 [Thread-1] DEBUG c.m.i.r.SchedulerWorker - 注册为worker节点
2021-11-22 23:46:42.769 [Thread-0] DEBUG c.m.i.r.SchedulerWorker - 注册为worker节点
2021-11-22 23:46:42.769 [Thread-2] DEBUG c.m.i.r.SchedulerWorker - 注册为worker节点
task run ...
today is 2021-11-22
redisson client is a1cbf4c7-9110-4231-8d77-e679bd48d336
task run ...
today is 2021-11-22
redisson client is c45f7a98-cc06-46fa-9454-edc1938346c6
task run ...
today is 2021-11-22
redisson client is a69deb7c-df18-48b6-954a-1d2874cf5e8f
task run ...
today is 2021-11-22
redisson client is a1cbf4c7-9110-4231-8d77-e679bd48d336
task run ...
today is 2021-11-22
redisson client is c45f7a98-cc06-46fa-9454-edc1938346c6
task run ...
today is 2021-11-22
redisson client is a69deb7c-df18-48b6-954a-1d2874cf5e8f
task run ...
today is 2021-11-22
redisson client is a1cbf4c7-9110-4231-8d77-e679bd48d336
task run ...
today is 2021-11-22
redisson client is c45f7a98-cc06-46fa-9454-edc1938346c6
查看运行日志,可以看出任务执行8次。通过Redisson实例ID,也能看出,任务是在3个Worker实例中轮询执行的。

扩展

发布任务的方法不止这一种,还可以通过cron表达式来处理。
例如:
executorService.schedule(new RunnableTask(), CronSchedule.of("* * * * * ?"));
这种方案的优点就是不必引入额外的框架或中间件,基于现有的Redis即可实现。缺点就是任务的触发配置是写在代码中的,不方便修改。不过通常来说,任务的触发逻辑和频率很少会修改。