← All tips

Prevent Concurrent Spring Scheduled Task Execution

🤖

Curated by Jepoy  ·  AI-Generated Content

This article was autonomously generated by an AI pipeline designed and built by Jepoy. The author created the system, prompts, and infrastructure that produces this content — not the article itself. Content is intended for educational purposes and may contain inaccuracies. Always verify technical details before applying in production.

Prevent Concurrent Spring Scheduled Task Execution

As a Java developer working with Spring Boot, you’ve likely encountered the challenge of concurrent execution for scheduled tasks, especially in distributed environments. Imagine a scenario where multiple instances of your application, tasked with processing a critical batch of orders, could independently pick up and process the same set of new orders. This overlap can lead to duplicate processing, data inconsistencies, and wasted resources. A robust solution to prevent such race conditions is to implement a distributed lock, and Redis is an excellent, widely adopted choice for this purpose.

Leveraging Spring Boot’s @Scheduled annotation alongside a Redis-based distributed locking mechanism provides a powerful and straightforward way to ensure your tasks run serially across instances. The core principle involves acquiring an exclusive lock before your task logic begins and releasing it upon completion. This guarantees that only one application instance can execute the critical section of the scheduled job at any given time. The spring-boot-starter-data-redis and spring-integration-redis dependencies are essential to enable this pattern.

Here’s a practical example illustrating the integration using RedisLockRegistry from Spring Integration. This component offers a convenient abstraction for managing distributed locks with Redis. The obtain() method acquires a lock instance tied to a specific key, and tryLock() attempts to secure it within a specified timeout.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.redis.util.RedisLockRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

@Configuration
@EnableScheduling
public class ScheduledTaskWithDistributedLock {

    private static final String ORDER_PROCESSING_LOCK_KEY = "distributed:lock:order-processing";

    private final RedisLockRegistry lockRegistry;

    @Autowired
    public ScheduledTaskWithDistributedLock(RedisLockRegistry lockRegistry) {
        this.lockRegistry = lockRegistry;
    }

    @Scheduled(cron = "0 0 1 * * ?") // Run every day at 1 AM
    public void processNewOrders() {
        Lock lock = lockRegistry.obtain(ORDER_PROCESSING_LOCK_KEY);
        boolean acquired = false;
        try {
            // Attempt to acquire the lock, waiting for a maximum of 30 seconds
            acquired = lock.tryLock(30, TimeUnit.SECONDS);
            if (acquired) {
                System.out.println(String.format("[%s] Acquired lock. Initiating order processing...", Thread.currentThread().getName()));
                // --- Your critical order processing logic goes here ---
                // For demonstration, simulate a task that takes 15 seconds
                Thread.sleep(15000);
                System.out.println(String.format("[%s] Order processing completed.", Thread.currentThread().getName()));
                // --- End of critical logic ---
            } else {
                System.out.println(String.format("[%s] Could not acquire lock. Another instance is likely processing orders. Skipping execution.", Thread.currentThread().getName()));
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println(String.format("[%s] Task interrupted during lock acquisition or execution.", Thread.currentThread().getName()));
        } finally {
            if (acquired) {
                lock.unlock();
                System.out.println(String.format("[%s] Released lock.", Thread.currentThread().getName()));
            }
        }
    }
}

A crucial consideration when using distributed locks is the lock’s time-to-live (TTL) and the potential for deadlock. If your scheduled task consistently exceeds the lock’s TTL, or if an application instance holding the lock crashes ungracefully, the lock might remain uncleared. This can prevent any instance from acquiring the lock and executing the task thereafter. To mitigate this, ensure your TTL is set appropriately for your task’s expected duration, and for very long-running tasks, investigate strategies like lock renewal mechanisms or a separate watchdog process.