场景说明:web服务原本仅有一台机器,现在因为用户量增加准备扩充为2台机器,那项目中的定时任务要求仅能单次在一个机器上执行。
解决:准备采用redis实现分布式锁功能,定时任务执行前先查看执行方法对应的key在redis中是否存在,如果不存在,则把key放入(相当于加锁),指定过期时间(防止方法执行失败导致的死锁),方法执行完成后移除key(释放锁);如果key已经存在,则直接跳过,方法不执行。
具体代码如下:
先声明一个接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JobLock {
// redis锁key的后缀,为空取method.toString结果;否则取设置的值;
// 建议设置为空字符串
String lockedSuffix() default "";
// key在redis里存在的时间,秒
int expireSecond() default 10;
}
切面类如下:
@Aspect
@Component
public class JobLockAspect {
// 假设存在三个集群(每集群2机器)独立运行,但是使用相同的redis,会导致6台机器仅有一个机器执行,实际我需要每个集群中均有一个机器执行
// token这里就是用来区分不同的集群
@Value("${passport.token}")
private String token;
private static final String LOCK_VALUE = "locked";
private static final String REDIS_START = "BI_";
static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(DataSourceAspect.class);
@Autowired
private ICacheService redisService;
public void cacheLockPoint(ProceedingJoinPoint joinPoint) {
try {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
// 被拦截的方法
Method method = methodSignature.getMethod();
// 获取被拦截方法上面的 @JobLock注解的内容
if (method.getAnnotation(JobLock.class) == null){
joinPoint.proceed();
return;
}
String lockKey = method.getAnnotation(JobLock.class).lockedSuffix();
// 如果设置的key为空,则method.toString()作为key,实例如:public void com.ruisitech.bi.job.TestJob.monitorMigrate()
if(StringUtils.isEmpty(lockKey)){
lockKey = method.toString();
}
int timeOut = method.getAnnotation(JobLock.class).expireSecond();
String redisKey = getRedisKey(lockKey);
if (redisService.setnx(redisKey,LOCK_VALUE).intValue() == 1) {
redisService.expire(redisKey,timeOut);
log.info("method:" + lockKey +"获取锁:"+ redisKey+",开始运行!");
joinPoint.proceed();
// 移除
redisService.del(redisKey);
return;
}else{
log.info("method:" + lockKey + "获取锁:"+ redisKey + ",失败!");
return ;
}
} catch (Exception e) {
log.error("定时任务执行失败", e);
} catch (Throwable e) {
log.error("定时任务执行失败", e);
}
}
public String getRedisKey(String lockKey){
return REDIS_START + token + lockKey;
}
}
aop配置如下:
<bean id="jobLockAspect" class="com.ruisitech.bi.job.manage.JobLockAspect"></bean>
<!--定义切面信息-->
<aop:config>
<!-- 切面类 -->
<aop:aspect ref="jobLockAspect">
<!-- 切入点 -->
<aop:pointcut expression="execution (* com.aaa.bb.job..*.*(..))" id="myPointcut" />
<!-- 切入的方法 -->
<aop:around method="cacheLockPoint" pointcut-ref="myPointcut" />
</aop:aspect>
</aop:config>
使用实例:com.ruisitech.bi.job包中某个类中包含如下两个定时方法
@Scheduled(cron = "0 */1 * * * ?")
@JobLock(lockedSuffix ="test", expireSecond =10)
public void test1(){
System.out.println("countThreeDayNum");
}
@Scheduled(cron = "0 */1 * * * ?")
@JobLock(lockedSuffix ="test", expireSecond =10)
public void test2(){
System.out.println("getDayMigrateTableMeta");
}
那么test1和test2每分钟会仅有一个方法执行。
内部方法调用导致的 aop失效问题
见:https://blog.csdn.net/h2604396739/article/details/102610610