0.前言
传统spring定时任务采用的是@Sechedu
注解去实现,但是该注解只能指定固定的时间任务,例如:配置了2s执行一次,那么永远只能是每两秒执行一次
但是在有些特殊场景下需要一些动态定时任务,例如:最初配置了2s执行一次,在执行任务中,修改配置为5秒执行一次,那么就需要动态的加载配置,使任务动态的变成5s执行一次
1.原理
要想实现动态定时任务,就需要借助Spring
的SchedulingConfigurer
接口,该方式可以实现动态时间,定时任务,具体原理如下图:
2.Cron设计
如下图所示
定时任务数据库表设计
3. 配置解释
3.1 SchedulingConfigurer
要想实现定时任务主要是实现SchedulingConfigurer
接口,然后重写configureTasks
方法
3.2 ScheduledTaskRegistrar
在configureTasks
方法中,ScheduledTaskRegistrar
通过addTriggerTask
来添加触发器任务,从而去执行。而addTriggerTask
方法有两个参数,一个是要执行得任务,一个是触发器
具体配置代码如下:
package net.test.api.job;
import io.lettuce.core.dynamic.support.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.List;
import net.test.demo.common.framework.SpringContext;
import net.test.dao.entity.demo.ScheduleSetting;
import net.test.dao.mapper.demo.ScheduledSettingMapper;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
@Component
public class CronSchedule implements SchedulingConfigurer {
@Autowired
private ScheduledSettingMapper scheduledSettingMapper;
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
// 获取所有任务
List<ScheduleSetting> scheduleList = scheduledSettingMapper.getScheduleList();
System.out.println(scheduleList.size());
if (CollectionUtils.isNotEmpty(scheduleList)){
for (ScheduleSetting s : scheduleList){
scheduledTaskRegistrar.addTriggerTask(getRunnable(s), getTrigger(s));
}
}
}
/**
* 转换首字母小写
*
* @param str
* @return
*/
public static String lowerFirstCapse(String str) {
char[] chars = str.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
private Runnable getRunnable(ScheduleSetting scheduleSetting){
return new Runnable() {
@Override
public void run() {
Class<?> clazz;
try {
clazz = Class.forName(scheduleSetting.getBeanName());
String className = lowerFirstCapse(clazz.getSimpleName());
Object bean = SpringContext.getBean(className);
Method method = ReflectionUtils.findMethod(bean.getClass(), scheduleSetting.getMethodName());
if (StringUtils.isNotEmpty(scheduleSetting.getMethodParams())) {
ReflectionUtils.invokeMethod(method, bean, scheduleSetting.getMethodParams());
} else {
ReflectionUtils.invokeMethod(method, bean);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
};
}
private Trigger getTrigger(ScheduleSetting scheduleSetting){
return new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
CronTrigger trigger = new CronTrigger(scheduleSetting.getCronExpression());
Date nextExec = trigger.nextExecutionTime(triggerContext);
return nextExec;
}
};
}
}
多服务环境下定时任务重复执行问题解决方案
在开发的过程中,经常会遇到需要使用定时器的问题,比如需要定时向任务表写任务。但是项目是部署到集群环境下的,如果不做处理,就会出现定时任务重复执行的问题。问题产生的原因:由于我们项目同时部署在多台集群机器上,因此到达指定的定时时间时,多台机器上的定时器可能会同时启动,造成重复数据或者程序异常等问题。
该问题的解决方案可能有以下几种,仅供参考:
- 一、指定机器执行包含定时器任务
该方案操作简单,但只适合临时解决以下,正常生产环境应该没人会采取该方案。
- 二、数据库加锁
通过数据库的锁机制,来获取任务执行状态,先更新,后执行的原则,可以实现避免任务重新执行的目标。
- 三、redis的过期机制和分布式锁
我们最终决定采用的方案。面向切面编程,通过注解控制
package net.config;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* redis分布式锁解决集群服务器定时任务重复执行的问题,类文件支持多个方法配置
*
* @author Administrator
*/
@Aspect
@Slf4j
@Component
public class CacheLockAspect {
private static final String LOCK_VALUE = "locked";
@Resource
private RedisTemplate redisTemplate;
@Around("execution(* *.*(..)) && @annotation(com.iris.websocket.annotation.CacheLock)")
public void cacheLockPoint(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method cacheMethod = signature.getMethod();
if(null == cacheMethod) {
log.info("未获取到使用方法 pjp: {}", pjp);
throw new RuntimeException("未获取到使用方法 pjp");
}
boolean lock = false;
String lockKey = cacheMethod.getAnnotation(CacheLock.class).lockedPrefix();
long timeOut = cacheMethod.getAnnotation(CacheLock.class).expireTime();
boolean release = cacheMethod.getAnnotation(CacheLock.class).release();
if (StringUtils.isBlank(lockKey)) {
throw new RuntimeException(String.format("method:%s没有配置lockedPrefix", cacheMethod));
}
try {
lock = redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK_VALUE, timeOut, TimeUnit.SECONDS);
if (lock) {
log.info("method:{}获取锁:{},开始运行!", cacheMethod, lockKey);
pjp.proceed();
return;
}
log.info("method:{} Lock被占用不执行此轮任务!", cacheMethod);
release = false;
} catch (Throwable e) {
log.error("method:{},运行错误!", cacheMethod, e);
throw new RuntimeException("获取redis定时任务锁失败");
} finally {
if (lock && release) {
log.info("method:{}释放锁:{},执行完成!", cacheMethod, lockKey);
releaseLock(lockKey, LOCK_VALUE);
}
}
}
/**
* 释放分布式锁
*/
public boolean releaseLock(String key, String value) {
boolean flag = false;
String releaseScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
Long success = 1L;
try {
Long result = (Long) redisTemplate.execute(
(RedisConnection connection) -> connection.eval(
releaseScript.getBytes(),
ReturnType.INTEGER,
1,
key.getBytes(),
value.getBytes())
);
flag = success.equals(result);
} catch (Exception e) {
log.error("释放锁异常, key = {}", key, e);
}
return flag;
}
}
自定义注解
package com.iris.websocket.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheLock {
String lockedPrefix() default ""; //缓存锁key
long expireTime() default 10; //缓存过期时间,单位:秒
boolean release() default true; //执行完成释放锁,不释放锁过期也会释放
}
使用方法
@CacheLock(lockedPrefix = "gtCallConnRateTask", expireTime = 10)
public void gtCallConnRateTask() {
log.info("推送定时任务开始执行------------");
ordersService.gtCallConnRateTask();
log.info("推送定时任务执行完毕------------");
}