How to Prevent Product Overselling in High‑Traffic E‑Commerce Systems
This article explains why inventory overselling occurs during massive sales events, analyzes the root causes such as non‑atomic database operations, and presents practical solutions—including optimistic locking, Redis atomic scripts, distributed locks, message‑queue peak shaving, and pre‑deduct strategies—while highlighting common pitfalls and best‑practice combinations for robust e‑commerce back‑ends.
Introduction
During a major e‑commerce flash sale, a perfectly designed distributed architecture was overwhelmed at midnight, leading to exhausted database connection pools, negative inventory values, and a flood of customer service calls.
1 Why does overselling happen?
1.1 Database "last line" vulnerability
A naive implementation checks stock then updates it, which is not atomic:
public boolean buy(int goodsId) {
// 1. query stock
int stock = getStockFromDatabase(goodsId);
if (stock > 0) {
// 2. deduct stock
updateStock(goodsId, stock - 1);
return true;
}
return false;
}In a concurrent scenario, multiple requests can read the same stock value and both update it, resulting in negative inventory.
1.2 Essence of overselling
Multiple requests bypass the cache, read the same stock value at the same moment, and finally overwrite each other in the database, just like 100 people all trying to grab the same item.
2 Solutions to prevent overselling
2.1 Database optimistic lock
The core principle is to control concurrency with a version number.
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = 123 AND version = #{currentVersion};Java implementation:
@Transactional
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;
}Pros
No additional middleware required
Simple to implement
Cons
High DB pressure under heavy concurrency
Potential for many update failures
Suitable for systems with daily orders below 10,000.
2.2 Redis atomic operation
The core principle uses Redis together with a Lua script to guarantee atomicity.
// Lua script ensures atomicity
String lua = "if redis.call('get', KEYS[1]) >= ARGV[1] then " +
"return redis.call('decrby', KEYS[1], ARGV[1]) " +
"else return -1 end";Java method executing the script:
public boolean preDeduct(String itemId, int count) {
RedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(itemId), count);
return result != null && result >= 0;
}2.3 Distributed lock (Redisson)
Redisson provides a distributed lock to serialize critical stock‑deduction logic.
RLock lock = redisson.getLock("stock_lock:" + productId);
try {
if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {
// perform stock operation
}
} finally {
lock.unlock();
}2.4 Message‑queue peak shaving
RocketMQ transactional messages can be used to guarantee eventual consistency.
TransactionMQProducer producer = new TransactionMQProducer("stock_group");
producer.setExecutor(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg) {
// deduct DB stock
return LocalTransactionState.COMMIT_MESSAGE;
}
});2.5 Pre‑deduct inventory
A token‑bucket (Guava RateLimiter) limits the request rate, and a pre‑stock table records successful reservations.
RateLimiter limiter = RateLimiter.create(1000); // 1000 tokens per second
if (!limiter.tryAcquire()) return false;
preStockDao.insert(itemId, userId);
return true;3 Pitfalls
3.1 Cache‑DB inconsistency
Deleting the cache before updating the database creates a race window:
redisTemplate.delete("stock:" + productId);
productDao.updateStock(productId, newStock); // concurrent writes possible3.2 Not handling stock rollback
When a flash‑sale order is cancelled, the stock must be rolled back within a transaction:
@Transactional
public void cancelOrder(Order order) {
stockDao.restock(order.getItemId(), order.getCount());
orderDao.delete(order.getId());
}3.3 Lock granularity too large
Using a global lock throttles unrelated requests:
RLock globalLock = redisson.getLock("global_stock_lock");Conclusion
Large‑scale e‑commerce platforms typically combine multiple measures: Redis as the first line of defense (handling ~80% of traffic), distributed locks for core business logic, and pre‑deduct plus message‑queue mechanisms to ensure eventual consistency. This combination can sustain 120 k QPS with zero overselling incidents.
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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
