- 定时任务在部署多台服务时会出现重复执行的问题,所以要借助基于reidis的redisson分布式锁来保证每次只执行一次。
- 配置
2.1 引入redisson
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.14.0</version>
</dependency>
2.2 redisson配置
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yuwen
* @date 2021-05-06
* @description
*/
@Configuration
public class MyRedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() {
Config config = new Config();
// 集群
// config.useClusterServers().addNodeAddress("redis://10.12.34.53:6379");
// 单例 如果时ssl安全连接,则使用rediss://
config.useSingleServer().setAddress("redis://172.29.120.252:6379");
config.useSingleServer().setPassword("123456");
return Redisson.create(config);
}
}
2.3 编写一个注解用于注释表示定时任务
import java.lang.annotation.*;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
* @author yuwen
* @param lockTime 加锁时间,超过这个时间自动释放锁
* @param minInterval 最小执行期间,如果前一次任务执行和这一次执行时间差小于这个区间,则跳过这次执行
* @date 2021-05-06
* @description
*/
public @interface ScheduleLock {
String value();
long lockTime();
TimeUnit lockTimeUnit();
long minInterval();
ChronoUnit intervalTimeUnit();
}
2.4 编写一个切面,用于添加分布式锁
import lombok.extern.slf4j.Slf4j;
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.redisson.api.RAtomicLong;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
* @author yuwen
* @date 2021-05-06
* @description 通过aop方式给定时任务添加分布式锁并通过原子整长型来控制定时任务执行的次数
*/
@Aspect
@Component
@Slf4j
public class ScheduleLockAspect {
@Autowired
private RedissonClient redissonClient;
@Around("@annotation(com.example.mybatistest.config.ScheduleLock) && " +
"@annotation(org.springframework.scheduling.annotation.Scheduled)")
public Object lockAndExecute(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
ScheduleLock annotation = signature.getMethod().getAnnotation(ScheduleLock.class);
String scheduleName = annotation.value();
String lockName = toLockName(scheduleName);
// 获取锁
RLock lock = redissonClient.getLock(lockName);
try {
// 锁住
boolean locked = lock.tryLock(annotation.lockTime(), annotation.lockTime(), annotation.lockTimeUnit());
if (!locked) {
log.warn("ScheduleLock {}: getting lock failed", scheduleName);
return null;
}
} catch (InterruptedException e) {
log.error("ScheduleLock {}: lock interrupted", scheduleName, e);
return null;
}
try {
// 获取原子整长形
RAtomicLong atomicUpdateTime = redissonClient.getAtomicLong(toTimestampName(scheduleName));
Instant lastExecTime = Instant.ofEpochMilli(atomicUpdateTime.get());
log.info(String.format("redisson原子时间:%s", LocalDateTime.ofInstant(lastExecTime, ZoneId.of("Asia/Shanghai"))));
log.info(String.format("当前时间:%s", LocalDateTime.ofInstant(Instant.now(), ZoneId.of("Asia/Shanghai"))));
// 获取当前时间和原子整长形区间
Duration elapsed = Duration.between(lastExecTime, Instant.now());
// 手动指定的区间
Duration minInterval = Duration.of(annotation.minInterval(), annotation.intervalTimeUnit());
if (elapsed.compareTo(minInterval) >= 0) {
// 如果区间已经超过指定的区间,则执行当前定时任务
log.debug("ScheduleLock {}: lock acquired, proceeding", scheduleName);
Object result = pjp.proceed();
atomicUpdateTime.set(Instant.now().toEpochMilli());
log.debug("ScheduleLock {}: execution succeeded", scheduleName);
return result;
} else {
// 如果时间没有达到指定的区间,则不执行当前定时任务
log.info("ScheduleLock {}: Min execution interval doesn't meet, last exec: {}",
scheduleName, lastExecTime);
return null;
}
} finally {
log.info("解锁");
lock.unlock();
}
}
private String toLockName(String srcName) {
return "_schedule_lock__" + srcName;
}
private String toTimestampName(String srcName) {
return "_schedule_timestamp__" + srcName;
}
}
3 定时任务
import com.example.mybatistest.config.ScheduleLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
@Configuration
@Slf4j
/**
* @author yuwen
* @date 2021-05-06
* @description 定时任务,这里添加两个注解,一个是定时任务注解,一个是自定义注解,自定义注解用来添加分布式锁的
*/
public class TestTask {
/**
* 定时10分钟执行一次
* lockTime 锁的时长,这里锁2min来执行任务,2min后不管任务执行完成与否,都释放锁
* minInterval 最小执行时间,如果任务在5min内又执行了,则跳过这次执行
*/
@Scheduled(cron = "0 0/10 * * * ?")
@ScheduleLock(
value = "lower_limit_alarm_lock",
lockTime = 2,
lockTimeUnit = TimeUnit.MINUTES,
minInterval = 5,
intervalTimeUnit = ChronoUnit.MINUTES
)
public void testTask() {
log.info("test");
}
}
- 当然有更好的开源方案,比如XXL-JOB分布式调度平台,很多大公司都在用,个人还是倾向于使用成熟的平台来实现微服务中的定时任务