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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer XiaoFu
xiaofucode.com – a programmer learning guide driven by the pursuit of profit
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
