Designing a High‑Availability Inventory System: Stock Pre‑allocation, Idempotency, Concurrency Control, and Rollback Strategies
The article describes how JD Daojia's inventory system serves millions of stores by using health‑monitoring platforms, pre‑allocation policies, idempotent order processing, distributed locks, and rollback mechanisms to ensure stability, prevent overselling, and handle high‑concurrency scenarios such as flash‑sales.
After more than two years of online testing and technical iteration, JD Daojia's inventory system now supports tens of thousands of merchants and hundreds of thousands of stores. The article explains how the system achieves stability and high availability through a powerful underlying service platform that provides 24/7 health monitoring of applications, JVMs, Docker containers, and physical machines.
When to Pre‑occupy (or Deduct) Stock
How should the system handle limited stock when a user places an order?
Three typical approaches are discussed:
Pre‑occupy stock when the user adds the item to the cart – only one user could add the product.
Pre‑occupy stock when the user submits the order – only one user could successfully submit.
Pre‑occupy stock after order submission and payment – all users can create orders, but only one can pay.
JD Daojia adopts the second approach (pre‑occupy at order submission) because it balances user experience and inventory utilization.
Duplicate Order Submissions
Repeated order submissions can cause severe stock over‑deduction.
Three scenarios are identified:
Benign user clicks the submit button multiple times due to missing response.
Malicious users flood the order‑submission API.
Order‑submission system retries after a timeout.
Corresponding countermeasures:
Disable the submit button after the first click.
Use a token mechanism: each checkout page receives a unique token that must be validated and used only once.
Ensure idempotent inventory APIs by passing the order ID and acquiring a distributed lock.
Example idempotent code (Redis‑based lock):
int ret = redis.incr(orderId);
redis.expire(orderId, 5, TimeUnit.MINUTES);
if (ret == 1) {
// first time processing this order
boolean alreadySuccess = alreadySuccessDoOrder(orderProductRequest);
if (!alreadySuccess) {
doOrder(orderProductRequest);
}
} else {
return "Operation failed: duplicate submission";
}Inventory Rollback Mechanisms
Rollback is needed in several cases:
User cancels before payment.
User cancels after payment.
Risk‑control system cancels the order.
Partial failure in a distributed transaction (e.g., points, coupons, and stock services).
For the first three scenarios, the order‑cancellation service publishes an MQ message that each downstream system consumes to release stock. For the fourth scenario, the order‑submission service must actively invoke rollback APIs of the stock and coupon services, which must be idempotent. If the submission service crashes during rollback, each service can run a worker that periodically scans orders older than a safe window (e.g., 40 minutes) and performs self‑rollback.
Concurrent Stock Deduction for a Single Item
How to safely deduct stock when many users try to buy the same product simultaneously?
Two illustrative code snippets are provided.
Snippet 1 uses a Java synchronized block to serialize all requests, which limits throughput.
synchronized(this) {
long stockNum = getProductStockNum(productId);
if (stockNum > requestBuyNum) {
int ret = updateSQL("update stock_main set stockNum=stockNum-" + requestBuyNum + " where productId=" + productId);
if (ret == 1) {
return "Deduction succeeded";
} else {
return "Deduction failed";
}
}
}Snippet 2 moves the concurrency control into the SQL WHERE clause, allowing the database to handle atomicity without explicit locking.
int ret = updateSQL("update stock_main set stockNum=stockNum-" + requestBuyNum + " where productId=" + productId + " and stockNum>=" + requestBuyNum);
if (ret == 1) {
return "Deduction succeeded";
} else {
return "Deduction failed";
}For flash‑sale (seckill) scenarios, an additional in‑memory counter can reject a configurable percentage of requests before hitting the DAO layer, reducing database pressure.
public class SeckillServiceImpl {
private long count = 0;
public String buy(User user, int productId, int productNum) throws InterruptedException {
count++;
if (count % 2 == 1) {
Thread.sleep(1000);
return "Purchase failed";
} else {
return doBuy(user, productId, productNum);
}
}
}To prevent a single user from buying the same item multiple times, a Redis key per user‑product pair is incremented; only the first increment succeeds.
int tmp = redis.incr(user.getUid() + productId);
if (tmp == 1) {
redis.expire(user.getUid() + productId, 3600);
doBuy1(user, productId, productNum);
} else {
return "Purchase failed";
}Recruitment Note
The article concludes with an invitation to join JD Daojia's engineering team (Java senior engineers in Beijing and Shanghai) and a link to the technical public account for more articles.
Dada Group Technology
Sharing insights and experiences from Dada Group's R&D department on product refinement and technology advancement, connecting with fellow geeks to exchange ideas and grow together.
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.