{"record":{"id":"5ce56ac801605808","repo":"yudaocode/SpringBoot-Labs","slug":"error-5ce56a","errorCode":null,"errorMessage":"库存不足","messagePattern":"库存不足","errorType":"http","errorClass":"java.lang.Exception","httpStatus":500,"severity":"error","filePath":"lab-52/lab-52-multiple-datasource/src/main/java/cn/iocoder/springboot/lab52/seatademo/service/impl/ProductServiceImpl.java","lineNumber":37,"sourceCode":"    @Autowired\n    private ProductDao productDao;\n\n    @Override\n    @DS(value = \"product-ds\")\n    @Transactional(propagation = Propagation.REQUIRES_NEW) // 开启新事物\n    public void reduceStock(Long productId, Integer amount) throws Exception {\n        logger.info(\"[reduceStock] 当前 XID: {}\", RootContext.getXID());\n\n        // 检查库存\n        checkStock(productId, amount);\n\n        logger.info(\"[reduceStock] 开始扣减 {} 库存\", productId);\n        // 扣减库存\n        int updateCount = productDao.reduceStock(productId, amount);\n        // 扣除成功\n        if (updateCount == 0) {\n            logger.warn(\"[reduceStock] 扣除 {} 库存失败\", productId);\n            throw new Exception(\"库存不足\");\n        }\n        // 扣除失败\n        logger.info(\"[reduceStock] 扣除 {} 库存成功\", productId);\n    }\n\n    private void checkStock(Long productId, Integer requiredAmount) throws Exception {\n        logger.info(\"[checkStock] 检查 {} 库存\", productId);\n        Integer stock = productDao.getStock(productId);\n        if (stock < requiredAmount) {\n            logger.warn(\"[checkStock] {} 库存不足，当前库存: {}\", productId, stock);\n            throw new Exception(\"库存不足\");\n        }\n    }\n\n}\n","sourceCodeStart":19,"sourceCodeEnd":53,"githubUrl":"https://github.com/yudaocode/SpringBoot-Labs/blob/6c12efaed06d12907a0f40dd2ad1f7020aec8798/lab-52/lab-52-multiple-datasource/src/main/java/cn/iocoder/springboot/lab52/seatademo/service/impl/ProductServiceImpl.java#L19-L53","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Treat the error as correct oversell protection: retry with a lower amount or restock the product.","Keep the atomic guard in SQL (UPDATE ... WHERE id=? AND stock >= ?) and translate 0 rows to a typed InsufficientStockException.","For hot products, add Redis/DB row locks or queue-based stock deduction to reduce contention instead of weakening the guard.","Confirm Seata DataSourceProxy wraps the product datasource so a throw triggers branch rollback (check undo_log table entries)."],"exampleFix":"// before\nint updateCount = productDao.reduceStock(productId, amount);\nif (updateCount == 0) {\n    throw new Exception(\"库存不足\");\n}\n\n// after\nint updated = productDao.reduceStockIfEnough(productId, amount);\n// UPDATE product SET stock = stock - #{amount} WHERE id = #{productId} AND stock >= #{amount}\nif (updated == 0) {\n    throw new InsufficientStockException(productId);\n}","handlingStrategy":"validation","validationCode":"// Atomic stock check-and-decrement in one statement:\n// UPDATE product SET stock = stock - #{amount} WHERE id = #{productId} AND stock >= #{amount}\nint ok = productDao.reduceStockIfEnough(productId, amount);\nif (ok == 0) { /* structured rejection, no exception */ }","typeGuard":null,"tryCatchPattern":"try {\n    productService.reduceStock(productId, amount);\n} catch (Exception e) {\n    throw new OrderFailedException(\"INSUFFICIENT_STOCK\", e);\n}","preventionTips":["Never remove the stock >= amount SQL guard to silence this error — it prevents oversell.","Expect this error in load tests as stock depletes; assert rollback of the order row.","Use typed exceptions carrying productId for observability."],"tags":["seata","stock","oversell","concurrency","distributed-transaction"],"backgroundTag":null,"analyzedSha":"6c12efaed06d12907a0f40dd2ad1f7020aec8798","analyzedAt":"2026-08-14T13:06:31.500Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}