crossoverJie/JCSprout · warning · RuntimeException

并发更新库存失败

Error message

并发更新库存失败

What it means

Thrown by saleStockOptimistic() when the MyBatis optimistic-lock UPDATE affects zero rows. The SQL updates stock SET sale=sale+1, version=version+1 WHERE id=#{id} AND version=#{version}; if no row matches, it means another transaction already incremented the version between this thread's read and write, so the update was a no-op. This is the expected, correct behaviour of optimistic locking under contention — it is not a bug but a signal to retry.

Source

Thrown at MD/third-party-component/seconds-kill.md:231

    @Override
    public int createOptimisticOrder(int sid) throws Exception {

        //校验库存
        Stock stock = checkStock(sid);

        //乐观锁更新库存
        saleStockOptimistic(stock);

        //创建订单
        int id = createOrder(stock);

        return id;
    }
    
    private void saleStockOptimistic(Stock stock) {
        int count = stockService.updateStockByOptimistic(stock);
        if (count == 0){
            throw new RuntimeException("并发更新库存失败") ;
        }
    }
```

对应的 XML:

```xml
    <update id="updateByOptimistic" parameterType="com.crossoverJie.seconds.kill.pojo.Stock">
        update stock
        <set>
            sale = sale + 1,
            version = version + 1,
        </set>

        WHERE id = #{id,jdbcType=INTEGER}
        AND version = #{version,jdbcType=INTEGER}

    </update>

View on GitHub (pinned to fc4c6e5f6d)

Solutions

  1. Wrap the check + optimistic update in a retry loop (e.g. up to 3 attempts) — re-read stock, re-check, and re-update on each iteration.
  2. Catch the exception at the controller/service boundary and return a 'please retry' response (HTTP 409 Conflict) to the client.
  3. If retry counts are high, consider adding a Redis-based pre-deduction or a distributed lock to reduce DB contention.
  4. Ensure the stock table has an index on (id, version) for fast conflict detection.

Example fix

// before — single attempt, throws on first conflict
private void saleStockOptimistic(Stock stock) {
    int count = stockService.updateStockByOptimistic(stock);
    if (count == 0) {
        throw new RuntimeException("并发更新库存失败");
    }
}

// after — retry on version conflict
private void saleStockOptimistic(int sid) {
    for (int attempt = 0; attempt < 3; attempt++) {
        Stock stock = stockService.getStockById(sid);
        if (stock.getSale() >= stock.getCount()) {
            throw new RuntimeException("库存不足");
        }
        if (stockService.updateStockByOptimistic(stock) > 0) {
            return;
        }
    }
    throw new RuntimeException("并发更新库存失败");
}
Defensive patterns

Strategy: retry

Validate before calling

// No static pre-validation prevents this — the conflict is detected at DB write time.
// The correct defense is a bounded retry loop around read + optimistic update.

Try / catch

int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++) {
    try {
        return orderService.createOptimisticOrder(sid);
    } catch (RuntimeException e) {
        if (!"并发更新库存失败".equals(e.getMessage()) || attempt == maxRetries - 1) {
            throw e;
        }
        // version conflict — retry with fresh stock read
    }
}

Prevention

When it happens

Trigger: Two or more threads read the same stock row with the same version, then both attempt the optimistic UPDATE. Only the first succeeds (version matches, then increments); the second's WHERE clause no longer matches and returns 0 affected rows, triggering the exception. Common during high-concurrency flash sales.

Common situations: Legitimate contention during a flash sale where many buyers compete for the same item. Not retrying after a conflict means a buyer who could have succeeded on the second attempt gets a failure instead. The exception propagates as a 500 if not caught.

Related errors


AI-assisted analysis of crossoverJie/JCSprout@fc4c6e5f6d (2026-08-14). Data as JSON: /api/errors/2254af18326d63e7. Report an issue: GitHub.