基于Redis加锁+注解+AOP解决JOB重复执行问题
- 现象
- 解决方案
- 自定义注解
- 定义AOP策略
- redis 加锁
- 实践
现象
线上xxljob有时候会遇到同一个任务在调度的时候重复执行,如下图:
线上JOB服务运行了2个实例,有时候会重复调度到同一个实例,有时候会重复调度到不同实例上,对于Job重复执行会存在很多风险,可以采用Redis加锁的方式来解决。这里用统一的方式提供这个内部功能,其他Job或者从管理页面进来的请求直接执行Job可以都限制住,保证同一时间分布式环境中只有一个实例在运行。
解决方案
自定义注解
首先定义一个自定义注解,将redis加锁需要的参数可以通过注解声明:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JobNoRepete {
String name();
String redisKey();
long expireTime();
TimeUnit timeUnit();
}
定义AOP策略
@Component
@Aspect
@Slf4j
public class JobNoRepeteAop {
@Resource
private RedisService redisService;
@Around(value = "@annotation(annotation)", argNames = "pj,annotation")
public Object around(ProceedingJoinPoint pj, JobRepetitionDefense annotation) throws Throwable {
String name = annotation.name();
String redisKey = annotation.redisKey();
long expireTime = annotation.expireTime();
TimeUnit timeUnit = annotation.timeUnit();
log.info("job执行防重开始执行,name={},redisKey={},expireTime={},timeUnit={}",
name, redisKey, expireTime, timeUnit);
try {
return redisService.executeOnlyOnce(redisKey, expireTime, timeUnit, pj::proceed);
} finally {
log.info("job执行防重执行完成,name={}", name);
}
}
}
redis 加锁
redis 加锁逻辑,使用spring redis中的StringRedisTemplate:
@Slf4j
@Component
public class RedisService {
@Resource
private StringRedisTemplate stringRedisTemplate;
public <T> T executeOnlyOnce(String redisKey, long expireTime, TimeUnit timeUnit, CustomCallable<T> callable) throws Throwable {
if (StrUtil.isBlank(redisKey) || expireTime <= 0 || Objects.isNull(timeUnit) || Objects.isNull(callable)) {
throw new IllegalArgumentException("参数错误");
}
String uuid = UUID.randomUUID().toString();
if (!stringRedisTemplate.opsForValue().setIfAbsent(redisKey, uuid, expireTime, timeUnit)) {
throw new RuntimeException("任务正在执行,请稍后再试");
}
//执行逻辑
try {
return callable.call();
} finally {
//执行完成主动释放锁
try {
String oldValue = stringRedisTemplate.opsForValue().get(redisKey);
if (Objects.equals(uuid, oldValue)) {
stringRedisTemplate.delete(redisKey);
}
} catch (Exception e) {
//释放锁失败,等待expireTime后自动释放
log.error("释放锁异常", e);
}
}
}
}
public interface CustomCallable<V> {
V call() throws Throwable;
}
实践
对于适用的场景就可以直接使用注解的方式进行声明,例如:
@Service
@Slf4j
public class testService {
private static final int EXPIRE_HOURS = 24;
@JobNoRepete(name = "测试redis", redisKey = Constant.JOB_LOCK_TO_REDIS,
expireTime = EXPIRE_HOURS, timeUnit = TimeUnit.HOURS)
public void test(LocalDate localDate) {
//内部逻辑
}
}