副标题: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-Job | ElasticJob | SchedulerX |
|---|---|---|---|
| 开发团队 | 许雪里 | 当当网 | 阿里云 |
| 开源情况 | 开源 | 开源 | 商业化 |
| 上手难度 | ⭐⭐ 简单 | ⭐⭐⭐ 中等 | ⭐⭐⭐⭐ 复杂 |
| 社区活跃度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 分片支持 | ✅ | ✅ | ✅ |
| 故障转移 | ✅ | ✅ | ✅ |
| 动态扩容 | ✅ | ✅ | ✅ |
| 可视化管理 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 依赖 | MySQL | ZooKeeper | 阿里云 |
| 适用场景 | 中小型项目 ⭐ | 复杂调度 | 阿里云用户 |
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 + " 执行失败"
);
}
}
}
🎉 总结
核心要点 ✨
-
XXL-Job:
- 简单易用
- 社区活跃
- 适合大部分场景
-
ElasticJob:
- 功能强大
- 灵活可扩展
- 适合复杂调度
-
SchedulerX:
- 功能最全
- 阿里云集成
- 企业级选择
记忆口诀 📝
任务调度三选择,
场景不同各有别。
XXL-Job最简单,
中小项目首选它。
UI美观文档全,
社区活跃问题少。
ElasticJob功能强,
复杂调度不用慌。
分片策略很灵活,
ZooKeeper来帮忙。
SchedulerX云上跑,
阿里出品质量高。
MapReduce工作流,
企业级别选择好!
愿你的定时任务稳定运行,准时执行! ⏰✨