简介
最近遇到了一个业务场景,系统有个功能可以新增服务监测,设置采集周期(/秒),例如每 5 秒、10 秒 采集一次
代码中需要根据设置的周期去采集服务器的状态数据,将状态展示在页面上
最开始我想根据任务调度去实现,每条服务监测数据对应一个调度器,根据设置的周期实时去该 cron 表达式
具体操作参考下面这篇博客
后面发现行不通,cron 表达式不能做到每多少秒执行一次,如每 200 秒执行一次
cron 表达式只能取近似值,每 3 分钟执行一次,像 0/200 * * * * ?,这样的表达式是不合法的
本文介绍如何使用 JDK 中提供的 Scheduled 线程池(ScheduledThreadPoolExecutor)实现我上面描述的场景
ScheduledThreadPoolExecutor
创建一个动态任务管理类,使用 ScheduledThreadPoolExecutor,如下
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.concurrent.*;
@Component
public class DynamicTaskManager {
// 动态线程池:核心线程随任务数增长,最大上限2000
private static final int MAX_THREADS = 2000;
private final ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(
0, // 初始核心线程为0
r -> {
Thread t = new Thread(r);
t.setName("dynamic-task-" + t.getId());
t.setDaemon(false);
return t;
}
);
// 任务注册表:key=任务唯一标识(比如serviceId),value=任务句柄
private final Map<String, ScheduledFuture<?>> taskMap = new ConcurrentHashMap<>();
/**
* 启动或更新一个任务
*/
public void startOrUpdateTask(String taskId, long intervalSeconds, Runnable task) {
// 先停掉旧任务
stopTask(taskId);
// 动态调整核心线程数:每个任务对应一个核心线程,最大不超过2000
int currentTaskCount = taskMap.size() + 1;
int poolSize = Math.min(currentTaskCount, MAX_THREADS);
scheduler.setCorePoolSize(poolSize);
scheduler.setKeepAliveTime(60, TimeUnit.SECONDS);
scheduler.allowCoreThreadTimeOut(true);
// 提交新任务
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(
task,
0,
intervalSeconds,
TimeUnit.SECONDS
);
taskMap.put(taskId, future);
}
/**
* 停止任务
*/
public void stopTask(String taskId) {
ScheduledFuture<?> future = taskMap.remove(taskId);
if (future != null && !future.isDone()) {
future.cancel(false);
}
// 任务减少时,相应调整核心线程数
int currentTaskCount = taskMap.size();
scheduler.setCorePoolSize(currentTaskCount);
}
/**
* 应用关闭时优雅停止所有任务
*/
@PreDestroy
public void shutdown() {
scheduler.shutdown();
taskMap.clear();
}
}
使用起来就很简单了,在项目启动时,取读取所有的服务监测表,初始化这些任务,加到管理器中
页面上每次新增服务监测时,或者更新/删除服务监测时,同步维护这个任务管理器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
@Autowired
private DynamicTaskManager taskManager;
@PostMapping("/start")
public Map<String, Object> startTask(@RequestParam String taskId,
@RequestParam(defaultValue = "5") long intervalSeconds) {
Map<String, Object> result = new HashMap<>();
try {
SampleTask task = new SampleTask(taskId);
taskManager.startOrUpdateTask(taskId, intervalSeconds, task);
result.put("success", true);
result.put("message", "任务 [" + taskId + "] 已启动,间隔: " + intervalSeconds + "秒");
} catch (Exception e) {
result.put("success", false);
result.put("message", "启动任务失败: " + e.getMessage());
}
return result;
}
@PostMapping("/stop")
public Map<String, Object> stopTask(@RequestParam String taskId) {
Map<String, Object> result = new HashMap<>();
try {
taskManager.stopTask(taskId);
result.put("success", true);
result.put("message", "任务 [" + taskId + "] 已停止");
} catch (Exception e) {
result.put("success", false);
result.put("message", "停止任务失败: " + e.getMessage());
}
return result;
}
@PostMapping("/update")
public Map<String, Object> updateTask(@RequestParam String taskId,
@RequestParam long intervalSeconds) {
Map<String, Object> result = new HashMap<>();
try {
SampleTask task = new SampleTask(taskId);
taskManager.startOrUpdateTask(taskId, intervalSeconds, task);
result.put("success", true);
result.put("message", "任务 [" + taskId + "] 已更新,新间隔: " + intervalSeconds + "秒");
} catch (Exception e) {
result.put("success", false);
result.put("message", "更新任务失败: " + e.getMessage());
}
return result;
}
}
测试
写个简单任务
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class SampleTask implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(SampleTask.class);
private final String taskId;
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public SampleTask(String taskId) {
this.taskId = taskId;
}
@Override
public void run() {
String currentTime = LocalDateTime.now().format(formatter);
logger.info("任务 [{}] 执行时间: {}", taskId, currentTime);
}
}
启动项目,试一下
更新一下采集周期,改成每 10 秒执行一次
看日志,采集周期改了,完全可以
再加两个任务,看每个任务都能根据自己的周期去执行,非常 nice