crossoverJie/JCSprout · warning · RuntimeException

库存不足 Redis currentCount=

Error message

库存不足 Redis currentCount=

What it means

Thrown by checkStockByRedis() when the cached sale count in Redis equals the cached total count — meaning inventory is exhausted according to the Redis cache layer. This is the Redis-optimised variant of the stock check: instead of querying the DB every time, it reads count and sale from Redis keys (STOCK_COUNT+sid and STOCK_SALE+sid). The message includes the current sale value for diagnostics.

Source

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

    @Override
    public int createOptimisticOrderUseRedis(int sid) throws Exception {
        //检验库存,从 Redis 获取
        Stock stock = checkStockByRedis(sid);

        //乐观锁更新库存 以及更新 Redis
        saleStockOptimisticByRedis(stock);

        //创建订单
        int id = createOrder(stock);
        return id ;
    }
    
    
    private Stock checkStockByRedis(int sid) throws Exception {
        Integer count = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid));
        Integer sale = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid));
        if (count.equals(sale)){
            throw new RuntimeException("库存不足 Redis currentCount=" + sale);
        }
        Integer version = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_VERSION + sid));
        Stock stock = new Stock() ;
        stock.setId(sid);
        stock.setCount(count);
        stock.setSale(sale);
        stock.setVersion(version);

        return stock;
    }    
    
    
    /**
     * 乐观锁更新数据库 还要更新 Redis
     * @param stock
     */
    private void saleStockOptimisticByRedis(Stock stock) {
        int count = stockService.updateStockByOptimistic(stock);

View on GitHub (pinned to fc4c6e5f6d)

Solutions

  1. Ensure Redis stock keys (STOCK_COUNT, STOCK_SALE, STOCK_VERSION) are initialised before the sale begins — via a startup script or an admin endpoint.
  2. Add a null/missing-key guard before Integer.parseInt so you get a clear error instead of a NumberFormatException masking the real issue.
  3. If this fires immediately at sale start, check whether a previous sale drained the Redis keys without resetting them.
  4. Reconcile Redis with the DB periodically or on startup to prevent drift.

Example fix

// before — assumes Redis keys always exist and are numeric
Integer count = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid));
Integer sale = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid));
if (count.equals(sale)) {
    throw new RuntimeException("库存不足 Redis currentCount=" + sale);
}

// after — guard missing keys and use >=
String countStr = redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid);
String saleStr = redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid);
if (countStr == null || saleStr == null) {
    throw new IllegalStateException("Stock keys not initialised in Redis for sid=" + sid);
}
int count = Integer.parseInt(countStr);
int sale = Integer.parseInt(saleStr);
if (sale >= count) {
    throw new RuntimeException("库存不足 Redis currentCount=" + sale);
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate Redis keys exist and are numeric before the order flow.
String countStr = redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid);
String saleStr  = redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid);
if (countStr == null || saleStr == null) {
    throw new IllegalStateException("Redis stock keys not initialised for sid=" + sid);
}
int count = Integer.parseInt(countStr);
int sale  = Integer.parseInt(saleStr);
if (sale >= count) {
    // stock exhausted — reject early
    throw new RuntimeException("库存不足 Redis currentCount=" + sale);
}

Try / catch

try {
    int orderId = orderService.createOptimisticOrderUseRedis(sid);
    return ResponseEntity.ok(orderId);
} catch (RuntimeException e) {
    if (e.getMessage() != null && e.getMessage().startsWith("库存不足")) {
        return ResponseEntity.status(409).body("Sold out: " + e.getMessage());
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling createOptimisticOrderUseRedis(sid) after the Redis STOCK_SALE key has caught up to the STOCK_COUNT key for that sid. Because the Redis check and the DB update are not atomic, under extreme concurrency multiple threads can read count > sale from Redis before any of them increments the Redis sale key, so this check alone does not prevent overselling — the DB optimistic lock in saleStockOptimisticByRedis is the real guard.

Common situations: Stock genuinely sold out during a flash sale. Redis stock keys were not initialised (returning null, which would cause a NumberFormatException before reaching this check). Redis and DB are out of sync — e.g. a manual DB update was not reflected in Redis, or a Redis increment failed after a successful DB update.

Related errors


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