Scheduled改造

前提: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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值