Multiple instance problems encountered by SpringBoot integrating scheduled tasks

Talk part

That’s right. I improved the logging of scheduled tasks a few days ago. Today I switched servers, deployed one more node, and used nginx load balancing. However, when I checked the logs, I found the following situation.

image-20231105153728831

That’s bad, the legendary multi-instance problem has appeared. Today we will talk about how to do scheduled tasks in actual project operations. First, let’s look at the following issues

1. What are scheduled tasks, and what practical problems can they help us solve?

As the name implies, a scheduled task is to let the program execute a certain piece of code at a specified time. For example, send a good morning blessing to your girlfriend at 8 o’clock every day.

So what problems can it solve for us in development?

In actual development, there are many scenarios that require scheduled tasks, such as daily synchronization of data, cache warm-up, regular cleaning of log files, regular statistical rankings…

2. Which scenarios in the actual project require the use of scheduled tasks?

Requirement 1: The product manager requires the system to implement the hot search list within 3 days and update the data at 0 o’clock every day.

Requirement 2: The system needs to rely on data from a third-party system, and the request concurrency is large, and the third-party data is updated daily.

Requirement 3: The system generates a large number of operation logs every day, and the product manager requires that only one month’s worth of data be retained.

Requirement 4: For the system home page data, the maximum concurrency is between 9 and 12 o’clock every day, and the cache needs to be warmed up regularly.

All the above requirements can be realized using scheduled tasks.

3. What are the recommended scheduled task components?

Spring integrates Scheduled, which is lightweight and easy to use, without UI display.

xxl-Job, xxl is the pinyin beginning of the name of Xu Xueli, the developer of xxl-job, which is mainly used to process distributed scheduled tasks. It is mainly composed of a dispatch center and executors, and has a good UI interface.

Elastic-Job, Elastic-Job is a distributed task scheduling framework launched by Dangdang. It is used to solve the coordination and scheduling problem of distributed tasks and ensure that tasks are executed without duplication or omission; there is no UI display and requires the support of the distributed coordination tool Zookeeper.

4. How to implement distributed scheduled tasks and avoid multi-instance problems?

First, let’s talk about what the multi-instance problem is. In our project development, when we deploy scheduled tasks, we usually only deploy one machine. If we deploy multiple machines, the same task will be executed multiple times (each machine will be executed without affecting each other), then if there are some scheduled tasks to calculate the income for the user, the income will be calculated for the user regularly every day. If multiple units are deployed, the same user will repeatedly calculate the income multiple times, then it will be Barbie Q. Then if If only one device is deployed, there will be a single point of failure and availability cannot be guaranteed.

The xxl-job and elastic-Job mentioned above can solve the multi-instance problem and ensure that tasks are executed without duplication or omission.

Then we use Spring’s own Scheduled. How to avoid the multi-instance problem? We can use redis lock to ensure it. The specific logic is as follows

Each instance calls the setnx command to insert a piece of data. After the insertion is successful, the instance that returns 1 will execute the job, and the instance that returns 0 will not execute it.

Back to business

First, let’s take a look at the previous code logic. What I have here is integrated Scheduled, a self-encapsulated scheduled task. During execution, it does not solve the problem of multiple instances.

image-20231105153919796

Then our logic is to add a redis lock when this code is executed to ensure it is executed once.

1. Redis locking method encapsulation

/**
* Lock
* @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) & amp; & amp; Long.parseLong(currentLock) < System.currentTimeMillis()) {<!-- -->
        String preLock = (String) redisTemplate.opsForValue().getAndSet(getKey(key), timeStamp);

        if (StringUtils.hasLength(preLock) & amp; & amp; preLock.equals(currentLock)) {<!-- -->
            return true;
        }
    }
    return false;
}

/**
* Unlock
* @param key
* @param timeStamp
*/
public void unLock(String key, String timeStamp){<!-- -->
    try {<!-- -->
        String currentValue = (String) redisTemplate.opsForValue().get(getKey(key));
        if (StringUtils.hasLength(currentValue) & amp; & amp; currentValue.equals(timeStamp)) {<!-- -->
            redisTemplate.opsForValue().getOperations().delete(getKey(key));
        }
    } catch (Exception e) {<!-- -->
        log.error("Unlocking exception");
    }
}

2. Multiple instances to solve implementation logic

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 scheduled task processing logic
            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 task: {} execution failed, {}", scheduled.getTaskName(), e);
        scheduledLog.setExecuteStatus(Boolean.FALSE);
        scheduledLog.setExecuteDesc(e.getMessage());
    } finally {<!-- -->
        long endTime = System.currentTimeMillis();
        log.info("[{}][][{}ms]", "Scheduled Task", scheduled.getTaskName(), endTime - startTime);
        if (flag) {<!-- -->
            completableFutureService.runAsyncTask(() -> {<!-- -->
                scheduledLogMapper.insert(scheduledLog);
            });
        }
    }
}

3. Effect display

Only a single node executes two examples every 30 seconds successfully.

image-20231105162053036

Conclusion

1. The above problem is solved, go and add it to your code!

2. It’s not easy to make. Just click three times and then go. Your support will always be my biggest motivation!