一、为什么需要高可用的定时任务系统
想象一下,你负责维护一个电商平台的促销活动系统。每天凌晨需要执行几十个定时任务:清理缓存、更新库存、发放优惠券...突然某天服务器宕机,所有任务都没执行。结果用户看到的还是昨天的商品价格,库存数据也没刷新,这简直就是一场灾难!
定时任务调度系统就像人体的生物钟,一旦失灵,整个系统就会乱套。高可用设计就是要确保这个"生物钟"在任何情况下都能正常工作,即使遇到服务器崩溃、网络抖动、甚至机房断电。
二、定时任务的基础技术选型
在Java生态中,我们有几个主流选择:
- Timer:JDK自带的古董级工具,单线程执行,一个任务卡住会影响所有任务
- ScheduledThreadPoolExecutor:改进版的线程池方案,但缺乏分布式支持
- Quartz:老牌框架,支持集群但配置复杂
- XXL-JOB/Elastic-Job:新兴的分布式调度系统
这里我们选择Quartz作为示例,因为它:
- 久经考验(GitHub 4.5k stars)
- 支持任务持久化
- 提供故障转移机制
- 可以与Spring无缝集成
// Quartz基础配置示例(技术栈:Spring Boot + Quartz)
@Configuration
public class QuartzConfig {
@Bean
public SchedulerFactoryBean schedulerFactory(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
// 使用数据库存储任务状态
factory.setDataSource(dataSource);
factory.setOverwriteExistingJobs(true);
// 集群配置
Properties props = new Properties();
props.put("org.quartz.scheduler.instanceId", "AUTO");
props.put("org.quartz.jobStore.isClustered", "true");
factory.setQuartzProperties(props);
return factory;
}
}
三、高可用设计的五个关键点
3.1 持久化存储
内存存储任务状态是定时任务的"阿喀琉斯之踵"。我们使用MySQL存储任务信息:
-- Quartz需要的11张核心表(部分示例)
CREATE TABLE QRTZ_JOB_DETAILS (
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
3.2 心跳检测与故障转移
// 自定义健康检查任务(每30秒执行)
@DisallowConcurrentExecution
public class HealthCheckJob implements Job {
private static final Logger logger = LoggerFactory.getLogger(HealthCheckJob.class);
@Override
public void execute(JobExecutionContext context) {
// 检查数据库连接
checkDataSource();
// 检查其他节点状态
checkClusterNodes();
logger.info("系统健康检查通过");
}
private void checkClusterNodes() {
// 实现细节省略...
}
}
3.3 幂等性设计
定时任务最怕重复执行。我们通过Redis分布式锁实现:
public class CouponDistributionJob implements Job {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public void execute(JobExecutionContext context) {
String lockKey = "job:coupon:" + LocalDate.now();
// 获取分布式锁(有效期5分钟)
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 5, TimeUnit.MINUTES);
if (locked != null && locked) {
try {
realDistributionLogic();
} finally {
redisTemplate.delete(lockKey);
}
}
}
}
3.4 动态调整能力
通过REST API实现运行时控制:
@RestController
@RequestMapping("/api/jobs")
public class JobController {
@Autowired
private Scheduler scheduler;
@PostMapping("/pause")
public void pauseJob(@RequestParam String jobName) throws SchedulerException {
JobKey jobKey = new JobKey(jobName);
if (scheduler.checkExists(jobKey)) {
scheduler.pauseJob(jobKey);
}
}
// 其他操作方法...
}
3.5 监控与告警
集成Prometheus监控指标:
public class JobMetricsListener implements JobListener {
private final Counter failedJobs = Counter.build()
.name("quartz_jobs_failed_total")
.help("失败的任务总数")
.register();
@Override
public void jobWasExecuted(JobExecutionContext context,
JobExecutionException jobException) {
if (jobException != null) {
failedJobs.inc();
// 发送告警通知
alertService.sendAlert(context.getJobDetail().getKey(), jobException);
}
}
}
四、进阶优化方案
4.1 分片任务处理
大数据量任务可以采用分片策略:
public class BigDataProcessJob implements InterruptableJob {
private volatile boolean interrupted = false;
@Override
public void execute(JobExecutionContext context) {
JobDataMap data = context.getMergedJobDataMap();
int shardIndex = data.getInt("shardIndex");
int totalShards = data.getInt("totalShards");
List<Long> ids = fetchDataIds(shardIndex, totalShards);
for (Long id : ids) {
if (interrupted) {
break; // 支持任务中断
}
processSingleItem(id);
}
}
@Override
public void interrupt() {
interrupted = true;
}
}
4.2 弹性调度策略
根据系统负载动态调整:
public class AdaptiveScheduler {
public void adjustScheduleBasedOnLoad() {
double load = getSystemLoad();
if (load > 0.8) {
// 负载高时延迟非关键任务
delayNonCriticalJobs();
} else {
// 负载低时提前执行可预计算任务
advancePrecomputeJobs();
}
}
}
五、避坑指南
- 时间同步问题:所有服务器必须使用NTP同步时间
- 数据库连接池:配置合理的连接数,推荐HikariCP
- 事务隔离:任务执行与业务逻辑使用不同事务管理器
- 日志分离:任务执行日志建议单独存储
- 版本兼容:Quartz与Spring Boot版本匹配很重要
六、总结
设计高可用定时任务系统就像给系统安装了一个"永不停歇的心脏",需要从存储、执行、监控多个维度构建安全网。通过Quartz的持久化机制保障任务不丢失,借助分布式锁实现幂等控制,配合监控系统实时掌握运行状态。记住,好的调度系统应该像优秀的交通指挥系统——即使部分道路拥堵或封闭,整个城市交通依然能有序运转。
最后给出一个完整的电商场景示例:
// 商品库存自动补货任务(技术栈:Spring Boot + Quartz + Redis)
public class InventoryReplenishmentJob implements Job {
@Autowired
private InventoryService inventoryService;
@Autowired
private RedisLockHelper lockHelper;
@Override
public void execute(JobExecutionContext context) {
String sku = context.getMergedJobDataMap().getString("sku");
String lockKey = "inventory:replenish:" + sku;
try {
if (lockHelper.tryLock(lockKey, 10, TimeUnit.MINUTES)) {
int current = inventoryService.getStock(sku);
if (current < 100) { // 库存阈值
inventoryService.replenish(sku, 500);
}
}
} finally {
lockHelper.unlock(lockKey);
}
}
}
评论