Implementing Distributed Locks in Spring Boot with Redisson

This article explains how Redisson extends Redis to provide a rich set of distributed Java objects and services, walks through integrating Redisson into a Spring Boot project, demonstrates various lock types (reentrant, read‑write, semaphore, latch) with code examples, and details the internal watchdog mechanism that guarantees atomic lock acquisition and automatic renewal.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
Implementing Distributed Locks in Spring Boot with Redisson

What is Redisson

Redisson is a Java in‑memory data grid built on Redis. It provides distributed implementations of common Java objects such as BitSet, Set, Map, List, Queue, BlockingQueue, Deque, Semaphore, Lock, AtomicLong, CountDownLatch, publish/subscribe, Bloom filter, remote service, Spring cache, executor service, live object service and scheduler service. It abstracts Redis connections using Netty and maps native Redis data structures (Hash, List, Set, String, Geo, HyperLogLog) to familiar Java collections.

Netty framework : provides synchronous, asynchronous, streaming and pipelined command execution, Lua script support and result handling.

Base data structures : wraps Redis Hash, List, Set, String, Geo, HyperLogLog as Java Map, List, Set, object bucket, geospatial bucket, etc.

Distributed data structures : adds multimap, local‑cached map, sorted set, scored sorted set, lex sorted set, various queues, Bloom filter, atomic types and others not natively present in Redis.

Distributed locks : implements Lock plus advanced primitives such as MultiLock, ReadWriteLock, FairLock, RedLock, Semaphore, PermitExpirableSemaphore and CountDownLatch.

Node support : can act as an independent node to execute remote tasks for distributed execution and scheduling services.

Official repository: https://github.com/redisson/redisson

Integrating Redisson into a Spring Boot project

Add Maven dependency

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.11.1</version>
</dependency>

Configure Redisson

@Configuration
public class RedissonConfig {
    private static final String REDISSON_PREFIX = "redis://";
    @Value("${spring.redis.host}")
    private String redisHost;
    @Value("${spring.redis.port}")
    private String redisPort;

    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() {
        Config config = new Config();
        // Redis URL must start with redis:// or rediss://
        config.useSingleServer().setAddress(REDISSON_PREFIX + redisHost + ":" + redisPort);
        return Redisson.create(config);
    }
}

Cluster mode example:

Config config = new Config();
config.useClusterServers()
    .setScanInterval(2000) // ms
    .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
    .addNodeAddress("redis://127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);

Redisson distributed lock primitives

Reentrant lock (RLock)

Redisson implements java.util.concurrent.locks.Lock with async, reactive and RxJava2 APIs. Before showing Redisson’s lock, a raw Redis SETNX lock implementation is presented:

List<CategoryDTO> getCategoryTreeWithRedisLock() {
    String uuid = UUID.randomUUID().toString();
    Boolean lock = stringRedisTemplate.opsForValue()
        .setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
    if (lock) {
        log.info("Lock acquired");
        List<CategoryDTO> categoryDTOList = null;
        try {
            String categoryJson = stringRedisTemplate.opsForValue().get(CATEGORY_CACHE);
            if (StringUtils.isBlank(categoryJson)) {
                categoryDTOList = getCategoryTree();
                stringRedisTemplate.opsForValue().set(CATEGORY_CACHE,
                    JSON.toJSONString(categoryDTOList), 5, TimeUnit.MINUTES);
            } else {
                categoryDTOList = JSON.parseArray(categoryJson, CategoryDTO.class);
            }
        } finally {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "return redis.call('del', KEYS[1]) else return 0 end";
            stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList("lock"), uuid);
        }
        return categoryDTOList;
    } else {
        log.info("Lock acquisition failed, retrying...");
        try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { log.error("Redis lock error", e); }
        return tryAgainWithTime();
    }
}

The raw SETNX approach has two pitfalls: (1) lock acquisition and expiration are not atomic, leading to possible dead‑locks; (2) unlocking is unsafe if the lock expires before the Lua unlock script runs.

Redisson lock usage example (reentrant lock with automatic renewal):

public void testLock() {
    RLock rLock = redissonClient.getLock("my-lock");
    rLock.lock(); // default lease time 30 s, watchdog will renew automatically
    try {
        System.out.println("Lock acquired, executing business..." + Thread.currentThread().getId());
        TimeUnit.SECONDS.sleep(20);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("Releasing lock..." + Thread.currentThread().getId());
        rLock.unlock();
    }
}

Redisson starts a watchdog when no explicit lease time is supplied. The watchdog uses a default 30 s timeout and renews the lock every TTL/3 (≈10 s) by scheduling a background task that calls renewExpirationAsync.

Read‑Write lock (RReadWriteLock)

Redisson provides a distributed read‑write lock that implements java.util.concurrent.locks.ReadWriteLock. Write lock is exclusive, read lock is shared.

public String writeValue() {
    RReadWriteLock rwLock = redissonClient.getReadWriteLock("rw-lock");
    RLock lock = rwLock.writeLock();
    try {
        lock.lock();
        String s = UUID.randomUUID().toString();
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        ops.set("writeValue", s);
        TimeUnit.SECONDS.sleep(10);
        return s;
    } catch (InterruptedException e) {
        e.printStackTrace();
        return null;
    } finally {
        lock.unlock();
    }
}

Read lock usage is analogous, calling rwLock.readLock(). Combination scenarios:

read + read: concurrent reads succeed.

write + read: reads block until the write lock is released.

write + write: mutual exclusion.

read + write: write blocks until all reads finish.

Semaphore (RSemaphore)

Distributed semaphore can be used for rate limiting or resource pools. Example of a parking garage with three slots:

public String park() throws InterruptedException {
    RSemaphore park = redissonClient.getSemaphore("park");
    park.acquire(); // occupy a slot
    boolean flag = park.tryAcquire();
    if (flag) {
        // business logic
    } else {
        return "error";
    }
    return "ok=>" + flag;
}

public String go() {
    RSemaphore park = redissonClient.getSemaphore("park");
    park.release(); // free a slot
    return "ok";
}

CountDownLatch (RCountDownLatch)

Distributed latch synchronizes multiple participants. Example simulating five classes that must finish before a gate can be locked:

public String lockDoor() throws InterruptedException {
    RCountDownLatch door = redissonClient.getCountDownLatch("door");
    door.trySetCount(5);
    door.await(); // wait for all classes
    return "Holiday...";
}

public String gogogo(Long id) {
    RCountDownLatch door = redissonClient.getCountDownLatch("door");
    door.countDown(); // one class leaves
    return id + " classes have left...";
}

How Redisson solves raw Redis lock problems

Redisson guarantees atomic lock acquisition and release by executing Lua scripts on the Redis server. If no lease time is provided, a watchdog with a default 30 s timeout is started; every TTL/3 a background task extends the lock’s expiration, preventing accidental expiration while the business thread holds the lock.

Key implementation excerpts:

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    if (ttl == null) return;
    RFuture<RedissonLockEntry> future = subscribe(threadId);
    commandExecutor.syncSubscription(future);
    try {
        while (true) {
            ttl = tryAcquire(leaseTime, unit, threadId);
            if (ttl == null) break;
            if (ttl >= 0) {
                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                if (interruptibly) getEntry(threadId).getLatch().acquire();
                else getEntry(threadId).getLatch().acquireUninterruptibly();
            }
        }
    } finally {
        unsubscribe(future, threadId);
    }
}
private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry old = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (old != null) {
        old.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        renewExpiration();
    }
}

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) return;
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) return;
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) return;
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                if (res) renewExpiration();
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    ee.setTimeout(task);
}

Thus, Redisson’s combination of Lua‑based atomic operations and the watchdog renewal mechanism resolves the two major shortcomings of a naïve Redis SETNX lock.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaRedisSpring BootSemaphoreDistributed LockRedissonWatchdog
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.