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.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
How to Prevent Product Overselling in High‑Traffic E‑Commerce Systems

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 possible

3.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.

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.

Redisoversellingmessage-queueecommercedistributed-lockoptimistic-lock
Su San Talks Tech
Written by

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.

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.