java

Spring Boot Distributed Locking with Redisson: Prevent Race Conditions in Microservices

Learn Spring Boot distributed locking with Redisson to stop race conditions, prevent overselling, and secure payments across microservices.

Spring Boot Distributed Locking with Redisson: Prevent Race Conditions in Microservices

I once spent a weekend debugging an e‑commerce inventory system that kept overselling the same pair of shoes. The logs showed two API requests arriving at the exact millisecond. Both read “stock = 1”, both validated, both decremented. The final stock was -1. The customer was furious, the database was inconsistent, and I was staring at a problem that a simple synchronized block couldn’t solve because the two requests were handled by two different Kubernetes pods. That night I learned what every microservice developer eventually faces: distributed locking isn’t a luxury, it’s a necessity.

We’re going to build a production‑ready distributed locking system using Spring Boot and Redisson, which implements the RedLock algorithm. No theory without practice – you’ll see real code that handles inventory, scheduled jobs, and idempotent payments. By the end you’ll know exactly how to prevent race conditions across any number of service instances.

Let me show you what I mean by starting with the simplest possible scenario. Imagine you have two instances of a payment service, both processing a webhook for the same order. Both check if the payment has already been processed, both see “not yet processed”, both apply the charge. The customer gets billed twice. How do you guarantee that only one instance ever processes a given order? The answer is a distributed lock.

The core problem: crossing JVM boundaries

In a single Java process you could use synchronized or ReentrantLock. But a lock on one pod cannot block code running on another pod. You need a shared, external coordinator that both pods can see. Redis is a natural choice because it’s fast, simple, and already used as a cache or session store in most microservice architectures.

Redisson wraps Redis with robust Java locks that can be reentrant, fair, or even read/write. It also implements the RedLock algorithm, which uses multiple independent Redis nodes to stay available even if some nodes fail.

Setting up Redisson with Spring Boot

First, add the Redisson Spring Boot Starter to your pom.xml:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.0</version>
</dependency>

Then configure the client. I like to keep the Redisson configuration in a separate YAML file:

singleServerConfig:
  address: "redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}"
  connectionPoolSize: 20
  connectTimeout: 3000
  timeout: 3000
codec: !<org.redisson.codec.JsonJacksonCodec> {}

And load it in a configuration bean:

@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() throws IOException {
    Config config = Config.fromYAML(
        new ClassPathResource("redisson.yaml").getInputStream()
    );
    return Redisson.create(config);
}

Now you have a RedissonClient that you can inject anywhere.

Acquiring your first distributed lock

Let’s protect the inventory reservation method that got me into trouble. I’ll use a lock with a key that uniquely identifies the resource (e.g., stock:itemId).

@Autowired
private RedissonClient redisson;

public void reserveItem(String itemId, int quantity) {
    RLock lock = redisson.getLock("stock:" + itemId);
    try {
        // Wait up to 5 seconds, hold lock for 30 seconds maximum
        if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
            int currentStock = inventoryRepository.getStock(itemId);
            if (currentStock >= quantity) {
                inventoryRepository.updateStock(itemId, currentStock - quantity);
            }
        } else {
            throw new ServiceException("Could not acquire lock, try again later");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

Notice the tryLock with a wait time and a lease time. The wait time says “I’m willing to block for up to 5 seconds trying to get the lock.” The lease time says “give up the lock automatically after 30 seconds even if I crash.” The latter prevents a deadlock if the service dies while holding the lock.

But what if the operation takes longer than 30 seconds? You don’t want the lock to expire while your database transaction is still running. Redisson has a built‑in watchdog that automatically extends the lease time while the lock‑holding thread is alive. You enable it by omitting the lease time (or passing -1):

lock.lock(5, TimeUnit.SECONDS); // no lease time = watchdog enabled

By default the watchdog refreshes every 10 seconds (one third of the default 30‑second lease). If the thread dies, the lock eventually expires. This is much safer than guessing a static TTL.

Reentrant locks: the same thread can lock again

A reentrant lock allows the same thread to acquire the same lock multiple times without blocking itself. This is important when a service method calls another method that also tries to acquire the same lock.

RLock lock = redisson.getLock("user:123");

public void outerMethod() {
    lock.lock(10, TimeUnit.SECONDS);
    try {
        innerMethod(); // this also tries to lock the same key
    } finally {
        lock.unlock();
    }
}

public void innerMethod() {
    lock.lock(10, TimeUnit.SECONDS);
    try {
        // do work
    } finally {
        lock.unlock();
    }
}

Without reentrancy, innerMethod would deadlock because it tries to acquire a lock already held by the same thread. Redisson’s RLock is reentrant by default, so this works flawlessly.

Fair locks: respecting order

Sometimes you want requests to be processed in the order they arrived. A fair lock gives the lock to the oldest waiting thread. Redisson supports this with RedissonClient.getFairLock(key). Use it when your application logic expects strict FIFO ordering – for example, a ticket booking system where the first to queue should get the seat.

RLock fairLock = redisson.getFairLock("booking:seat123");
fairLock.lock(5, 10, TimeUnit.SECONDS);

Fair locks add a performance penalty because each lock request touches Redis more often, but they prevent starvation in contention‑heavy scenarios.

ReadWrite locks for shared resources

If you have a resource that is read frequently but written rarely, you can improve concurrency with a read/write lock. Multiple readers can hold the read lock simultaneously, but a write lock is exclusive.

RReadWriteLock rwLock = redisson.getReadWriteLock("config:global");
RLock readLock = rwLock.readLock();
RLock writeLock = rwLock.writeLock();

// Reading (many instances can do this concurrently)
readLock.lock(10, TimeUnit.SECONDS);
try {
    String config = configRepository.getGlobalConfig();
    // use config
} finally {
    readLock.unlock();
}

// Writing (only one instance at a time)
writeLock.lock(10, TimeUnit.SECONDS);
try {
    configRepository.updateGlobalConfig(newValue);
} finally {
    writeLock.unlock();
}

This pattern is excellent for session configuration or feature flags that are cached in memory.

Handling lock exceptions and failure scenarios

No lock system is perfect. Redis nodes can go down, network partitions can happen. You need to decide how your application behaves when a lock cannot be acquired.

One common pattern is to fail fast: if you can’t acquire the lock within a short timeout, throw an exception and let the client retry. Another is to degrade gracefully – for example, fall back to a local lock if the distributed lock is unavailable (though this increases risk of race conditions).

I always set a reasonable wait timeout (e.g., 2 seconds) and log a warning when a lock is not acquired. This gives me visibility into contention.

For critical workflows like payment processing, I also add a database‑level idempotency key as a second layer of protection. The lock prevents concurrent execution; the idempotency key prevents duplicate state changes if the lock system fails.

Testing distributed locks with Testcontainers

You can’t test distributed locking with an embedded Redis. I use Testcontainers to spin up a real Redis instance in tests.

@Testcontainers
class InventoryServiceTest {

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7.2")
            .withExposedPorts(6379);

    @DynamicPropertySource
    static void configureRedis(DynamicPropertyRegistry registry) {
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
    }

    @Autowired
    private InventoryService inventoryService;

    @Test
    void testConcurrentReservations() throws Exception {
        // Use a CountDownLatch to simulate simultaneous requests
        int threadCount = 10;
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);
        AtomicInteger successCount = new AtomicInteger(0);

        // Assume stock is 5
        for (int i = 0; i < threadCount; i++) {
            executor.submit(() -> {
                try {
                    inventoryService.reserveItem("item1", 1);
                    successCount.incrementAndGet();
                } catch (Exception e) {
                    // lock not acquired – expected
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await(10, TimeUnit.SECONDS);
        assertThat(successCount.get()).isEqualTo(5);
        assertThat(inventoryService.getStock("item1")).isZero();
    }
}

This test verifies that exactly five reservations succeed and the stock ends up at zero, no matter how many threads race.

Comparison with other distributed locking approaches

Redisson is my go‑to, but you might encounter alternatives:

  • ShedLock focuses on scheduled tasks. It uses a simple database table or Redis entry to prevent duplicate job execution. It lacks reentrancy and read/write locks.
  • ZooKeeper provides sequential and ephemeral nodes for locks. It’s more consistent (CP) than Redis’s eventual consistency, but it adds operational complexity and latency.
  • Database‑based locks (e.g., SELECT ... FOR UPDATE or advisory locks in PostgreSQL) work well inside a single database transaction, but they don’t scale across microservices that use different databases.

For most microservice use cases, Redisson strikes the right balance between simplicity, performance, and feature set.

Real‑world use cases I’ve implemented

  • Idempotent payment webhooks: Each payment has a unique idempotencyKey. The webhook handler first tries to acquire a lock on that key. If the lock is acquired, it processes the webhook and updates the database. If the lock is not acquired, it knows another instance is already handling that webhook and simply returns “accepted”.
  • Scheduled job coordination: A job that runs every five minutes and sends summary emails to users. Without locking, all four replicas would send four identical emails. The job acquires a lock on job:email-summary and only the instance holding the lock runs the job.
  • Distributed rate limiter: Using Redisson’s RRateLimiter to allow only N requests per second across all service instances, protecting a downstream API from overload.
  • Distributed semaphore for finite resources: For example, limiting concurrent database writes to a table that has a row‑level lock contention issue.

Each of these patterns follows the same core: acquire a lock on a well‑defined key, do the work, release the lock.

Watchdog pitfalls and manual extension

The watchdog is great, but it can mask slow operations. I’ve seen developers rely on it to cover database queries that take minutes. That’s dangerous. If your operation consistently takes longer than the default lease time, set a proper lease time based on your worst‑case execution time. The watchdog should be a safety net, not a design crutch.

If you need manual lease extension, you can call lock.lock(leaseTime, timeUnit) repeatedly, but that’s rarely necessary.

Final thoughts on distributed locking

Distributed locking is not a silver bullet. It adds latency, consumes Redis resources, and can become a single point of failure if not configured correctly (use multiple Redis nodes for RedLock). Always measure the cost. For many operations, an idempotent service that can safely retry is simpler and more robust than adding explicit locks.

But when you absolutely need mutual exclusion across replicas – for inventory, payments, or leader election – distributed locking with Redisson and Spring Boot is a proven, reliable solution. Start with the simple tryLock, enable the watchdog, test with Testcontainers, and monitor your lock acquisition times.

If this walkthrough helped you understand how to protect your microservices from race conditions, I’d appreciate it if you like this article, share it with a colleague who’s still debugging weekend oversells, and comment with your own story of a race condition that kept you up at night. I read every comment, and your feedback helps me write better content for the community.

Now go clean up that inventory logic.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: Spring Boot, Redisson, distributed locking, race conditions, microservices



Similar Posts
Blog Image
Master Virtual Threads in Spring Boot 3.2: Complete Project Loom Implementation Guide with Performance Benchmarks

Master Spring Boot 3.2 Virtual Threads with Project Loom integration. Learn implementation, performance optimization, and real-world applications. Start building scalable apps today!

Blog Image
Virtual Threads with Spring Boot 3: Build Lightning-Fast Event-Driven Systems Using Apache Kafka

Learn to build scalable event-driven systems using Java Virtual Threads, Apache Kafka, and Spring Boot 3. Master high-performance concurrent programming patterns.

Blog Image
Building Event-Driven Microservices: Apache Kafka and Spring Cloud Stream Integration Guide

Learn to integrate Apache Kafka with Spring Cloud Stream for building scalable event-driven microservices. Discover reactive patterns, real-time processing, and enterprise architecture best practices.

Blog Image
Complete Spring Boot Microservices Distributed Tracing Guide with OpenTelemetry and Jaeger Implementation

Learn to implement distributed tracing in Spring Boot microservices using OpenTelemetry and Jaeger. Master automatic instrumentation, custom spans, and performance monitoring for better observability.

Blog Image
Apache Kafka Spring Boot Integration Guide: Building Scalable Event-Driven Microservices Architecture

Learn how to integrate Apache Kafka with Spring Boot for scalable event-driven microservices. Build robust messaging systems with auto-configuration and real-time processing.

Blog Image
Master Advanced Spring Boot Caching Strategies with Redis and Cache-Aside Pattern Implementation

Learn advanced Spring Boot caching with Redis and cache-aside patterns. Boost app performance, implement distributed caching, and master cache strategies. Complete guide with examples.