• 关于为了一时方便,使用@Scheduled注解定时踩的坑 Tom


    摘要:

            事情是这样的前两周在做项目的时候碰到一个需求---要求每天晚上执行一个任务,公司统一使用的是 xxl-job 写定时任务的,我当时为了方便自己,然后就简单的使用了Spring的那个@Scheduled来定时,当时写完觉得这也太方便了吧,以后我就只使用这个方法定时了,方便又快捷,用什么 xxl-job 呢(要什么自行车呢),哈哈。

    需求:

            要求在当天11:58的时候需要生成当天任务的报告,然后在第二天12:10得时候生成第二天得任务,-----本来我使用了两个@Scheduled注解,想用两个定时任务来做的,结果生成任务的那一个定时任务居然不起作用。。。然后刚好起作用的这个任务在不起作用的那个之前,所以我就在第一个任务执行时发消息到MQ,使用MQ的延时队列来做了。

    换一种阅读体验方式吧,嘿嘿

    一、第一版代码

        @Scheduled(cron = "0 58 23 */1 * ?")
        @Override
        public void createReprotTimer() {
            // 发送到 MQ 12分钟后(00:10)--- 生成第二天的任务
            String activeProfile = SpringUtil.getActiveProfile();
            String tag = "prod".equals(activeProfile) ? "CREATE_TASK" : "CREATE_TASK_DEV";
            producerUtil.sendTimeMsg(
                    "TID_COMMON",
                    tag,
                    "生成任务".getBytes(),
                    "CREATE_TASK",
                    System.currentTimeMillis() + 720 * 1000
            );
            // 每天晚上 11:58 生成报告
            this.createReprot();
        }

    是不是贼简单,哈哈,一切看着没什么毛病,当然在测试环境测试时也没有任何问题,但是后面上了生产问题就暴露出来了

    到生产发现的问题:测试环境一切正常,在生产环境一直会出现报告和任务重复生成的情况

    原因:我们测试环境只有一台服务器,所以这个执行完全没毛病,但是到了正式环境时每个服务都是以集群的形式部署的,当时我这个服务部署在两台服务器上,所以每天到了11:58的时候两台服务器都会检测到我的定时,所以它们会执行两次。

    二、第二版代码

    方案:基于这个问题我立刻就想到了跨服务器肯定的使用第三方工具了,于是就使用了Redis来解决。

    思路:执行这个方法的时候,先判断Redis中是否存在我存放的指定的业务Key,如果里面已经存在了这个Key,那么就说明已经生成过任务--执行过这个方法了,那么就直接跳过,如果判断里面没有Key---说明今天还没有执行过定时任务,则直接执行,然后再把Key压到Redis中并且定时一分钟后过期。

        @Scheduled(cron = "0 58 23 */1 * ?")
        @Override
        public void createReprotTimer() {
            if (!redisUtil.hasKey("CREATE_TASK_TIME")) {
                // // 发送到 MQ 12分钟后(00:10)--- 生成第二天的任务
                String activeProfile = SpringUtil.getActiveProfile();
                String tag = "prod".equals(activeProfile) ? "CREATE_TASK" : "CREATE_TASK_DEV";
                producerUtil.sendTimeMsg(
                        "TID_COMMON",
                        tag,
                        "生成任务".getBytes(),
                        "CREATE_TASK",
                        System.currentTimeMillis() + 720 * 1000
                );
                // 每天晚上 11:58 生成报告
                this.createReprot();
                redisUtil.setEx("CREATE_TASK_TIME", "CREATE_TASK_TIME", 1, TimeUnit.MINUTES);
            }
        }

    第二版代码出现的问题:不起作用,依然会重复执行

    分析:貌似代码没问题,思路也没问题了。。。

    原因:看似代码没任何问题----当然对于一般业务性质的问题是没有太大的问题的,但是我们这个场景是定时场景,那就意味着两台服务器肯定是同一时刻执行到这段代码的----他们同时判断Redis中是否有这个业务Key,那么这个它们肯定得到的结果就是Redis中没有这个Key,它们两个就会执行这段代码,导致生成重复的任务;

    解决方法:问题原因找到了,那么也就意味着问题已经几乎解决了,很明显,这里只需要加一个分布式锁就可以了,保证在这个时间点上这段代码是顺序执行的就可以了。

    三、第三版代码

    分布式锁我这里使用的是Redisson,用法很简单,开箱即用--------算了写一下吧

    1、引入依赖:
    <!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.16.3</version>
    </dependency>
    
     2、业务代码:
        @Scheduled(cron = "0 58 23 */1 * ?")
        @Override
        public void createReprotTimer() {
            // 幂等处理,防止生成重复报告和发送重复MQ消息, 加锁:防止两台服务同时执行这段代码
            RLock lock = redissonClient.getLock("CREATE_REPORT");
            lock.lock(1, TimeUnit.MINUTES);
            try {
                if (!redisUtil.hasKey("CREATE_TASK_TIME")) {
                    // // 发送到 MQ 12分钟后(00:10)--- 生成第二天的任务
                    String activeProfile = SpringUtil.getActiveProfile();
                    String tag = "prod".equals(activeProfile) ? "CREATE_TASK" : "CREATE_TASK_DEV";
                    producerUtil.sendTimeMsg(
                            "TID_COMMON",
                            tag,
                            "生成任务".getBytes(),
                            "CREATE_TASK",
                            System.currentTimeMillis() + 720 * 1000
                    );
                    // 每天晚上 11:58 生成报告
                    this.createReprot();
                    redisUtil.setEx("CREATE_TASK_TIME", "CREATE_TASK_TIME", 1, TimeUnit.MINUTES);
                }
            } catch (Exception e) {
                throw new ServiceException(OrderExceptionEnum.ORDER_FILE_CANCEL);
            } finally {
                lock.unlock();
            }
        }

     稍微再解释一下吧:首先我们将这一段代码锁住,并且设置锁的超时时间为1分钟,防止出现所无法释放的情况,然后执行时去Redis中获取这个唯一的业务Key,如果没有就直接执行这个代码,并且执行完成之后将Key压入Redis中;如果已经有了我们要找的这个唯一的业务Key,那么就直接跳过即可。

    完美解决---又是一个愉快的周末

    输了不可怕,大不了从头再来,我们还年轻---周红

  • 相关阅读:
    数据库复习笔记
    mysql基础实验过程+遇到的问题的解决方法(error105处理)
    R文件变红原因to按钮变色的优化
    windos命令行设置网络
    牛客网-21天刷题计划-第2节 进阶-对称的二叉树
    0型文法、1型文法、2型文法、3型文法对照
    ES练习遇到错误
    安装kafka
    使用ES时踩过的坑
    前端报错
  • 原文地址:https://www.cnblogs.com/Tom-shushu/p/15676773.html
Copyright © 2020-2023  润新知