全栈小白的gravatar头像
全栈小白 2023-11-06 14:28:03
SpringBoot整合定时任务遇到的多实例问题

唠嗑部分

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

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

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

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,自行封装的定时任务,在执行时,没有解决多实例问题

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

那我们的逻辑是,在此段代码执行时加入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("解锁异常");
    }
}

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);
            });
        }
    }
}

3、效果展示

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

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

结语

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

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


打赏

已有1人打赏

最代码官方的gravatar头像
最近浏览
暂无贡献等级
Dominick  LV14 5月7日
唐僧洗头爱飘柔  LV22 5月6日
fellowfun  LV12 4月28日
最代码灬丿正牌  LV16 3月21日
wumingming  LV1 3月19日
rickson  LV5 3月8日
java小书童  LV17 1月15日
best2018  LV46 1月4日
fff2003  LV6 1月2日
顶部 客服 微信二维码 底部
>扫描二维码关注最代码为好友扫描二维码关注最代码为好友