How to Prevent Inventory Overselling in High-Concurrency Flash Sale Systems: Pessimistic Locks, Optimistic Locks, and Redis Strategies Explained

This article focuses on coupon flash sale scenarios and explains the root causes of inventory overselling, the differences between pessimistic and optimistic locking, and extended strategies such as Redis pre-deduction and message queues for traffic shaping. The core issue is inconsistent concurrent stock deduction. Keywords: high-concurrency flash sales, inventory overselling, optimistic locking.

The technical specification snapshot provides a quick overview

Parameter Description
Primary Languages Java, SQL
Core Protocols / Mechanisms MySQL transactions, CAS, Redis atomic operations, MQ asynchronous consumption
Scenario Characteristics High-concurrency flash sales, inventory deduction, one order per user
Star Count Not provided in the original article
Core Dependencies Spring Boot, MyBatis-Plus, MySQL, Redis, RabbitMQ

AI Visual Insight: This image serves as the article cover and emphasizes the flash sale theme and concurrent access context. It is primarily illustrative rather than a system architecture diagram.

AI Visual Insight: This animated image reinforces the dynamic nature of flash sale traffic and highlights the pressure created when high-frequency requests hit the system simultaneously. It does not convey specific architectural details.

The inventory overselling problem is fundamentally a loss of atomicity in concurrent reads and writes

In a flash sale system, multiple threads may read the same stock record at the same time. If the inventory check and inventory deduction are executed separately, multiple requests can observe the same stock value and eventually drive inventory below zero.

In the original example, 200 threads competed for 100 coupons. Theoretically, only 100 orders should succeed. In practice, stock dropped to -9 and the order count reached 109, which shows that concurrent updates overwrote each other in the database.

AI Visual Insight: This image shows a flash sale stress test or concurrency simulation scenario. Its core purpose is to demonstrate that a large number of requests are entering the inventory deduction path at the same time to verify whether overselling occurs.

AI Visual Insight: This image visually presents abnormal results after stress testing, typically including negative inventory or excessive order counts, to prove that an unlocked implementation cannot satisfy data consistency requirements.

A minimal overselling example makes the issue clear

-- Threads A and B both read stock=1 at nearly the same time
select stock from seckill_voucher where voucher_id = 1;
-- After the business layer determines that inventory is available, both execute the deduction
update seckill_voucher set stock = stock - 1 where voucher_id = 1;

The problem with this code is that the read and write steps are not a single indivisible atomic action.

Pessimistic locking guarantees strong consistency by locking before updating

Pessimistic locking assumes that concurrent conflicts are likely to occur, so it locks the data first and then performs business processing. In MySQL, the most typical pattern is select ... for update.

It is suitable for scenarios with strict consistency requirements and relatively controllable concurrency, such as back-office administration, manual order repair, and low-throughput transaction flows.

-- Acquire a row lock first so other transactions must wait
select stock from seckill_voucher where voucher_id = 1 for update;

-- Then execute the deduction
update seckill_voucher
set stock = stock - 1
where voucher_id = 1;

The core value of this solution is that it serializes updates to the same record and completely blocks concurrent overwrites.

In Java, you typically use it together with a transaction

@Transactional
public Result seckillVoucher(Long voucherId) {
    // Lock the current inventory record during the query
    SeckillVoucher voucher = seckillVoucherMapper.selectForUpdate(voucherId);
    if (voucher.getStock() <= 0) {
        return Result.fail("Out of stock");
    }
    // Deduct inventory and ensure completion within the same transaction
    boolean success = seckillVoucherMapper.decreaseStock(voucherId);
    return success ? Result.ok() : Result.fail("Deduction failed");
}

This code wraps the query and update in a transaction so the lock is not released before commit.

AI Visual Insight: This diagram shows a timeline in which two or more threads interleave their query and deduction operations, clearly illustrating the race condition that causes overselling after the same stock is read multiple times.

Optimistic locking is a better fit for high-concurrency flash sale scenarios

Optimistic locking does not proactively acquire locks. Instead, it validates whether the data still satisfies the required condition at update time. The idea is to defer conflict detection until the write phase and avoid blocking threads.

In flash sale systems, the most common optimistic locking pattern does not use a version number. Instead, it embeds the stock condition directly in the update statement and only allows deduction when stock > 0.

update seckill_voucher
set stock = stock - 1
where voucher_id = ?
and stock > 0;

This SQL statement combines validation and deduction into a single atomic update, which is the key to preventing overselling.

A typical MyBatis-Plus implementation is concise enough

Boolean success = iSeckillVoucherService
    .update()
    .setSql("stock = stock - 1") // Decrease inventory by one
    .eq("voucher_id", voucherId) // Specify the voucher ID
    .gt("stock", 0) // Update only when inventory is greater than 0
    .update();

if (!success) {
    return Result.fail("Out of stock, flash sale failed");
}

This code is essentially database-layer CAS and relies on the number of affected rows to determine success or failure.

AI Visual Insight: This image illustrates the version-based optimistic locking workflow, focusing on reading the version, comparing it during submission, and retrying or returning failure when the comparison fails.

AI Visual Insight: This image shows an update model based on stock values or CAS conditions, emphasizing that concurrent validation is compressed into a single SQL statement to reduce lock wait time and context switching.

The choice between pessimistic and optimistic locking depends on throughput and consistency goals

Pessimistic locking is simple, strongly consistent, and stable in business semantics, but it introduces heavy blocking and causes hotspot products to queue up. Optimistic locking is non-blocking and delivers higher throughput, but conflicts can fail and may require retries or user-friendly failure prompts.

For flash sale scenarios, write conflicts on hotspot items are extremely frequent, but total request volume is even higher. Therefore, the more reasonable goal is not to make every request succeed. The goal is to ensure that inventory is never oversold and the system is not overwhelmed. That is exactly where optimistic locking performs best.

A short comparison table is enough to guide implementation

Solution Consistency Concurrency Performance Implementation Complexity Suitable Scenarios
Pessimistic Locking Strong Low Low Back-office management, low-concurrency transactions
Optimistic Locking Strong Medium to High Low Flash sales, limited-time purchase events, hotspot inventory deduction
Redis Atomic Deduction Eventual consistency Very High Medium Ultra-high-concurrency inventory pre-deduction
MQ Asynchronous Ordering Eventual consistency High High Traffic shaping, asynchronous persistence

This table can serve as the first decision layer when designing a flash sale request path.

The database alone is still not enough to absorb extreme traffic spikes

When concurrency rises further, even with optimistic locking, the database will still come under pressure because of a large number of failed requests. In production, engineers typically add Redis pre-deduction, asynchronous ordering through a message queue, and rate limiting in front of the database.

The value of Redis lies in its fast atomic operations and high throughput. It is ideal for moving hotspot inventory into the memory layer first and then writing back to the database asynchronously.

Long stock = redisTemplate.opsForValue().decrement("stock:" + voucherId);
if (stock < 0) {
    // If the value becomes less than 0 after deduction, restore inventory immediately
    redisTemplate.opsForValue().increment("stock:" + voucherId);
    return Result.fail("Out of stock");
}
// Send an order message asynchronously to reduce instantaneous database pressure
rabbitTemplate.convertAndSend("orderQueue", voucherId);

This code shifts high-frequency deductions from the database to Redis and then uses MQ to smooth traffic asynchronously.

Rate limiting and idempotency are also foundational infrastructure for flash sale systems

RateLimiter limiter = RateLimiter.create(100);
public Result seckill(Long voucherId) {
    if (!limiter.tryAcquire()) {
        return Result.fail("Too many requests, please try again later");
    }
    // Execute database deduction through optimistic locking
    boolean success = update().setSql("stock = stock - 1").gt("stock", 0).update();
    return success ? Result.ok() : Result.fail("Out of stock");
}

This code reduces invalid requests at the entry point through rate limiting and reserves database resources for requests that are more likely to succeed.

The recommended production approach is a layered defense model

A practical flash sale architecture is usually not a single-point solution but a coordinated multilayer design: entry-point rate limiting absorbs traffic spikes, Redis pre-deduction protects throughput, Lua scripts or atomic operations protect local consistency, MQ asynchronous ordering smooths bursts, database optimistic locking provides the final safeguard, and a unique index enforces one order per user.

If the business requires even stronger consistency, you should also add order compensation, message retries, dead-letter queues, and inventory reconciliation jobs. These capabilities are not part of overselling prevention itself, but they determine whether the system can run reliably over the long term.

FAQ

1. Why is optimistic locking recommended more often than pessimistic locking in flash sale systems?

Because the core challenge in flash sales is high throughput. Pessimistic locking serializes processing on hotspot inventory and causes severe database waiting. Optimistic locking compresses validation into a single update statement, which preserves higher concurrency without allowing overselling.

2. Why can where stock > 0 prevent inventory from going negative?

Because the inventory check and the inventory deduction are completed within the same SQL statement, which gives the operation atomicity at the database level. Once inventory reaches 0, subsequent updates no longer satisfy the condition, the affected row count becomes 0, and no further deduction occurs.

3. Do you still need database optimistic locking after introducing Redis pre-deduction?

Yes. Redis handles traffic spikes and fast failure, but the database remains the final source of truth. Optimistic locking acts as the final consistency safeguard and prevents overselling from leaking through when cache and database states diverge.

The core summary ties together the full governance path

This article breaks down the causes of inventory overselling and the corresponding mitigation path for coupon flash sale scenarios. It systematically compares pessimistic locking, optimistic locking, Redis atomic deduction, message queues, rate limiting, and segmented inventory strategies, and provides practical Java/MySQL code examples and implementation guidance.