前提:Spring Scheduled 定时任务、考虑到多节点部署任务会因为节点时间延迟、或者任务交叉重复执行的情况。为了避免任务重复执行、采用注解升级处理
原理: AOP拦截 、redis中间锁来保证一次cron执行任务只能有一次生效。
作用:保证了多节点重复执行问题
注解 @DistributedScheduled
/**
* @Author: Red
* @Descpription: 简单的实现多节点任务重复执行问题、分布式还是选用xxl-job
* 之类的。原理就是AOP拦截 分布式锁过滤 保存结果
* 1s的任务可能会存在问题
* @link
* @Date: 11:31 2023/3/24
* @since 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Scheduled
public @interface DistributedScheduled {
/**
* 执行状态保存时间
* 默认1分钟
* @return
*/
int validity() default 1;
/**
* cron 表达式
* @return
*/
@AliasFor(annotation = Scheduled.class, attribute = "cron")
String cron() default "";
/**
* 信息字符串,可用于提供构建分布式锁key的信息
* @return 信息字符串
*/
String value() default "";
/**
* 过期时间的单位
*
* @return
*/
TimeUnit unit() default TimeUnit.MINUTES;
}
切面
/**
* aop处理
*/
@Component
@Aspect
public class ScheduledAdvisor {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
RedissonExecute redissonExecute;
@Autowired
Environment environment;
/**
* 简单实现
*
* @param point
* @param distributedScheduled
* @return
* @throws Throwable
*/
@Around("@annotation(distributedScheduled)")
public Object around(ProceedingJoinPoint point, DistributedScheduled distributedScheduled) throws Throwable {
//key的名称 项目名称 + 业务名称
long createTimeMillis = SystemClock.now();
String property = environment.getProperty("spring.application.name");
MethodSignature signature = (MethodSignature) point.getSignature();
// 如果没有值则使用方法全名
String value = Optional.ofNullable(distributedScheduled.value()).filter(StringUtils::isNotBlank).orElseGet(() ->
"TMP_LOCK:" + signature.getDeclaringTypeName() + "#" + signature.getMethod().getName()
);
String lockName = KeywordUtils.format(property, value);
String success_key = lockName + "#result";
RLock rLock = redissonExecute.getLock(lockName);
if (rLock.tryLock()) {
logger.warn("锁名称:{},线程ID:{},次数:{}", rLock.getName(), Thread.currentThread().getId(), rLock.getHoldCount());
try {
String success = stringRedisTemplate.opsForValue().get(success_key);
if ("DOING".equalsIgnoreCase(success)) {
logger.warn("{} 任务执行中 cron : {} ", lockName,
distributedScheduled
.cron());
return null;
} else if ("SUCCESS".equalsIgnoreCase(success)) {
logger.debug("{} 任务已经成功执行, cron : {}", lockName, distributedScheduled.cron());
return null;
} else if ("FAIL".equalsIgnoreCase(success)) {
logger.warn("{} 任务已经执行失败,现在再次执行 cron : {}", lockName,
distributedScheduled.cron());
}
Object o;
try {
setSuccessKey(success_key, "DOING", adjustExpireMillis(createTimeMillis, distributedScheduled.validity(),
distributedScheduled.unit()));
o = point.proceed();
setSuccessKey(success_key, "SUCCESS", adjustExpireMillis(createTimeMillis, distributedScheduled.validity(),
distributedScheduled.unit()));
} catch (Throwable throwable) {
setSuccessKey(success_key, "FAIL", adjustExpireMillis(createTimeMillis, distributedScheduled.validity(),
distributedScheduled.unit()));
throw throwable;
}
return o;
} finally {
//这里强制删除锁 //是否考虑 unlock
redissonExecute.forceUnlock(rLock);
}
} else {
logger.info("{} 任务已被其他节点执行 cron : {}", lockName, distributedScheduled.cron());
}
return null;
}
/**
* 结果保持毫秒级
*
* @param success_key
* @param doing
* @param validity
*/
private void setSuccessKey(String success_key, String doing, Long validity) {
if (validity != null && validity != 0) {
stringRedisTemplate.opsForValue().set(success_key, doing, validity, TimeUnit.MILLISECONDS);
} else {
logger.trace("没有找到对应的key {}", success_key);
}
}
/**
* 调整过期时间
*
* @param createTime
* @param validity
* @param timeUnit
* @return
*/
private Long adjustExpireMillis(Long createTime, Integer validity, TimeUnit timeUnit) {
long e = TimeoutUtils.toMillis(validity, timeUnit) - (SystemClock.now() - createTime - 1);
return e < 0 ? 0 : e;
}
测试 Task
@Component
public class TestScheduled {
private final Logger logger = LoggerFactory.getLogger(TestScheduled.class);
@DistributedScheduled(cron = "0/20 * * * * ?", value = "test", validity = 10, unit = TimeUnit.SECONDS)
public void execute() {
logger.info("我触发了我触发了触发了");
}
}
@Test
public void test() {
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.execute(()->testScheduled.execute());
executorService.execute(()->testScheduled.execute());
executorService.execute(()->testScheduled.execute());
executorService.execute(()->testScheduled.execute());
executorService.execute(()->testScheduled.execute());
executorService.execute(()->testScheduled.execute());
executorService.shutdown();
while (!executorService.isTerminated()){}
}
结果
2023-03-24 16:19:48.634 INFO 90940 --- [pool-2-thread-1] cn.xxx.utils.job.ScheduledAdvisor : xxx-xxx:test 任务已被其他节点执行 cron : 0/20 * * * * ?
2023-03-24 16:19:48.635 INFO 90940 --- [pool-2-thread-4] cn.xxx.utils.job.ScheduledAdvisor : xxx-xxx:test 任务已被其他节点执行 cron : 0/20 * * * * ?
2023-03-24 16:19:48.636 INFO 90940 --- [pool-2-thread-3] cn.xxx.utils.job.ScheduledAdvisor : xxx-xxx:test 任务已被其他节点执行 cron : 0/20 * * * * ?
2023-03-24 16:19:48.641 INFO 90940 --- [pool-2-thread-1] cn.xxx.utils.job.ScheduledAdvisor : xxx-xxx:test 任务已被其他节点执行 cron : 0/20 * * * * ?
2023-03-24 16:19:48.641 INFO 90940 --- [pool-2-thread-4] cn.xxx.utils.job.ScheduledAdvisor : xxx-xxx:test 任务已被其他节点执行 cron : 0/20 * * * * ?
2023-03-24 16:19:48.641 WARN 90940 --- [pool-2-thread-2] cn.xxx.utils.job.ScheduledAdvisor : 锁名称:layout-insight:test,线程ID:83,次数:1
2023-03-24 16:19:48.700 INFO 90940 --- [pool-2-thread-2] cn.xxx.insight.job.TestScheduled : 我触发了我触发了触发了
博客参考: https://juejin.cn/post/6844903975829897223