分布式调度系统设计文档

分布式调度系统设计文档

目标与需求

项目目前主要使用xxljob做分布式调度, 但是它不太适合税务的系统, 因为税务每次跑定时任务的时候,有一大批企业都需要跑, 而xxljob,只能控制触发,不能控制多个节点,各自负责哪些企业, 不能最大化利用系统资源,而且还导致必须大量加锁,所以本次需要去掉xxljob,改为自己开发

  1. 最大化利用系统资源

  2. 节点注册,主备切换,主节点死机,备节点自动升级

  3. 主节点分配企业偏移量,备节点执行任务

架构概述

我们采用主备架构设计,主节点负责任务的注册和分配,同时也负责执行任务,备节点只负责任务的执行。主节点和备节点之间通过心跳机制保持通信,以便实时监控节点状态。

任务分为账套表任务、税局账号信息表任务和普通任务。

定时任务最小粒度为1秒

数据库设计

job_config:定时任务配置表

字段:

字段说明类型
id主键int
job_handler调度任务varchar
job_name任务名称varchar
schedule_type调度类型(1-固定速度,2-cron表达式),如果执行频繁需要使用固定速度,固定速度是秒为单位tinyint
schedule_conf调度配置,值含义取决于调度类型varchar
type任务类型(NORMAL-普通任务,SYS_COMPANY-企业表任务,BASIC_TAX_BUREAU_LOGIN-税局表任务)varchar
route_policy路由策略(1-单点执行,2-分片广播)tinyint
trigger_last_time上次调度时间datetime
trigger_status调度状态:0-停止,1-运行tinyint

job_log:定时任务运行记录表

字段:

字段说明类型
id主键bigint
job_id任务idint
executor_time任务发送到节点的时间datetime
executor_node_ip执行任务的节点IPvarchar
offset_start偏移量开始int
offset_end偏移量结束int
handle_result任务发送结果(0-失败,1-成功)tinyint
executor_result任务执行结果(0-失败,1-成功,2,执行中)tinyint

job_exe_now_config:手动执行一次配置表

字段说明类型
ID主键int
job_id任务idint
param执行时传递的参数text
status可执行状态(0-已执行,1-待执行)tinyint

流程逻辑

服务注册设计

节点启动后,启动一个心跳机制,每秒将自己的信息注册(更新)至redis(服务名、ip、节点类型、可用资源、最后一次心跳时间等),并尝试注册为主节点。

心跳:

  1. 除了每秒更新节点信息,主节点扫描每个节点的最后心跳时间,如果最后心跳时间停止更新超过1分钟,则删除节点信息。

  2. 备节点扫描主节点的最后心跳时间,如果主节点心跳停止时间过长则删除主节点信息,进行重新选举主节点。

  3. 主节点每次心跳还要判断自己还是不是主节点(因为主节点可能会因为断网时间过长导致主节点被其他节点抢走了),如果不是则停止任务调度线程

主节点选举设计

  1. 通过redis setnx命令实现,所有的服务启动成功后争抢同一个KEY,抢到的成为主节点,其他节点通过心跳判断主节点是否还在线,如果主节点的最后心跳时间停止过长,则认为主节点已经失效,删除主节点信息,其他节点开始争抢成为主节点。

任务调度分配设计

  1. 服务启动时先从数据库读取每个预设表的数据条目总数,然后缓存起来,并且监听这张表,如果有变动则进行实时更新

  2. 任务分配:

    1. 方案一:通过spring的定时任务,开启一个每秒执行一次的定时任务,每次从任务表读取符合执行条件的任务,从redis中获取所有的节点信息,分配偏移量后进行执行;
    2. 方案二:主节点扫描任务配置表有效的任务,将任务添加到spring本身定时任务模块中执行(ThreadPoolTaskScheduler.schedule)
  • 预设任务分配:如果任务是分片广播,则获取对应表的数据总数,然后平均分配给每个节点,如果是单点执行,则获取资源最多的节点,然后执行所有的企业

  • 普通任务执行:如果是分片广播则所有节点都进行执行,如果是单点执行,则获取资源最多的节点执行任务

  • 任务调用:主节点确认好需要执行的节点等信息后,通过RPC调用对应的节点的添加任务方法,如果添加失败则重试3次,3次后还是失败,则换节点执行,如果所有节点都不能执行,则记录为执行失败,执行成功后更新任务配置表的最后执行时间。

  • 任务执行:开启一个无界队列线程池,将任务放入执行,将主节点传来的偏移量直接传递给任务,让任务自己去数据库查询对应偏移量的数据。

Cron解析工具类

方案一:使用spring自带的

这个类在spring中已经被标记为弃用了

    @Test
    void springParseCron() {
        String cronExpression = "0 0 12 * * ?"; // 每天中午12点触发
        CronSequenceGenerator generator = new CronSequenceGenerator(cronExpression);

        // 获取下一次任务执行时间
        Date nextRunTime = generator.next(new Date());
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN);
        System.out.println("Next run time: " + simpleDateFormat.format(nextRunTime));
    }

方案二:org.quartz-scheduler

        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
        </dependency>

    public static void main(String[] args) throws ParseException {
        String cronExpression = "0 0 12 * * ?"; // 每天中午12点触发
        CronExpression expression = new CronExpression(cronExpression);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN);
        Date nextRunTime = expression.getNextValidTimeAfter(new Date());
        System.out.println("Next run time: " + simpleDateFormat.format(nextRunTime));
    }

获取系统资源工具类

        OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
        System.out.println("cpu使用率:" + osBean.getSystemLoadAverage());

或者

OshiUtil.getMemory().getTotal()
OshiUtil.getMemory().getAvailable()

int cpuNum = OshiUtil.getCpuInfo(100).getCpuNum();
double cpuUsed = OshiUtil.getCpuInfo(2000).getUsed();

Spring手动定时任务管理

     /**
     * 任务调度池
     * @return
     */
    @Bean(name = "jobSchedulerThread")
    public ThreadPoolTaskScheduler jobSchedulerThread() {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        //线程池大小
        threadPoolTaskScheduler.setPoolSize(jobSchedulerCorePoolSize);
        //线程名称前缀
        threadPoolTaskScheduler.setThreadNamePrefix("jobSchedule-");
        //等待时长
        threadPoolTaskScheduler.setAwaitTerminationSeconds(60);
        //关闭任务线程时是否等待当前被调度的任务完成
        threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        return threadPoolTaskScheduler;
    }

		@Autowired
    @Qualifier("jobSchedulerThread")
    // 任务调度线程池
    private ThreadPoolTaskScheduler jobSchedulerThread; 

		/**
     * 启动定时任务调度
     */
    private void startJobSchedule() {
        List<WbJobConfigEntity> jobs = wbJobConfigMapper.selectList(new LambdaQueryWrapper<WbJobConfigEntity>()
                .eq(WbJobConfigEntity::getTriggerStatus, 1)
        );
        for (WbJobConfigEntity job : jobs) {
            addJobSchedule(job);
        }
        log.debug("启动定时任务调度");
    }

    public void addJobSchedule(WbJobConfigEntity job) {
        RLock lock = redissonTemplate.getLock("wb:addJobSchedule:" + job.getJobHandler());
        try {
            if (lock.tryLock()) {
                if (futureMap.get(job.getJobHandler()) == null || futureMap.get(job.getJobHandler()).isCancelled()) {
                    JobScheduleConfig bean = SpringUtil.getBean(JobScheduleConfig.class);
                    bean.setJob(job);
                    if (job.getScheduleType() == 1) {
                        futureMap.put(job.getJobHandler(), jobSchedulerThread.scheduleAtFixedRate(bean, 1000L * Integer.parseInt(job.getScheduleConf())));
                    } else {
                        futureMap.put(job.getJobHandler(), jobSchedulerThread.schedule(bean, new CronTrigger(job.getScheduleConf())));
                    }
                }
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * 停止定时任务调度
     */
    private void stopJobSchedule() {
        List<WbJobConfigEntity> jobs = wbJobConfigMapper.selectList(new LambdaQueryWrapper<WbJobConfigEntity>()
                .eq(WbJobConfigEntity::getTriggerStatus, 1)
        );
        for (WbJobConfigEntity job : jobs) {
            if (futureMap.get(job.getJobHandler()) != null && !futureMap.get(job.getJobHandler()).isCancelled()) {
                futureMap.get(job.getJobHandler()).cancel(true);
                futureMap.remove(job.getJobHandler());
            }
        }
        log.debug("任务停止调度");
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值