Why Use Distributed Locks When Idempotent APIs Already Prevent Duplicate Submissions?

The article explains that idempotency guarantees result correctness but does not control concurrent execution order, while distributed locks serialize access; through code examples and scenarios like stock deduction and money transfer it shows why both mechanisms are complementary and often needed together.

Programmer XiaoFu
Programmer XiaoFu
Programmer XiaoFu
Why Use Distributed Locks When Idempotent APIs Already Prevent Duplicate Submissions?

Idempotency vs. Distributed Lock

The author observes that many developers mistakenly think implementing idempotency alone is sufficient to prevent duplicate submissions, treating distributed locks as unnecessary.

What Idempotency Protects

Idempotency ensures result correctness : executing the same request once or many times yields the same effect. The most common implementation uses an idempotent token: the client obtains a unique token, sends it with the request, and the server checks whether the token has already been consumed before proceeding.

public String submitOrder(String idempotentToken, OrderRequest request) {
    // 1. Check if token has been consumed
    boolean exists = redis.hasKey("idempotent:" + idempotentToken);
    if (exists) {
        return "重复提交,请勿重复操作";
    }
    // 2. Mark token as consumed
    redis.opsForValue().set("idempotent:" + idempotentToken, "1", 30, TimeUnit.MINUTES);
    // 3. Execute business logic
    orderService.createOrder(request);
    return "下单成功";
}

At low concurrency this works, but under high concurrency two requests may read the token before either marks it, both pass the check, and both create an order – a classic check‑execute race condition.

Can Redis Atomic Operations Solve It?

Combining the check and mark into a single atomic SETNX operation eliminates the race for simple duplicate‑submission scenarios:

public String submitOrder(String idempotentToken, OrderRequest request) {
    // SETNX: set if absent, atomic
    Boolean success = redis.opsForValue()
        .setIfAbsent("idempotent:" + idempotentToken, "1", 30, TimeUnit.MINUTES);
    if (!success) {
        return "重复提交,请勿重复操作";
    }
    // Execute business logic
    orderService.createOrder(request);
    return "下单成功";
}

While this works for duplicate‑submission protection, it is essentially a minimal distributed lock and does not replace a full lock in more complex business scenarios.

Scenario 1: Stock Deduction Requires Mutual Exclusion

When many users attempt to buy the last item, each request is distinct and carries no idempotent token. Without a lock, overselling occurs.

public void deductStock(String skuId, int quantity) {
    int stock = stockMapper.getStock(skuId);
    if (stock < quantity) {
        throw new BizException("库存不足");
    }
    stockMapper.deductStock(skuId, quantity);
}

Adding a distributed lock guarantees that only one thread can perform the "check‑then‑deduct" sequence at a time:

public void deductStock(String skuId, int quantity) {
    RLock lock = redisson.getLock("lock:stock:" + skuId);
    lock.lock();
    try {
        int stock = stockMapper.getStock(skuId);
        if (stock < quantity) {
            throw new BizException("库存不足");
        }
        stockMapper.deductStock(skuId, quantity);
    } finally {
        lock.unlock();
    }
}

Scenario 2: Concurrent Transfers Cause Lost Updates

Two independent transfers to the same account can overwrite each other, leading to missing money. Idempotent tokens cannot detect this because the operations are different.

public void transfer(String from, String to, BigDecimal amount) {
    // 1. Query balance
    BigDecimal balance = accountMapper.getBalance(to);
    // 2. Add money
    accountMapper.updateBalance(to, balance.add(amount));
}

Without serialization, the final balance may be incorrect. A distributed lock around the account update prevents the write‑overwrite problem.

Scenario 3: Local synchronized Is Insufficient in a Microservice Cluster

Using Java's synchronized only protects threads within a single JVM. In a deployment with three service instances behind Nginx, each instance acquires its own lock, so the race condition persists.

Distributed locks solve the cross‑JVM mutual‑exclusion problem, making multiple machines behave as if they share a single lock.

Combining Distributed Lock and Idempotency

A robust order‑submission endpoint first acquires a distributed lock, then performs an idempotent check inside the lock. This protects both the execution order and the final result, and also handles lock expiration scenarios where a long‑running business operation might release the lock early.

public String submitOrder(String idempotentToken, OrderRequest request) {
    // First layer: distributed lock – only one request enters at a time
    RLock lock = redisson.getLock("lock:order:" + idempotentToken);
    boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
    if (!acquired) {
        return "系统繁忙,请稍后重试";
    }
    try {
        // Second layer: idempotent check – ensure no duplicate execution
        Boolean firstTime = redis.opsForValue()
            .setIfAbsent("idempotent:" + idempotentToken, "1", 30, TimeUnit.MINUTES);
        if (!firstTime) {
            return "订单已提交,请勿重复操作";
        }
        // Execute business logic
        orderService.createOrder(request);
        return "下单成功";
    } finally {
        lock.unlock();
    }
}

Because the lock has a timeout, the idempotent check inside the lock prevents a retry request from re‑executing the business logic after the lock expires.

Final Takeaway

Idempotency safeguards the result —the same outcome regardless of how many times the operation runs—while a distributed lock safeguards the process —ensuring only one thread manipulates shared state at a time. Both mechanisms address different aspects of concurrency and are often required together for reliable, high‑traffic services.

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.

Javamicroservicesredisdistributed lockidempotencyrace condition
Programmer XiaoFu
Written by

Programmer XiaoFu

xiaofucode.com – a programmer learning guide driven by the pursuit of profit

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.