任务调度

  • 任务场景
      在支付宝中如果你在蚂蚁森林里面种植了一颗树苗它会根据用户每天使用支付宝的各种场景地铁出行线下支付等来生成相应的能量每天早上支付宝会通知每个用户收取能量以便他们能够种植更多的树苗
  • 单机场景

    • Spring Scheduler
      import org.springframework.scheduling.annotation.Scheduled;
      import org.springframework.stereotype.Component;
      
      @Component
      @EnableScheduling
      public class EnergyCollectTask {
        @Scheduled(cron = "0 0 0 * * *")
        public void collectEnergy() {
            try {
                // Execute the energy collection logic
                System.out.println("Collecting energy...");
            } catch (Exception e) {
                // Handle exception logic
                System.out.println("Failed to collect energy, reason: " + e.getMessage());
                // Handle exception situation
                sendAlertMessage(e);
            }
        }
        
       private void sendAlertMessage(Exception e) {
            // Handle exception situation
            System.out.println("Sending alert message: " + e.getMessage());
        }
      }
    
    • spring scheduler 几种使用方式

      • @Scheduled(fixedDelay = 3 * 1000)
        • nextExecutionTime= lastCompletionTime + fixedDelay
        • 隔3s执行一次
      • @Scheduled(cron = “*/3 * * * * *”)
        • 每3s执行一次
      • @Scheduled(fixedRate = 3 * 1000)
        • nextExecutionTime= firstExecutionTime + fixedRate * times
  • spring scheduler 实现

  • 如果同一时刻有大量任务需要处理DelayWorkQueue是根据PriorityQueue实现的底层数据结构是堆,从而删除和插入的都是O(logN)为了性能考虑可以使用时间轮算法来获取任务它的插入和删除都是O(1)

      时间轮可以理解为一种环形结构像钟表一样被分为多个 slot 槽位每个 slot 代表一个时间段每个 slot 中可以存放多个任务使用的是链表结构保存该时间段到期的所有任务时间轮通过一个时针随着时间一个个 slot 转动并执行 slot 中的所有到期任务
      时间轮定时器最大的优势就是任务的新增和取消都是 O(1) 时间复杂度而且只需要一个线程就可以驱动时间轮进行工作

  随着微服务化架构的逐步演进单体架构逐渐演变为分布式微服务架构在此的背景下很多原先的单点式任务调度平台已经不能满足业务系统的需求系统可以部署多个应用节点而每个应用节点都可以当作任务执行器如果一个任务执行器宕机任务可以转移到其他存活的执行器上去执行同时对于数据量大且处理耗时的任务可以利用好多个节点的资源实现任务在多个节点分片处理

  • 在分布式场景下需要确保任务能够被分配到可用的机器上并得到调度执行从而就需要考虑分布式场景下所面临的问题

    • 高可用性
      • 在分布式场景下集群中的某些节点可能会宕机需要确保任务被正常执行
    • 调度策略
      • 轮询调度异常转移分片处理
    • 任务的注册和发现
    • 任务的持久化存储
      • 避免在节点宕机重启等情况下能够重新加载已有的任务信息保证任务不丢失
    • 分布式锁
      • 多个节点同时执行同一个任务时需要保证同一时间只有一个节点在执行该任务
    • 可扩展性
      • 通过添加新的节点来提高任务执行的并发度从而提高任务的执行效率
    • 监控和报警
      • 通过监控和报警机制来查看任务是否正常执行
  • Quartz

    • 通过数据库的方式来保证同一时间只有一个节点获取任务触发器
    • 节点轮询
      • 每个节点的scheduler模块都会去轮询是否有到期的job每个节点会去抢占锁来保证同一时间只有一个节点执行任务
    • 节点异常处理
      • 每个节点都会及时更新自己的状态到qrtz_scheduler_state当节点出现宕机时其他节点会及时更新这个节点为DEAD同时将未执行的任务根据策略重新执行
    • 任务执行处理
      • 任务超时未执行或者执行报错都会根据执行策略来进行调度或者重试
    • 动态管理任务
      • 通过API的方式来查询和修改任务信息
  • xxl-job

    • quartz通过抢占式获取DB锁来确保只会存在一个节点来处理任务可能会导致节点负载悬殊同时调度逻辑和执行任务器耦合在一起可能会导致性能影响
    • xxl-job在此基础上做了一些拓展和优化实现调度器和执行器的分离支持任务的分片处理并且考虑各节点的负载均衡

scheduler线程定时扫描数据库中的任务同时利用数据库锁的方式避免同一任务被重复调度

  • 配置任务

  
路由策略

  • FIRST(第一个)

    • 固定选择第一个机器
  • LAST(最后一个)

    • 固定选择最后一个机器
  • ROUND(轮询)

  • RANDOM(随机)

    • 随机选择在线的机器
  • CONSISTENT_HASH(一致性HASH)

    • 每个任务按照Hash算法固定选择某一台机器且所有任务均匀散列在不同机器上
  • LEAST_FREQUENTLY_USED(最不经常使用)

    • 使用频率最低的机器优先被选举
  • LEAST_RECENTLY_USED(最近最久未使用)

    • 最久未使用的机器优先被选举
  • FAILOVER(故障转移)

    • 按照顺序依次进行心跳检测第一个心跳检测成功的机器选定为目标执行器并发起调度
  • BUSYOVER(忙碌转移)

    • 按照顺序依次进行空闲检测第一个空闲检测成功的机器选定为目标执行器并发起调度
  • SHARDING_BROADCAST(分片广播)广播触发对应集群中所有机器执行一次任务同时系统自动传递分片参数可根据分片参数开发分片任务

  • 阻塞处理策略

    • 任务的一次运行还没有结束下一次调度的时间又到了
    • 单机串行默认调度请求进入单机执行器后调度请求进入FIFO队列并以串行方式运行
    • 丢弃后续调度调度请求进入单机执行器后发现执行器存在运行的调度任务本次请求将会被丢弃并标记为失败
    • 覆盖之前调度调度请求进入单机执行器后发现执行器存在运行的调度任务将会终止运行中的调度任务并清空队列然后运行本地调度任务
  • 总结

    • 从单机式任务调度到分布式多节点部署其中都需要考虑到任务触发的时间触发策略重试机制等
    • 在分布式下需要考虑到高可用避免节点故障而导致任务失败合理利用集群资源进行分片处理增加任务的处理效率同时可以进行动态扩容但是也需要考虑到分布式场景下锁的竞争目前有数据库的实现也有基于rediszookeeper分布式锁的实现其目的都是为了解决多节点调度竞争问题