项目开发过程中,或多或少会涉及到需要定时任务来执行处理的功能模块;而往往定时任务处理是很重要的一层;对定时任务的管控与监控就显得很重要。本文就如何对定时任务动态管理,与简单监控做以说明。
工程github仓库地址:https://github.com/nirvana-x/dispatch_task
配置类:定时任务的加载,启动时,初始化好定时任务,本文是从数据库读取,初始化后,进行装配即可。然后在具体执行的时候加锁控制。
/**
* @author
* @Description 定时任务配置类
* @Date 2019/5/14 18:47
**/
@Component
@EnableScheduling
public class TaskConfig implements SchedulingConfigurer {
private static final Logger logger = LoggerFactory.getLogger(TaskConfig.class);
private static final int TIMEOUT = 5 * 1000;
/**
* 任务执行的间隔时间60秒 小于60秒时,不让其执行
*/
private static final int INTERVAL_TIME = 30;
private static final String LOCK_PREFIX = "lock_";
private Map<String, ScheduledFuture<?>> futures = new HashMap<>();
@Autowired
private TaskBeanDao taskBeanDao;
@Autowired
private RedisLockHelper redisLockHelper;
@Autowired
@Qualifier("threadPoolTaskScheduler")
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
/**
* 模拟 schedule 配置,实际执⾏行行是从 t_task_config 表读取
*/
private List<TaskBean> taskConfigs = new ArrayList<>();
/**
* 定时获取数据库最新的定时任务
*/
@PostConstruct
public void init() {
taskConfigs = taskBeanDao.findByEnable(TaskBeanEnableEnum.ENABLE_STATUS.getType());
}
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
for (TaskBean taskBean : taskConfigs) {
ScheduledFuture future = null;
if (taskBean.getCron().contains("fixedDelay")) {
long fixedDelay = Long.valueOf(taskBean.getCron().split("fixedDelay=")[1]);
future = threadPoolTaskScheduler.scheduleWithFixedDelay(new TaskThread(taskBean), fixedDelay);
} else {
future = threadPoolTaskScheduler.schedule(new TaskThread(taskBean), new
CronTrigger(taskBean.getCron()));
}
futures.put(taskBean.getTaskCode(), future);
}
}
public class TaskThread extends Thread {
private TaskBean taskBean;
public TaskThread(TaskBean taskBean) {
this.taskBean = taskBean;
}
public TaskBean getTaskBean() {
return taskBean;
}
public void setTaskBean(TaskBean taskBean) {
this.taskBean = taskBean;
}
@Override
public void run() {
long time = System.currentTimeMillis() + TIMEOUT;
String lock = LOCK_PREFIX + taskBean.getTaskCode();
if (!redisLockHelper.lock(lock, String.valueOf(time))) {
logger.info("【定时任务未获取到锁【{}】】", lock);
} else {
try {
//获取锁成功
logger.info("【定时任务获取锁{}成功】,任务编码:{},任务名称:{}", lock, taskBean.getTaskCode(), taskBean.getTaskName());
//获取最新的task任务执行信息
taskBean = taskBeanDao.findById(taskBean.getId()).orElse(null);
//TODO 验证执行时间是否有效 避免锁在毫秒级就被释放
long executeTime = (time - TIMEOUT - taskBean.getLastExecuteTime().getTime()) / 1000;
if (executeTime < INTERVAL_TIME) {
logger.info("【该任务已被其他实例执行】,任务编码:{},任务名称:{}", lock, taskBean.getTaskCode(),
taskBean.getTaskName());
return;
}
//调用业务模块进行处理
logger.info("【执行定时任务【编码:{}】开始】 任务名称:{}", taskBean.getTaskCode(), taskBean.getTaskName());
//2.2 合法性校验.
if (StringUtils.isEmpty(taskBean.getCron())) {
logger.info("【定时任务【编码:{}】执行计划为空】,任务名称:{}", taskBean.getTaskCode(), taskBean.getTaskName());
} else {
taskBean.setLastExecuteTime(new Date());
taskBeanDao.saveAndFlush(taskBean);
String postResult = HttpUtils.postHttp(taskBean.getUrl(), null, 30000);
logger.info("任务【编码:{}】调用业务层返回:{}", taskBean.getTaskCode(), postResult);
}
logger.info("【执行定时任务【编码:{}】结束】 任务名称:{}", taskBean.getTaskCode(), taskBean.getTaskName());
} catch (Exception e) {
logger.error("【调用业务处理层报错】,任务编码:{},任务名称:{}", taskBean.getTaskCode(),
taskBean.getTaskName());
logger.error(e.getMessage(), e);
} finally {
redisLockHelper.unlock(lock, String.valueOf(time));
logger.info("【定时任务释放锁【{}】成功】,任务编码:{},任务名称:{}", lock, taskBean.getTaskCode(),
taskBean.getTaskName());
}
}
}
}
public Map<String, ScheduledFuture<?>> getFutures() {
return futures;
}
}
任务锁处理类:获取锁与释放锁的处理工具类。
/**
* @author
* @Description ${TODO}
* @Date 2019/5/23 9:59
**/
@Component
public class RedisLockHelper {
private static final Logger logger = LoggerFactory.getLogger(RedisLockHelper.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 加锁
* @param targetId targetId - 唯一标志
* @param timeStamp 当前时间+超时时间 也就是时间戳
* @return
*/
public boolean lock(String targetId,String timeStamp){
if(stringRedisTemplate.opsForValue().setIfAbsent(targetId,timeStamp)){
// 对应setnx命令,可以成功设置,也就是key不存在
return true;
}
// 判断锁超时 - 防止原来的操作异常,没有运行解锁操作 防止死锁
String currentLock = stringRedisTemplate.opsForValue().get(targetId);
// 如果锁过期 currentLock不为空且小于当前时间
if(!Strings.isNullOrEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()){
// 获取上一个锁的时间value 对应getset,如果lock存在
String preLock =stringRedisTemplate.opsForValue().getAndSet(targetId,timeStamp);
// 假设两个线程同时进来这里,因为key被占用了,而且锁过期了。获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
// 而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的timeStamp已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
if(!Strings.isNullOrEmpty(preLock) && preLock.equals(currentLock) ){
// preLock不为空且preLock等于currentLock,也就是校验是不是上个对应的时间戳,也是防止并发
return true;
}
}
return false;
}
/**
* 解锁
* @param target
* @param timeStamp
*/
public void unlock(String target,String timeStamp){
try {
String currentValue = stringRedisTemplate.opsForValue().get(target);
if(!Strings.isNullOrEmpty(currentValue) && currentValue.equals(timeStamp) ){
// 删除锁状态
stringRedisTemplate.opsForValue().getOperations().delete(target);
}
} catch (Exception e) {
logger.error("解锁异常{}",e);
}
}
}
Redis配置类:主要针对的是多实例间,任务同步问题,即某一实例任务执行计划有变动,则通知其他实例,也进行相应的变动。这里采用的是Redis的消息发布与订阅模式。这里重要的是订阅主题。一定要设置监听的topic。
@Configuration
@EnableCaching
public class RedisConfig {
@Autowired
private TaskBeanDao taskBeanDao;
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
List<TaskBean> taskBeanList = taskBeanDao.findByEnable(1);
taskBeanList.forEach(taskBean -> {
//订阅主题
container.addMessageListener(listenerAdapter, new PatternTopic(taskBean.getTaskCode()));
});
return container;
}
@Bean
MessageListenerAdapter listenerAdapter(MessageReceive receiver) {
//这个地方 是给messageListenerAdapter 传入一个消息接受的处理器,利用反射的方法调用“receiveMessage”
//也有好几个重载方法,这边默认调用处理器的方法 叫handleMessage 可以自己到源码里面看
return new MessageListenerAdapter(receiver, "receiveMessage");
}
}
消息订阅处理类
/**
* @author
* @Description redis 消息处理器
* @Date 2019/6/4 14:25
**/
@Component
public class MessageReceive {
private static final Logger logger = LoggerFactory.getLogger(MessageReceive.class);
@Autowired
private TaskConfig taskConfig;
@Autowired
private TaskBeanDao taskBeanDao;
/**
* 接收消息的方法
*/
public void receiveMessage(String message) {
logger.info("【Redis 消息订阅】,{}",message);
JSONArray object = JSON.parseArray(message);
JSONObject jsonObject = object.getJSONObject(1);
TaskBean taskBean = taskBeanDao.findById(jsonObject.getLong("id")).orElse(null);
logger.info("【匹配定时任务为】,{}",JSONObject.toJSONString(taskBean));
if (taskBean.getEnable().equals(TaskBeanEnableEnum.DISABLE_STATUS.getType())){
//说明定时任务是未启用(暂停定时任务)
taskConfig.cancelTask(taskBean);
}else{
//说明定时任务是新增或者修改
taskConfig.saveOrUpdateTaskConfigs(taskBean);
}
}
}
最后,在任务变动的业务处理模块只需要添加如下代码即可。
//Redis发布消息通知,通知其他实例变动
logger.info("【Redis发布消息通知,刷新定时任务,通知其他实例变动】,任务名称:{}", taskBean.getTaskName());
redisTemplate.convertAndSend(taskBean.getTaskCode(), taskBean);