yudaocode/SpringBoot-Labs · error · java.lang.Exception

库存不足

Error message

库存不足

What it means

ProductServiceImpl.reduceStock throws Exception('库存不足' — insufficient stock) when the stock-decrement UPDATE affects 0 rows. Under Seata AT this participant failure rolls back the whole global purchase transaction (order creation, balance deduction, etc.). Zero affected rows means the conditional UPDATE (stock >= amount guard) found no matching row — stock exhausted or a concurrent purchase consumed it first.

Source

Thrown at lab-52/lab-52-multiple-datasource/src/main/java/cn/iocoder/springboot/lab52/seatademo/service/impl/ProductServiceImpl.java:37

    @Autowired
    private ProductDao productDao;

    @Override
    @DS(value = "product-ds")
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 开启新事物
    public void reduceStock(Long productId, Integer amount) throws Exception {
        logger.info("[reduceStock] 当前 XID: {}", RootContext.getXID());

        // 检查库存
        checkStock(productId, amount);

        logger.info("[reduceStock] 开始扣减 {} 库存", productId);
        // 扣减库存
        int updateCount = productDao.reduceStock(productId, amount);
        // 扣除成功
        if (updateCount == 0) {
            logger.warn("[reduceStock] 扣除 {} 库存失败", productId);
            throw new Exception("库存不足");
        }
        // 扣除失败
        logger.info("[reduceStock] 扣除 {} 库存成功", productId);
    }

    private void checkStock(Long productId, Integer requiredAmount) throws Exception {
        logger.info("[checkStock] 检查 {} 库存", productId);
        Integer stock = productDao.getStock(productId);
        if (stock < requiredAmount) {
            logger.warn("[checkStock] {} 库存不足,当前库存: {}", productId, stock);
            throw new Exception("库存不足");
        }
    }

}

View on GitHub (pinned to 6c12efaed0)

Solutions

  1. Treat the error as correct oversell protection: retry with a lower amount or restock the product.
  2. Keep the atomic guard in SQL (UPDATE ... WHERE id=? AND stock >= ?) and translate 0 rows to a typed InsufficientStockException.
  3. For hot products, add Redis/DB row locks or queue-based stock deduction to reduce contention instead of weakening the guard.
  4. Confirm Seata DataSourceProxy wraps the product datasource so a throw triggers branch rollback (check undo_log table entries).

Example fix

// before
int updateCount = productDao.reduceStock(productId, amount);
if (updateCount == 0) {
    throw new Exception("库存不足");
}

// after
int updated = productDao.reduceStockIfEnough(productId, amount);
// UPDATE product SET stock = stock - #{amount} WHERE id = #{productId} AND stock >= #{amount}
if (updated == 0) {
    throw new InsufficientStockException(productId);
}
Defensive patterns

Strategy: validation

Validate before calling

// Atomic stock check-and-decrement in one statement:
// UPDATE product SET stock = stock - #{amount} WHERE id = #{productId} AND stock >= #{amount}
int ok = productDao.reduceStockIfEnough(productId, amount);
if (ok == 0) { /* structured rejection, no exception */ }

Try / catch

try {
    productService.reduceStock(productId, amount);
} catch (Exception e) {
    throw new OrderFailedException("INSUFFICIENT_STOCK", e);
}

Prevention

When it happens

Trigger: POST /product/reduce-stock (productId, amount) where the conditional UPDATE matches no row: stock lower than amount, product id absent, or a race with a concurrent purchase. checkStock earlier catches the static case; this branch catches races and missing rows.

Common situations: Oversell protection in flash-sale demos. Common follow-up issues: devs remove the WHERE stock >= #{amount} guard to 'fix' the error and thereby allow oversell (negative stock); or under Seata, the branch transaction's undo_log is left behind if the datasource is not proxied, and the throw no longer rolls back other participants.

Related errors


AI-assisted analysis of yudaocode/SpringBoot-Labs@6c12efaed0 (2026-08-14). Data as JSON: /api/errors/5ce56ac801605808. Report an issue: GitHub.