⏰ 分布式任务调度:让定时任务不再"单打独斗"!

57 阅读8分钟

副标题:XXL-Job、ElasticJob、SchedulerX三国杀,谁是任务调度之王?👑


🎬 开场:单机定时任务的困境

某电商系统的噩梦 💔:

需求:每天凌晨1点生成销售报表

方案1:使用Spring @Scheduled
@Scheduled(cron = "0 0 1 * * ?")
public void generateReport() {
    // 生成报表
}

问题来了:
├── 部署了3台服务器
├── 每台都执行了一次
├── 生成了3份重复的报表 😱
├── 数据库被写入3次
└── 老板看到3份一样的报表...

方案2:只在一台服务器上部署定时任务
问题:
├── 这台服务器挂了怎么办? 💀
├── 任务就不执行了?
└── 单点故障!

更大的问题:
├── 报表数据量太大(1亿条)
├── 单台机器处理要10小时
├── 第二天上午10点还没处理完
└── 老板又要骂人了... 😭

这就是我们需要分布式任务调度的原因!


📚 什么是分布式任务调度?

核心概念

分布式任务调度系统需要解决的问题

1. 任务不重复执行
   └─ 多个节点只有一个执行

2. 高可用
   └─ 某个节点挂了,其他节点接管

3. 任务分片
   └─ 大任务拆分给多个节点并行处理

4. 弹性扩缩容
   └─ 动态增减节点

5. 失败重试
   └─ 任务失败自动重试

6. 监控告警
   └─ 任务执行情况可视化

生活比喻 🏭

工厂流水线

单机定时任务(小作坊):
├── 1个工人
├── 做所有的事
├── 累死累活
└── 效率低下

分布式任务调度(现代工厂):
├── 多个工人(多个节点)
├── 任务分配(调度中心)
├── 流水线作业(任务分片)
├── 自动化(故障转移)
└── 高效协作

🥊 三大框架对比

总览对比表

特性XXL-JobElasticJobSchedulerX
开发团队许雪里当当网阿里云
开源情况开源开源商业化
上手难度⭐⭐ 简单⭐⭐⭐ 中等⭐⭐⭐⭐ 复杂
社区活跃度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
分片支持
故障转移
动态扩容
可视化管理⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
依赖MySQLZooKeeper阿里云
适用场景中小型项目 ⭐复杂调度阿里云用户

1️⃣ XXL-Job:国产之光 🌟

架构设计

           ┌─────────────────┐
           │  调度中心        │
           │  (Admin)        │
           │  - Web管理界面  │
           │  - 任务调度     │
           │  - 监控告警     │
           └────────┬────────┘
                    │
         ┌──────────┼──────────┐
         │          │          │
    ┌────▼───┐ ┌───▼────┐ ┌───▼────┐
    │执行器1 │ │执行器2 │ │执行器3 │
    │(APP1) │ │(APP2) │ │(APP3) │
    └────────┘ └────────┘ └────────┘
         │          │          │
         └──────────┴──────────┘
              心跳注册

核心特性

1. 任务路由策略

XXL-Job支持9种路由策略:

1. FIRST(第一个)
   └─ 固定选择第一个执行器

2. LAST(最后一个)
   └─ 固定选择最后一个执行器

3. ROUND(轮询)
   └─ 依次轮询所有执行器
   └─ 任务1 → 节点1
   └─ 任务2 → 节点2
   └─ 任务3 → 节点3
   └─ 任务4 → 节点1 (循环)

4. RANDOM(随机)
   └─ 随机选择一个执行器

5. CONSISTENT_HASH(一致性哈希)
   └─ 相同参数的任务总是路由到同一节点

6. LFU(最不经常使用)
   └─ 选择使用频率最低的节点

7. LRU(最近最少使用)
   └─ 选择最久未使用的节点

8. FAILOVER(故障转移)
   └─ 按顺序依次尝试,直到成功

9. BUSYOVER(忙碌转移)
   └─ 如果节点忙,则转移到其他节点

2. 分片广播

/**
 * 分片广播任务
 * 
 * 场景:处理1000万条订单数据
 * 部署了10个执行器节点
 * 每个节点处理100万条
 */
@XxlJob("orderProcessTask")
public void processOrders() {
    // 获取分片参数
    int shardIndex = XxlJobHelper.getShardIndex();  // 当前分片序号(0-9)
    int shardTotal = XxlJobHelper.getShardTotal();  // 总分片数(10)
    
    log.info("分片任务开始: {}/{}", shardIndex, shardTotal);
    
    // 根据分片处理数据
    // 节点0:处理ID % 10 == 0的订单
    // 节点1:处理ID % 10 == 1的订单
    // ...
    
    List<Order> orders = orderMapper.selectBySharding(shardIndex, shardTotal);
    
    for (Order order : orders) {
        processOrder(order);
    }
    
    log.info("分片任务完成: {}/{}", shardIndex, shardTotal);
}

SQL查询分片数据

-- 分片查询(假设有10个分片,当前是第3片)
SELECT * FROM orders 
WHERE id % 10 = 3
LIMIT 100000;

-- 或者按范围分片
SELECT * FROM orders 
WHERE id >= 3000000 AND id < 4000000;

3. 完整代码示例

1. 引入依赖

<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.4.0</version>
</dependency>

2. 配置执行器

@Configuration
public class XxlJobConfig {
    
    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;
    
    @Value("${xxl.job.executor.appname}")
    private String appname;
    
    @Value("${xxl.job.executor.port}")
    private int port;
    
    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
        executor.setAdminAddresses(adminAddresses);
        executor.setAppname(appname);
        executor.setPort(port);
        executor.setLogPath("/data/applogs/xxl-job");
        executor.setLogRetentionDays(30);
        return executor;
    }
}

配置文件

xxl:
  job:
    admin:
      addresses: http://127.0.0.1:8080/xxl-job-admin
    executor:
      appname: order-executor
      port: 9999
      logpath: /data/applogs/xxl-job
      logretentiondays: 30

3. 编写任务

@Component
public class OrderJobHandler {
    
    /**
     * 简单任务
     */
    @XxlJob("simpleTask")
    public void simpleTask() {
        log.info("简单任务执行");
        // 业务逻辑
    }
    
    /**
     * 分片任务
     */
    @XxlJob("shardingTask")
    public void shardingTask() {
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();
        
        log.info("分片任务执行: {}/{}", shardIndex, shardTotal);
        
        // 根据分片处理业务
        processSharding(shardIndex, shardTotal);
    }
    
    /**
     * 带参数的任务
     */
    @XxlJob("paramTask")
    public void paramTask() {
        String param = XxlJobHelper.getJobParam();
        log.info("任务参数: {}", param);
        
        // 使用参数处理业务
        processWithParam(param);
    }
    
    /**
     * 失败重试任务
     */
    @XxlJob("retryTask")
    public void retryTask() {
        try {
            // 可能失败的业务
            dangerousOperation();
            
            // 成功
            XxlJobHelper.handleSuccess("任务执行成功");
            
        } catch (Exception e) {
            log.error("任务执行失败", e);
            
            // 失败,会自动重试
            XxlJobHelper.handleFail("任务执行失败: " + e.getMessage());
        }
    }
}

XXL-Job的优缺点

优点 ✅:

  • 上手超级简单
  • UI界面美观友好
  • 文档完善,中文文档
  • 社区活跃
  • 无需ZooKeeper等中间件

缺点 ❌:

  • 依赖MySQL数据库
  • 调度中心是单点(需要部署集群)
  • 功能相对简单

2️⃣ ElasticJob:功能强大 💪

架构设计

    ┌──────────────────────────┐
    │     ZooKeeper            │
    │   (协调与注册中心)       │
    └────────┬─────────────────┘
             │
      ┌──────┼──────┐
      │      │      │
  ┌───▼──┐┌──▼──┐┌─▼───┐
  │App1  ││App2 ││App3 │
  │任务1 ││任务2││任务3│
  └──────┘└─────┘└─────┘

核心特性

1. 两种任务类型

Simple简单任务

public class MySimpleJob implements SimpleJob {
    
    @Override
    public void execute(ShardingContext context) {
        // 获取分片参数
        int shardingItem = context.getShardingItem();
        String shardingParameter = context.getShardingParameter();
        
        log.info("分片执行: item={}, param={}", shardingItem, shardingParameter);
        
        // 业务逻辑
        processBusiness(shardingItem, shardingParameter);
    }
}

Dataflow数据流任务

public class MyDataflowJob implements DataflowJob<Order> {
    
    /**
     * 获取待处理数据
     */
    @Override
    public List<Order> fetchData(ShardingContext context) {
        // 从数据库获取待处理订单
        return orderMapper.selectPending(
            context.getShardingItem(),
            context.getShardingTotalCount()
        );
    }
    
    /**
     * 处理数据
     */
    @Override
    public void processData(ShardingContext context, List<Order> data) {
        for (Order order : data) {
            // 处理订单
            processOrder(order);
        }
    }
}

2. 分片策略

/**
 * 平均分片策略
 */
public class AverageAllocationJobShardingStrategy implements JobShardingStrategy {
    
    @Override
    public Map<JobInstance, List<Integer>> sharding(
            List<JobInstance> jobInstances, 
            String jobName, 
            int shardingTotalCount) {
        
        Map<JobInstance, List<Integer>> result = new HashMap<>();
        
        // 假设有3个节点,10个分片
        // 节点1: [0, 1, 2, 3]
        // 节点2: [4, 5, 6]
        // 节点3: [7, 8, 9]
        
        int itemsPerNode = shardingTotalCount / jobInstances.size();
        int remainder = shardingTotalCount % jobInstances.size();
        
        int offset = 0;
        for (int i = 0; i < jobInstances.size(); i++) {
            JobInstance instance = jobInstances.get(i);
            List<Integer> shardingItems = new ArrayList<>();
            
            int count = itemsPerNode + (i < remainder ? 1 : 0);
            for (int j = 0; j < count; j++) {
                shardingItems.add(offset++);
            }
            
            result.put(instance, shardingItems);
        }
        
        return result;
    }
}

3. 完整配置

@Configuration
public class ElasticJobConfig {
    
    @Autowired
    private ZookeeperRegistryCenter regCenter;
    
    /**
     * 配置ZooKeeper注册中心
     */
    @Bean
    public ZookeeperConfiguration zkConfig() {
        return new ZookeeperConfiguration(
            "localhost:2181",
            "elastic-job-demo"
        );
    }
    
    @Bean(initMethod = "init")
    public ZookeeperRegistryCenter regCenter(ZookeeperConfiguration config) {
        return new ZookeeperRegistryCenter(config);
    }
    
    /**
     * 配置简单任务
     */
    @Bean
    public JobScheduler simpleJobScheduler(
            SimpleJob simpleJob,
            ZookeeperRegistryCenter regCenter) {
        
        // 任务配置
        JobCoreConfiguration coreConfig = JobCoreConfiguration
            .newBuilder("mySimpleJob", "0 0 2 * * ?", 10)  // cron, 10个分片
            .shardingItemParameters("0=A,1=B,2=C,3=D,4=E,5=F,6=G,7=H,8=I,9=J")
            .build();
        
        // 简单任务配置
        SimpleJobConfiguration jobConfig = new SimpleJobConfiguration(
            coreConfig,
            simpleJob.getClass().getCanonicalName()
        );
        
        // Lite配置
        LiteJobConfiguration liteConfig = LiteJobConfiguration
            .newBuilder(jobConfig)
            .overwrite(true)
            .build();
        
        // 创建调度器
        return new JobScheduler(regCenter, liteConfig);
    }
}

ElasticJob的优缺点

优点 ✅:

  • 功能强大
  • 支持多种任务类型
  • 分片策略灵活
  • 可以动态调整分片

缺点 ❌:

  • 依赖ZooKeeper
  • 配置复杂
  • 学习成本高
  • UI界面较弱

3️⃣ SchedulerX:阿里云方案 ☁️

核心特性

1. 分布式MapReduce

@Component
public class MapReduceJobProcessor extends MapJobProcessor {
    
    /**
     * Map阶段:拆分任务
     */
    @Override
    public ProcessResult process(TaskContext context) {
        // 获取总数据量
        long totalCount = orderMapper.count();
        
        // 拆分成100个子任务
        int pageSize = (int) (totalCount / 100);
        List<SubTask> subTasks = new ArrayList<>();
        
        for (int i = 0; i < 100; i++) {
            SubTask subTask = new SubTask();
            subTask.setTaskName("process_page_" + i);
            subTask.setContent(String.valueOf(i * pageSize));
            subTasks.add(subTask);
        }
        
        // 分发子任务
        return new ProcessResult(true, subTasks);
    }
    
    /**
     * Reduce阶段:汇总结果
     */
    @Override
    public ProcessResult reduce(TaskContext context) {
        // 汇总所有子任务的结果
        List<SubTaskResult> results = context.getSubTaskResults();
        
        int totalProcessed = 0;
        for (SubTaskResult result : results) {
            totalProcessed += Integer.parseInt(result.getResult());
        }
        
        log.info("总共处理了{}条数据", totalProcessed);
        
        return new ProcessResult(true);
    }
}

2. 工作流调度

支持DAG工作流:

任务A (数据提取)
  ↓
任务B (数据清洗)
  ↓
  ├─→ 任务C (统计1)
  ├─→ 任务D (统计2)
  └─→ 任务E (统计3)
      ↓
    任务F (汇总)

SchedulerX的优缺点

优点 ✅:

  • 功能最强大
  • 支持MapReduce
  • 支持工作流
  • 可视化强大
  • 阿里云深度集成

缺点 ❌:

  • 商业化产品(收费)
  • 必须使用阿里云
  • 文档相对较少

🎯 选型建议

决策树 🌲

开始选择任务调度框架
 │
 ├─ 使用阿里云?
 │   └─ 是 → SchedulerX ⭐⭐⭐
 │
 ├─ 需要复杂的任务依赖?
 │   └─ 是 → ElasticJob ⭐⭐⭐
 │
 ├─ 追求简单易用?
 │   └─ 是 → XXL-Job ⭐⭐⭐⭐⭐
 │
 ├─ 已有ZooKeeper?
 │   └─ 是 → ElasticJob ⭐⭐⭐⭐
 │
 └─ 中小型项目?
     └─ 是 → XXL-Job ⭐⭐⭐⭐⭐

场景推荐

场景推荐理由
中小型项目XXL-Job简单、够用、社区好
复杂调度ElasticJob功能强大、灵活
阿里云用户SchedulerX深度集成、功能最强
大数据处理ElasticJob/SchedulerX支持分片、MapReduce

💡 最佳实践

1. 幂等性设计

@XxlJob("orderTask")
public void processOrders() {
    String jobId = XxlJobHelper.getJobId();
    String redisKey = "job:lock:" + jobId;
    
    // 使用分布式锁防止重复执行
    RLock lock = redisson.getLock(redisKey);
    
    try {
        if (lock.tryLock(0, 30, TimeUnit.MINUTES)) {
            // 执行任务
            doProcess();
        } else {
            log.warn("任务正在执行中,跳过");
        }
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

2. 监控告警

@Component
public class JobMonitor {
    
    @Autowired
    private AlertService alertService;
    
    /**
     * 监控任务执行
     */
    public void monitorJobExecution(String jobName, long duration, boolean success) {
        // 1. 记录指标
        meterRegistry.timer("job.execution.time")
            .tag("job", jobName)
            .tag("success", String.valueOf(success))
            .record(duration, TimeUnit.MILLISECONDS);
        
        // 2. 检查是否超时
        if (duration > 60000) {  // 超过1分钟
            alertService.send(
                "任务执行超时",
                jobName + " 执行时间: " + duration + "ms"
            );
        }
        
        // 3. 检查是否失败
        if (!success) {
            alertService.send(
                "任务执行失败",
                jobName + " 执行失败"
            );
        }
    }
}

🎉 总结

核心要点 ✨

  1. XXL-Job

    • 简单易用
    • 社区活跃
    • 适合大部分场景
  2. ElasticJob

    • 功能强大
    • 灵活可扩展
    • 适合复杂调度
  3. SchedulerX

    • 功能最全
    • 阿里云集成
    • 企业级选择

记忆口诀 📝

任务调度三选择,
场景不同各有别

XXL-Job最简单,
中小项目首选它
UI美观文档全,
社区活跃问题少

ElasticJob功能强,
复杂调度不用慌
分片策略很灵活,
ZooKeeper来帮忙

SchedulerX云上跑,
阿里出品质量高
MapReduce工作流,
企业级别选择好!

愿你的定时任务稳定运行,准时执行! ⏰✨