需求:
项目中的定时任务会定时执行,但如果部署多个服务器的话就会在同一时间,每个服务都会执行一次,如果是新建或者修改类的操作的话就会有冲突,因此需要一个方案处理这个多处执行的问题。
简单来说就是通过aop环绕切片将需要加锁的方法包起来,然后在执行前往redis中用setIfAbsent写入一个key,如果返回true,说明没有其他服务正在执行该方法,就继续执行,如果返回false,说明已经有其他服务正在执行该方法,就不执行。
为了方便使用采用了自定义注解的方式,如果哪个定时任务需要使用的话直接加一个@ScheduledLock注解即可。
该方案使用到了redis,使用以及配置方法略过。
实现如下:
引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
添加自定义注解接口
package com.miracle.qaodo.annotation;
import java.lang.annotation.*;
/**
* @Author Diuut
* @Date 2021/2/23 16:17
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface ScheduledLock {
}
实现AOP逻辑
package com.miracle.qaodo.aspect;
import com.miracle.qaodo.annotation.ScheduledLock;
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.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* @Author Diuut
* @Date 2021/2/23 16:20
*/
@Aspect
@Component
@Slf4j
public class ScheduleLockAspect implements ApplicationContextAware {
//直接autowired redistemplate会报错Unexpected error occurred in scheduled task
//因为@Scheduled注解方式级别高于资源注入级别,导致了资源注入失败
//使用ApplicationContextAware,它实现了这个接口的bean,当spring容器初始化的时候,会自动的将ApplicationContext注入进来
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static ApplicationContext getContext() {
return context;
}
public static Object getBean(String name) {
return getContext().getBean(name);
}
@Around(value = "@annotation(scheduledLock)")
public Object around(ProceedingJoinPoint point, ScheduledLock scheduledLock) {
//拦截的类名
Class clazz = point.getTarget().getClass();
//拦截的方法
Method method = ((MethodSignature) point.getSignature()).getMethod();
log.info("定时任务锁 拦截了类:" + clazz + " 方法:" + method);
Object proceed = null;
RedisTemplate<String,String> redisTemplate = (RedisTemplate)getBean("redisTemplate");
if (redisTemplate.opsForValue().setIfAbsent("qdchess-SchdulesLock-" + method.getName(), "lock",10, TimeUnit.SECONDS)) {
//此处的key是可以根据自己的使用情况进行设置,只要方法之间不重复即可。
log.info("其他服务未执行,通过执行");
//获取锁,如果为false说明有其他服务正在执行,跳过执行
try {
proceed = point.proceed(); //执行定时任务
redisTemplate.delete("qdchess-SchdulesLock-" + method.getName());
return proceed;
} catch (Throwable throwable) {
redisTemplate.delete("qdchess-SchdulesLock-" + method.getName());
throwable.printStackTrace();
return null;
}
}
log.info("其他服务已执行,未通过执行");
return proceed;
}
}
在测试编写期间反复报错Unexpected error occurred in scheduled task,经查询是因为@Scheduled注解方式级别高于资源注入级别,导致了资源注入失败,解决方案是类实现ApplicationContextAware,它实现了这个接口的bean,当spring容器初始化的时候,会自动的将ApplicationContext注入进来。
使用方法就是在使用定时任务的地方加一个@ScheduledLock注解即可。如:
@Scheduled(cron = "0,20,40 * * * * ?")
@ScheduledLock
public void testLock(){
log.info(ServerTimer.getFull());
}