How to Implement Distributed Locks with Redisson in Spring Boot Scheduled Tasks

Wrap your @Scheduled method body in a try-with-resources block using RedisLockUtils.tryLock(lockKey) to ensure only one cluster instance executes the job at a time.

When running Spring Boot applications in a clustered environment, @Scheduled tasks execute on every node simultaneously, causing duplicate processing or race conditions. In the ContiNew Admin project, the continew-starter-cache-redisson module provides a lightweight utility—RedisLockUtils—that wraps Redisson’s RLock to implement distributed locks with automatic resource management. This guide demonstrates how to implement distributed locks with Redisson in scheduled tasks using patterns already established in the continew-org/continew-admin codebase.

Understanding the RedisLockUtils Wrapper

The RedisLockUtils class (located in continew-starter-cache-redisson/src/main/java/top/continew/starter/cache/redisson/util/RedisLockUtils.java) encapsulates Redisson’s distributed locking logic and implements AutoCloseable. This design allows developers to use Java’s try-with-resources syntax, ensuring the lock is released automatically—even if the business logic throws an exception.

Key characteristics of the utility:

  • Non-blocking acquisition: tryLock() attempts to acquire the lock immediately without waiting.
  • Auto-release: The lock unlocks automatically when the try block exits.
  • State inspection: The isLocked() method indicates whether the current instance successfully acquired the lock.

Implementing Distributed Locks in Scheduled Jobs

Step-by-Step Pattern

Follow this four-step pattern to protect any @Scheduled method:

  1. Generate a unique lock key that identifies the specific scheduled task (e.g., "Lock:NoticePublishJob").
  2. Acquire the lock using RedisLockUtils.tryLock(lockKey), which returns a lock instance immediately.
  3. Validate lock acquisition by calling isLocked(); if false, another instance holds the lock and the current thread should skip execution.
  4. Execute business logic inside the locked block.

Complete Implementation Example

The NoticePublishJob class in continew-server/src/main/java/top/continew/admin/job/NoticePublishJob.java provides a concrete example. The following adaptation adds distributed lock protection to the publishNoticeWithSchedule method:

package top.continew.admin.job;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import top.continew.starter.cache.redisson.util.RedisLockUtils;

@Slf4j
@Component
@RequiredArgsConstructor
public class NoticePublishJob {

    @Component
    public static class Scheduler {

        @Scheduled(cron = "0 * * * * ?")
        @Transactional(rollbackFor = Exception.class)
        public void publishNoticeWithSchedule() {
            String lockKey = "Lock:NoticePublishJob";
            try (RedisLockUtils lock = RedisLockUtils.tryLock(lockKey)) {
                if (!lock.isLocked()) {
                    log.warn("Another instance is already publishing notices – skipping.");
                    return;
                }
                log.info("定时任务 [公告发布] 开始执行。");
                publishNotice();   // Original business logic
                log.info("定时任务 [公告发布] 执行结束。");
            }
        }
    }
    
    // Additional class methods...
}

Critical implementation details:

  • The lockKey uses a "Lock:" prefix followed by the job name to avoid collisions with other Redis keys.
  • The try block ensures the Redisson lock releases even if publishNotice() throws an exception.
  • The early return pattern (if (!lock.isLocked())) prevents duplicate execution without blocking the thread.

Reference Implementation in Service Layer

For components outside scheduled jobs, the same pattern applies. In continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java, the createParentDir method demonstrates dynamic lock key construction for resource-specific locking:

// Inside FileServiceImpl.createParentDir
String lockKey = StrUtil.format("Lock:{}:{}", storage.getCode(), parentPath);
try (RedisLockUtils lock = RedisLockUtils.tryLock(lockKey)) {
    if (!lock.isLocked()) {
        return; // 获取锁失败,直接返回
    }
    // Critical section: create directory structure exclusively
}

This example shows that lock keys can incorporate runtime variables (storage code and path) to allow parallel processing of unrelated resources while preventing conflicts on identical paths.

Best Practices for Lock Key Design

Designing effective lock keys ensures both safety and performance in distributed systems:

  • Use a consistent prefix: Start keys with "Lock:" to namespace them separately from cache entries and business data.
  • Include business identifiers: For dynamic resources, incorporate unique IDs (like storage.getCode() and parentPath) to prevent global bottlenecks while maintaining safety for specific resources.
  • Keep keys short but descriptive: Excessively long keys increase Redis memory usage and network overhead without adding value.

Summary

  • RedisLockUtils.tryLock() provides non-blocking, auto-releasing distributed locks via Redisson.
  • Always check isLocked() before executing protected logic to handle lock contention gracefully.
  • The try-with-resources pattern guarantees lock release, preventing deadlocks during application crashes or exceptions.
  • ContiNew Admin implements this pattern in NoticePublishJob for scheduled tasks and FileServiceImpl for service-layer synchronization.

Frequently Asked Questions

What happens if the application crashes while holding a Redisson lock?

Redisson locks typically use a watchdog mechanism with a default lease time. If the JVM crashes or the instance becomes unavailable, Redis automatically expires the lock key after the lease duration expires, allowing other instances to acquire the lock. The RedisLockUtils wrapper maintains this safety mechanism, preventing permanent deadlocks.

Can I use RedisLockUtils for non-scheduled business logic?

Yes. While this guide focuses on how to implement distributed locks with Redisson in scheduled tasks, the FileServiceImpl.createParentDir method demonstrates identical usage in standard service methods. Any code path requiring cluster-wide exclusivity—such as database migrations, file system operations, or inventory updates—can use this utility.

How is RedisLockUtils different from using RedissonClient directly?

RedisLockUtils abstracts the boilerplate of obtaining an RLock, checking availability, and ensuring release. By implementing AutoCloseable, it eliminates manual unlock() calls and reduces the risk of deadlocks compared to raw Redisson client usage, where forgotten finally blocks can leave locks hanging indefinitely.

Does the lock block other instances indefinitely?

No. RedisLockUtils.tryLock() attempts to acquire the lock immediately and returns either a valid lock instance or an empty holder. It does not block threads waiting for the lock to become available, making it suitable for scheduled tasks where skipping an execution is preferable to delaying it indefinitely.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →