动态定时任务调度系统技术实现文档
一、系统概述
基于 Spring ThreadPoolTaskScheduler + CronTrigger 实现的动态定时任务调度系统,支持通过数据库配置动态管理定时任务的启停、暂停、恢复和立即执行。
二、核心架构
2.1 组件结构
skill-manage/
├── scheduler/
│ ├── DynamicTaskScheduler.java # 调度管理器(核心)
│ └── TaskStartupLoader.java # 应用启动加载器
├── task/
│ └── BrowseLogSimulateExecutor.java # 任务执行器
└── service/
└── BrowseLogSimulateConfigService.java # 业务服务层
2.2 数据表设计
表名 : t_browse_log_simulate_config
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键ID |
| config_name | varchar(100) | 配置名称 |
| cron_expression | varchar(50) | Cron表达式 |
| task_status | tinyint | 任务状态(0=停止,1=运行中,2=暂停) |
| enabled | tinyint | 是否启用(0=禁用,1=启用) |
| participation_rate_range | varchar(50) | 参与率范围(JSON格式) |
| logs_per_user_range | varchar(50) | 浏览次数范围(JSON格式) |
| work_time_range | varchar(50) | 工作时间范围(JSON格式) |
| last_execute_time | datetime | 最后执行时间 |
| next_execute_time | datetime | 下次执行时间 |
| execute_count | bigint | 累计执行次数 |
| total_generated_logs | bigint | 累计生成记录数 |
三、核心技术实现
3.1 调度管理器 - DynamicTaskScheduler
3.1.1 初始化配置
java
@Component
public class DynamicTaskScheduler {
private static final int CORE_POOL_SIZE = 10;
private ThreadPoolTaskScheduler taskScheduler;
private final ConcurrentHashMap<Long, ScheduledFuture<?>> taskMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, AtomicBoolean> executingTasks = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(CORE_POOL_SIZE);
taskScheduler.setThreadNamePrefix("dynamic-task-scheduler-");
taskScheduler.setAwaitTerminationSeconds(60); // 优雅关闭等待60秒
taskScheduler.setWaitForTasksToCompleteOnShutdown(true); // 等待任务完成
taskScheduler.setErrorHandler(t -> log.error("任务执行异常", t));
taskScheduler.initialize();
}
}
关键配置说明:
setPoolSize(10): 固定线程池大小setAwaitTerminationSeconds(60): 关闭时最多等待60秒setWaitForTasksToCompleteOnShutdown(true): 确保任务执行完毕再关闭ConcurrentHashMap: 线程安全的任务映射表AtomicBoolean: 防止任务并发执行
3.1.2 注册定时任务
java
public void scheduleTask(Long configId, String cronExpression, Runnable runnable) {
// 1. 取消已存在的任务
cancelTask(configId);
// 2. 校验 Cron 表达式
validateCronExpression(cronExpression);
// 3. 初始化执行状态
executingTasks.put(configId, new AtomicBoolean(false));
// 4. 创建 CronTrigger
CronTrigger trigger = new CronTrigger(cronExpression);
// 5. 注册任务(带并发控制)
ScheduledFuture<?> future = taskScheduler.schedule(() -> {
AtomicBoolean isExecuting = executingTasks.get(configId);
// 防止并发执行
if (isExecuting != null && isExecuting.get()) {
log.warn("任务正在执行中,跳过本次调度,configId: {}", configId);
return;
}
long startTime = System.currentTimeMillis();
try {
if (isExecuting != null) {
isExecuting.set(true); // 标记为执行中
}
runnable.run(); // 执行任务
long costTime = System.currentTimeMillis() - startTime;
log.debug("定时任务执行完成,configId: {}, 耗时: {}ms", configId, costTime);
} catch (Exception e) {
long costTime = System.currentTimeMillis() - startTime;
log.error("定时任务执行异常,configId: {}, 耗时: {}ms", configId, costTime, e);
} finally {
if (isExecuting != null) {
isExecuting.set(false); // 标记为执行完成
}
}
}, trigger);
taskMap.put(configId, future);
// 6. 计算并记录下次执行时间
Date nextExecuteTime = getNextExecutionTime(cronExpression);
log.info("注册定时任务成功,configId: {}, cron: {}, 下次执行: {}",
configId, cronExpression, nextExecuteTime);
}
核心特性:
- 真正的 Cron 调度 : 使用
CronTrigger解析 Cron 表达式 - 并发控制 : 使用
AtomicBoolean防止同一任务并发执行 - 执行监控: 记录每次执行耗时
- 异常隔离: 单个任务异常不影响其他任务
3.1.3 Cron 表达式校验
java
private void validateCronExpression(String cronExpression) {
try {
new CronSequenceGenerator(cronExpression);
} catch (Exception e) {
throw new IllegalArgumentException("无效的 Cron 表达式: " + cronExpression);
}
}
3.1.4 计算下次执行时间
java
public Date getNextExecutionTime(String cronExpression) {
try {
CronTrigger trigger = new CronTrigger(cronExpression);
// 必须传入非 null 的 TriggerContext
return trigger.nextExecutionTime(new TriggerContext() {
@Override
public Date lastScheduledExecutionTime() {
return new Date();
}
@Override
public Date lastActualExecutionTime() {
return new Date();
}
@Override
public Date lastCompletionTime() {
return new Date();
}
});
} catch (Exception e) {
log.error("计算下次执行时间失败,cron: {}", cronExpression, e);
return null;
}
}
注意 : CronTrigger.nextExecutionTime() 不能传 null,否则抛出 NPE。
3.1.5 任务控制方法
java
// 取消任务
public void cancelTask(Long configId) {
ScheduledFuture<?> future = taskMap.remove(configId);
if (future != null && !future.isCancelled()) {
future.cancel(false);
}
executingTasks.remove(configId); // 清理执行状态
}
// 暂停任务(等同于取消)
public void pauseTask(Long configId) {
cancelTask(configId);
}
// 恢复任务(重新注册)
public void resumeTask(Long configId, String cronExpression, Runnable runnable) {
scheduleTask(configId, cronExpression, runnable);
}
// 立即执行一次
public void executeOnce(Long configId, Runnable runnable) {
taskScheduler.submit(() -> {
try {
runnable.run();
} catch (Exception e) {
log.error("立即执行任务异常,configId: {}", configId, e);
}
});
}
// 检查任务是否运行中
public boolean isRunning(Long configId) {
ScheduledFuture<?> future = taskMap.get(configId);
return future != null && !future.isCancelled() && !future.isDone();
}
3.1.6 优雅关闭
java
@PreDestroy
public void destroy() {
if (taskScheduler != null) {
taskScheduler.shutdown();
log.info("动态任务调度器已关闭");
}
}
3.2 应用启动加载器 - TaskStartupLoader
java
@Component
public class TaskStartupLoader implements CommandLineRunner {
@Resource
private BrowseLogSimulateConfigMapper configMapper;
@Resource
private DynamicTaskScheduler taskScheduler;
@Resource
private BrowseLogSimulateExecutor taskExecutor;
@Override
public void run(String... args) {
log.info("========== 开始加载定时任务 ==========");
// 查询所有启用且运行中的任务
List<BrowseLogSimulateConfig> runningConfigs = configMapper.selectList(
new LambdaQueryWrapper<BrowseLogSimulateConfig>()
.eq(BrowseLogSimulateConfig::getEnabled, 1)
.eq(BrowseLogSimulateConfig::getTaskStatus, 1)
);
if (runningConfigs == null || runningConfigs.isEmpty()) {
log.info("没有需要加载的定时任务");
return;
}
log.info("发现 {} 个运行中的定时任务,开始注册...", runningConfigs.size());
int successCount = 0;
int failCount = 0;
for (BrowseLogSimulateConfig config : runningConfigs) {
try {
// 注册到调度器
taskScheduler.scheduleTask(
config.getId(),
config.getCronExpression(),
() -> taskExecutor.execute(config.getId())
);
// 更新下次执行时间
Date nextExecuteTime = taskScheduler.getNextExecutionTime(config.getCronExpression());
if (nextExecuteTime != null) {
configMapper.updateNextExecuteTime(config.getId(), nextExecuteTime);
}
log.info("✓ 任务注册成功 | ID:{} | 名称:{} | Cron:{} | 下次执行:{}",
config.getId(), config.getConfigName(),
config.getCronExpression(), nextExecuteTime);
successCount++;
} catch (Exception e) {
log.error("✗ 任务注册失败 | ID:{} | 名称:{}",
config.getId(), config.getConfigName(), e);
// 更新任务状态为停止
config.setTaskStatus(0);
configMapper.updateById(config);
failCount++;
}
}
log.info("========== 定时任务加载完成 | 总数:{} | 成功:{} | 失败:{} ==========",
runningConfigs.size(), successCount, failCount);
}
}
关键特性:
- 使用
CommandLineRunner在 Spring 容器启动完成后执行 - 自动加载数据库中所有运行中的任务
- 异常隔离:单个任务注册失败不影响其他任务
- 失败任务自动更新状态为停止
3.3 任务执行器 - BrowseLogSimulateExecutor
3.3.1 执行流程
java
public void execute(Long configId) {
long startTime = System.currentTimeMillis();
log.info("开始执行浏览记录模拟任务,configId: {}", configId);
try {
// 1. 加载配置
BrowseLogSimulateConfig config = configMapper.selectById(configId);
// 2. 解析配置参数(JSON格式)
SimulateParams params = parseConfig(config);
// 3. 获取有效用户(根据机构/学科/运营商过滤)
List<Customer> validCustomers = getValidCustomers(config);
// 4. 获取课程资源
List<CommonResource> courseList = getAllCourses();
// 5. 生成浏览记录(并行处理)
List<SysUserBrowseLog> allBrowseLogs = generateBrowseLogs(...);
// 6. 批量插入数据库
insertBatchBrowseLogs(allBrowseLogs, config.getBatchSize());
// 7. 更新统计信息
updateStatistics(configId, allBrowseLogs.size());
// 8. 更新下次执行时间
updateNextExecuteTime(configId, config.getCronExpression());
long costTime = System.currentTimeMillis() - startTime;
log.info("完成 | configId:{} | 浏览记录:{} | 耗时:{}ms",
configId, allBrowseLogs.size(), costTime);
} catch (Exception e) {
log.error("浏览记录模拟任务执行异常,configId: {}", configId, e);
}
}
3.3.2 JSON 参数解析
java
private SimulateParams parseConfig(BrowseLogSimulateConfig config) {
SimulateParams params = new SimulateParams();
// 解析参与率范围(兼容 BigDecimal、String 等类型)
if (StringUtils.isNotEmpty(config.getParticipationRateRange())) {
Map<String, Object> rateMap = JSONUtil.toBean(
config.getParticipationRateRange(), Map.class);
params.setMinParticipationRate(parseDouble(rateMap.get("min"), 0.2));
params.setMaxParticipationRate(parseDouble(rateMap.get("max"), 0.6));
}
// 解析浏览次数范围
if (StringUtils.isNotEmpty(config.getLogsPerUserRange())) {
Map<String, Object> logsMap = JSONUtil.toBean(
config.getLogsPerUserRange(), Map.class);
params.setMinLogsPerUser(parseInt(logsMap.get("min"), 5));
params.setMaxLogsPerUser(parseInt(logsMap.get("max"), 10));
}
return params;
}
// 安全解析 Double(兼容 Number、String、null)
private double parseDouble(Object value, double defaultValue) {
if (value == null) return defaultValue;
if (value instanceof Number) return ((Number) value).doubleValue();
try {
return Double.parseDouble(value.toString());
} catch (NumberFormatException e) {
return defaultValue;
}
}
3.3.3 统计信息更新
java
// Mapper 层 - 使用 SQL 累加(避免并发问题)
@Update("UPDATE t_browse_log_simulate_config SET " +
"last_execute_time = NOW(), " +
"execute_count = execute_count + 1, " +
"total_generated_logs = total_generated_logs + #{generatedCount}, " +
"update_time = NOW() " +
"WHERE id = #{configId}")
void updateStatistics(@Param("configId") Long configId,
@Param("generatedCount") Integer generatedCount);
// 更新下次执行时间
@Update("UPDATE t_browse_log_simulate_config SET " +
"next_execute_time = #{nextExecuteTime}, " +
"update_time = NOW() " +
"WHERE id = #{configId}")
void updateNextExecuteTime(@Param("configId") Long configId,
@Param("nextExecuteTime") Date nextExecuteTime);
四、业务流程
4.1 启动任务流程
Controller.start(id)
↓
Service.startTask(id)
↓
1. 查询配置
2. 校验状态(是否已在运行、是否启用)
3. 调用 Scheduler.scheduleTask()
├─ 校验 Cron 表达式
├─ 创建 CronTrigger
├─ 注册到 ThreadPoolTaskScheduler
└─ 计算下次执行时间
4. 更新数据库 task_status = 1
5. 更新 next_execute_time
4.2 定时触发流程
ThreadPoolTaskScheduler (按 Cron 表达式触发)
↓
DynamicTaskScheduler.schedule() 中的 Runnable
↓
1. 检查并发状态(AtomicBoolean)
├─ 如果正在执行 → 跳过本次
└─ 如果空闲 → 标记为执行中
2. 执行 BrowseLogSimulateExecutor.execute()
├─ 加载配置
├─ 生成浏览记录
├─ 批量插入数据库
├─ 更新统计信息(execute_count + 1)
└─ 更新下次执行时间
3. 标记为执行完成
4.3 应用重启流程
Spring Boot 启动
↓
@PostConstruct - DynamicTaskScheduler.init()
↓ 初始化线程池
CommandLineRunner - TaskStartupLoader.run()
↓
1. 查询 task_status=1 且 enabled=1 的任务
2. 逐个注册到调度器
3. 更新 next_execute_time
4. 输出加载日志(成功/失败统计)
五、关键技术点
5.1 防止任务并发执行
问题: 如果任务执行时间超过调度间隔,可能导致多个实例同时运行。
解决方案 : 使用 AtomicBoolean 标记执行状态
java
AtomicBoolean isExecuting = executingTasks.get(configId);
if (isExecuting != null && isExecuting.get()) {
log.warn("任务正在执行中,跳过本次调度");
return;
}
try {
isExecuting.set(true);
runnable.run();
} finally {
isExecuting.set(false);
}
5.2 JSON 数字类型转换
问题 : Hutool 解析 JSON 时,数字默认是 BigDecimal 类型,直接 cast 会报错。
解决方案 : 先转为 Number,再调用 .doubleValue() 或 .intValue()
java
Map<String, Object> map = JSONUtil.toBean(json, Map.class);
double value = ((Number) map.get("key")).doubleValue();
5.3 CronTrigger 空指针问题
问题 : CronTrigger.nextExecutionTime(null) 会抛出 NPE。
解决方案 : 传入有效的 TriggerContext
java
trigger.nextExecutionTime(new TriggerContext() {
@Override
public Date lastScheduledExecutionTime() {
return new Date();
}
// ... 其他方法
});
5.4 统计字段并发更新
问题: 多线程同时更新计数器可能丢失数据。
解决方案: 使用 SQL 原子操作
sql
UPDATE table SET
execute_count = execute_count + 1,
total_generated_logs = total_generated_logs + #{count}
WHERE id = #{id}
六、监控与日志
6.1 关键日志
// 启动加载
========== 开始加载定时任务 ==========
发现 3 个运行中的定时任务,开始注册...
✓ 任务注册成功 | ID:1 | 名称:默认配置 | Cron:0 */5 * * * ? | 下次执行:2026-05-09 14:00:00
========== 定时任务加载完成 | 总数:3 | 成功:3 | 失败:0 ==========
// 任务执行
开始执行浏览记录模拟任务,configId: 1
完成 | configId:1 | 总用户:1000 | 参与用户:350 | 浏览记录:2100 | 人均:6.00次 | 耗时:15234ms
更新执行统计成功,configId: 1, 本次生成: 2100
更新下次执行时间成功,configId: 1, 下次执行: 2026-05-09 14:05:00
6.2 监控指标
java
// 获取运行中的任务数量
public int getRunningTaskCount() {
return (int) taskMap.values().stream()
.filter(future -> !future.isCancelled() && !future.isDone())
.count();
}
// 检查任务状态
public boolean isRunning(Long configId) {
ScheduledFuture<?> future = taskMap.get(configId);
return future != null && !future.isCancelled() && !future.isDone();
}
七、扩展性设计
7.1 新增任务类型
只需创建新的 Executor,无需修改调度器:
java
@Component
public class NewTaskExecutor {
public void execute(Long configId) {
// 新任务逻辑
}
}
// 注册时传入新的 Executor
taskScheduler.scheduleTask(id, cron, () -> newTaskExecutor.execute(id));
7.2 配置字段扩展
使用 JSON 格式存储范围配置,新增字段无需修改表结构:
json
{
"participationRateRange": {"min": 0.2, "max": 0.6},
"logsPerUserRange": {"min": 5, "max": 10},
"workTimeRange": {"start": 8, "end": 22}
}
八、注意事项
- Cron 表达式校验: 注册前必须验证表达式合法性
- 线程池大小: 根据任务数量和执行时长合理设置
- 优雅关闭: 确保应用关闭时等待任务完成
- 异常隔离: 单个任务异常不应影响其他任务
- 并发控制: 长时间任务必须防止并发执行
- 资源清理: 取消任务时清理执行状态标记
- JSON 解析: 注意数字类型转换问题
- 统计更新: 使用 SQL 原子操作避免并发问题
九、总结
本调度系统基于 Spring 原生能力实现,具有以下优势:
✅ 简洁 : 仅3个核心类,代码量少
✅ 可靠 : 真正的 Cron 调度,支持复杂表达式
✅ 健壮 : 并发控制、异常隔离、优雅关闭
✅ 可扩展 : 新增任务类型无需修改调度器
✅ 可监控 : 详细的执行日志和统计信息
✅ 易维护: 配置化管理,支持动态启停