• SpringBoot整合定时任务遇到的多实例问题


    唠嗑部分

    是这样,前几日完善了定时任务的日志记录,今日切换了服务器,多部署了一个节点,使用nginx负载均衡,但是查看日志却发现了如下情况

    image-20231105153728831

    那糟糕了,传说中的多实例问题出现了,今天我们就来聊聊项目实战中定时任务如何做,首先我们看如下问题

    1、什么是定时任务,能帮我们解决什么实际问题?

    见名知意,定时任务就是让程序指定时间去执行某段代码,例如,每日8点给女朋友发早安祝福

    那么能给我们开发中解决什么问题呢?

    在实际开发中,有许多需要定时任务的场景,如,每日定时去同步数据、缓存的预热、定时清理日志文件、定时统计榜单…

    2、项目实战中哪些场景需要使用到定时任务?

    需求一:产品经理要求实现系统的3天内热搜榜,每日0点更新数据

    需求二:系统需要依赖第三方系统的数据,而且请求并发较大,第三方数据是每日更新的

    需求三:系统每天都会有大量操作日志,产品经理要求只保留一个月的数据

    需求四:对于系统主页数据,每日9-12点并发最大,需要定时对缓存预热

    以上需求都可以用定时任务实现

    3、推荐使用的定时任务组件有哪些?

    Spring整合了Scheduled,轻量级而且很好用,无UI展示

    xxl-Job,xxl是xxl-job的开发者大众点评的许雪里名称的拼音开头,主要用于处理分布式的定时任务,其主要由调度中心和执行器组成,有良好的UI界面。

    elastic-Job,Elastic-Job是当当网推出的分布式任务调度框架,用于解决分布式任务的协调调度问题,保证任务不重复不遗漏地执行;无UI展示,需要分布式协调工具Zookeeper的支持

    4、如何实现分布式定时任务,避免多实例问题?

    首先我们来说说什么是多实例问题,在我们的项目开发中,我们在部署定时任务时,通常只部署一台机器,如果部署多台机器时,同一个任务会执行多次(每个机器都会执行,互不影响),那如果有一些给用户计算收益定时任务,每天定时给用户计算收益,如果部署了多台,同一个用户将重复计算多次收益,那就芭比Q了,那如果只部署一台,则会有单点故障问题,可用性无法保证

    以上所说的xxl-job,elastic-Job均可以解决多实例问题,保证任务不重复不遗漏地执行

    那我们使用Spring自带的Scheduled,如何避免多实例问题呢,我们可以使用redis锁来保证,具体逻辑如下

    每个实例调用setnx命令插入一条数据,插入成功后返回1的实例执行job,返回0的不执行

    言归正传

    首先我们看下之前的代码逻辑,我这里是整合的Scheduled,自行封装的定时任务,在执行时,没有解决多实例问题

    image-20231105153919796

    那我们的逻辑是,在此段代码执行时加入redis锁,保证执行一次

    1、redis加锁方法封装

    /**
    * 加锁
    * @param key
    * @param timeStamp
    * @return
    */
    public Boolean lock(String key, String timeStamp){
        if (redisTemplate.opsForValue().setIfAbsent(getKey(key), timeStamp)) {
            return true;
        }
        String currentLock = (String) redisTemplate.opsForValue().get(getKey(key));
        if (StringUtils.hasLength(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()) {
            String preLock = (String) redisTemplate.opsForValue().getAndSet(getKey(key), timeStamp);
    
            if (StringUtils.hasLength(preLock) && preLock.equals(currentLock)) {
                return true;
            }
        }
        return false;
    }
    
    /**
    * 解锁
    * @param key
    * @param timeStamp
    */
    public void unLock(String key, String timeStamp){
        try {
            String currentValue = (String) redisTemplate.opsForValue().get(getKey(key));
            if (StringUtils.hasLength(currentValue) && currentValue.equals(timeStamp)) {
                redisTemplate.opsForValue().getOperations().delete(getKey(key));
            }
        } catch (Exception e) {
            log.error("解锁异常");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    2、多实例解决实现逻辑

    public void run() {
        long startTime = System.currentTimeMillis();
        Map<String, Scheduled> scheduledMap = scheduledTaskService.getScheduledMap();
        ScheduledLog scheduledLog = new ScheduledLog();
        Scheduled scheduled = scheduledMap.get(beanName);
        Boolean flag = Boolean.TRUE;
        String timeStamp = String.valueOf(System.currentTimeMillis() + 300L);
        try {
            Boolean lock = redisUtil.lock(redisUtil.getCacheKey(CachePrefixContent.LOCK_PREFIX, beanName), timeStamp);
            if (lock) {
                BaseResult result = BaseResult.ok();
                scheduledLog.setTaskId(scheduled.getTaskId());
                scheduledLog.setExecuteTime(LocalDateTime.now());
                // 执行定时任务处理逻辑
                execute(result);
                if (result.resOk()) {
                    scheduledLog.setExecuteStatus(Boolean.TRUE);
                } else {
                    scheduledLog.setExecuteStatus(Boolean.FALSE);
                }
                scheduledLog.setExecuteDesc(result.getMsg());
                redisUtil.unLock(redisUtil.getCacheKey(CachePrefixContent.LOCK_PREFIX, beanName), timeStamp);
            } else {
                flag = Boolean.FALSE;
            }
        } catch (Exception e) {
            log.error("定时任务:{}执行失败,{}", scheduled.getTaskName(), e);
            scheduledLog.setExecuteStatus(Boolean.FALSE);
            scheduledLog.setExecuteDesc(e.getMessage());
        } finally {
            long endTime = System.currentTimeMillis();
            log.info("【{}】【】【{}ms】", "定时任务", scheduled.getTaskName(), endTime - startTime);
            if (flag) {
                completableFutureService.runAsyncTask(() -> {
                    scheduledLogMapper.insert(scheduledLog);
                });
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    3、效果展示

    每30秒两个示例只有单台节点执行成功

    image-20231105162053036

    结语

    1、以上问题就解决了,快去给你的代码加上吧!

    2、制作不易,一键三连再走吧,您的支持永远是我最大的动力!

  • 相关阅读:
    基于ChatGPT+词向量/词嵌入实现相似商品推荐系统
    代碼隨想錄算法訓練營|第四十五天|1049. 最后一块石头的重量 II、494. 目标和、474.一和零。刷题心得(c++)
    GitHub上架即下架!《分布式系统人人都是架构师》全彩笔记开源
    QJsonValue的学习
    6-图文打造LeeCode算法宝典-数组与排序算法题解
    爬虫与反爬虫技术简介
    PHP 图片的合并,微信小程序码合并,文字合并
    HTTPS 加密工作过程
    Sql Server性能排查和优化懒人攻略
    Linux下redis单机安装、主从搭建及哨兵模式搭建及springboot整合测试
  • 原文地址:https://blog.csdn.net/admin_2022/article/details/134245625