当一个服务部署在多台服务器上时,定时任务可能出现多次执行的情况,就是每个服务上执行一次。有以下两种思路,一是固定死只有某服务器执行定时任务,二是随机暂停几秒,某一服务执行了,其他就不再执行。
1、固定某一个服务器作为执行定时任务的机器
通过配置文件或者其他方式,在一个服务器懂的时候可以用参数动态配置他是是否执行程序中的定时任务。
该方式存在一定的弊端,一个项目部署到到多个服务,本意是提高服务的负载和可用性,如果将定时任务固定设置在一个服务上, 那么该服务宕机后,定时任务就无法执行,存在少执行定时任务的可能性。
步骤:
配置文件中设置参数值,确定是否开启定时任务
# 定时抽取corn表达式
schedule-corn: "0 30 07 * * ?"
# 是否开启定时任务 0不开启,1开启
schedule-tag: 1
代码中获取参数并校验,改服务是否开启
//获取配置文件中的参数值
@Value("${schedule-tag}")
private Integer ScheduleTag;
@Scheduled(cron = "${schedule-corn}")
public void Schedule(){
if (Objects.equals("1",ScheduleTag)){
//执行定时任务的内容
}
}
2、借助Redis的过期机制
为你的定时器在Redis中定义一个键值对,可以用项目名称和服务器ip,执行任务前先从Redis中读取键,若没有值代表任务未被执行,同样的该台机器先更新redis,再触发定时任务。由于Redis存在过期机制,因此可以设置过期时间保证下次判断正常。
该模式下也存在一定问题,当Redis宕机时,所有的服务都能够执行定时任务;当多个服务的系统时间不相同时,若Redis键值对的过期时间过小,也有可能出现重复执行的情况。
步骤:
将前置逻辑封装为一个方法
/**
* 定时任务前置逻辑
* @param expiryTime 过期时间
* @param key
* @return
*/
public boolean preTask(String key,Long expiryTime) {
// 随机停1-10秒。
int randomInt = ThreadLocalRandom.current().nextInt(1, 10);
try {
Thread.sleep(randomInt * 1000L);
} catch (InterruptedException e) {
log.info(e.getMessage());
}
//获取服务所在服务器的名字
String name = SystemUtil.getHostInfo().getName();
//判断Redis是否存在该key,若不存在则保存参数中的键值对,并设置过期时间
Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, name + port, expiryTime, TimeUnit.SECONDS);
if (Objects.isNull(absent) || !absent) {
String namePort = (String) redisTemplate.opsForValue().get(key);
log.info("本实例不执行此轮 {} 定时任务。由 {} 执行。", key, namePort);
return false;
}
log.info("本实例开始执行 {} 定时任务。", key);
return true;
}
定时任务调用前置逻辑
//静态变量,作为该定时任务的key
private static final String EXTRACT_SCHEDULE_KEK = "TIME_TASK:EXTRACT_SCHEDULE_KEK";
@Scheduled(cron = "${schedule-corn}")
public void extractSchedule(){
// 多实例限制
boolean preTask = redisTaskUtil.preTask(EXTRACT_SCHEDULE_KEK, 3600L);
if (preTask){
//定时任务的内容
}
}