Preventing Product Overselling in High‑Concurrency E‑Commerce Systems
To prevent overselling during flash sales, the article explains how non‑atomic database updates cause negative stock and presents solutions such as optimistic DB locking, Redis Lua atomic deductions, Redisson distributed locks, transactional message queues, and pre‑deduction with rate limiting, recommending a combined approach that achieved 120 000 QPS with zero oversell.
Introduction
During a flash‑sale the distributed system was overwhelmed, causing the stock count to become negative and leading to overselling.
Why overselling happens
Database non‑atomic check‑update
Concurrent requests read the same stock value (>0) and then update it, so the check and update are not atomic.
Root cause: the query and update are separate statements, allowing multiple transactions to pass the stock>0 condition.
Solutions
1. Database optimistic lock
Use a version column to ensure only one transaction updates the row.
public boolean deductStock(Long productId) {
Product product = productDao.selectForUpdate(productId);
if (product.getStock() <= 0) return false;
int affected = productDao.updateWithVersion(
productId,
product.getVersion(),
product.getStock() - 1
);
return affected > 0;
}Advantages: no extra middleware; simple to implement.
Disadvantages: high DB load under heavy concurrency; possible many update failures.
2. Redis atomic operation
Execute stock deduction in a Lua script to guarantee atomicity.
String lua = "if redis.call('get', KEYS[1]) >= ARGV[1] then " +
"return redis.call('decrby', KEYS[1], ARGV[1]) " +
"else return -1 end";
public boolean preDeduct(String itemId, int count) {
RedisScript
script = new DefaultRedisScript<>(lua, Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(itemId), count);
return result != null && result >= 0;
}Performance: single‑node QPS 500 (DB) vs 80 000 (Redis).
3. Distributed lock (Redisson)
Lock at the product level to serialize critical sections.
RLock lock = redisson.getLock("stock_lock:" + productId);
try {
if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {
// execute stock operation
}
} finally {
lock.unlock();
}4. Message‑queue peak‑shaving (RocketMQ)
Use transactional messages to decouple stock deduction from the request path.
TransactionMQProducer producer = new TransactionMQProducer("stock_group");
producer.setExecutor(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg) {
// deduct DB stock
return LocalTransactionState.COMMIT_MESSAGE;
}
});5. Pre‑deduct stock
Rate‑limit requests and write a pre‑deduction record before the final DB update.
RateLimiter limiter = RateLimiter.create(1000); // 1000 tokens per second
public boolean preDeduct(Long itemId) {
if (!limiter.tryAcquire()) return false;
preStockDao.insert(itemId, userId);
return true;
}Common pitfalls
Cache‑DB inconsistency
Deleting the cache before updating the DB creates a race window.
redisTemplate.delete("stock:" + productId);
productDao.updateStock(productId, newStock); // concurrency windowMissing stock rollback
Canceling an order must roll back stock within the same transaction.
@Transactional
public void cancelOrder(Order order) {
stockDao.restock(order.getItemId(), order.getCount());
orderDao.delete(order.getId());
}Lock granularity too coarse
Global locks cause unnecessary request rejection.
RLock globalLock = redisson.getLock("global_stock_lock");Conclusion
In practice, a combination of Redis as the first line, distributed lock for critical paths, and pre‑deduction with a message queue yields the most reliable solution. Real‑world tests during a major sale showed 120 000 QPS with zero overselling.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
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.