{"record":{"id":"7368c4d1e1f33d14","repo":"crossoverJie/JCSprout","slug":"redis-currentcount","errorCode":null,"errorMessage":"库存不足 Redis currentCount=","messagePattern":"库存不足 Redis currentCount=","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"warning","filePath":"MD/third-party-component/seconds-kill.md","lineNumber":611,"sourceCode":"    @Override\n    public int createOptimisticOrderUseRedis(int sid) throws Exception {\n        //检验库存，从 Redis 获取\n        Stock stock = checkStockByRedis(sid);\n\n        //乐观锁更新库存 以及更新 Redis\n        saleStockOptimisticByRedis(stock);\n\n        //创建订单\n        int id = createOrder(stock);\n        return id ;\n    }\n    \n    \n    private Stock checkStockByRedis(int sid) throws Exception {\n        Integer count = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid));\n        Integer sale = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid));\n        if (count.equals(sale)){\n            throw new RuntimeException(\"库存不足 Redis currentCount=\" + sale);\n        }\n        Integer version = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_VERSION + sid));\n        Stock stock = new Stock() ;\n        stock.setId(sid);\n        stock.setCount(count);\n        stock.setSale(sale);\n        stock.setVersion(version);\n\n        return stock;\n    }    \n    \n    \n    /**\n     * 乐观锁更新数据库 还要更新 Redis\n     * @param stock\n     */\n    private void saleStockOptimisticByRedis(Stock stock) {\n        int count = stockService.updateStockByOptimistic(stock);","sourceCodeStart":593,"sourceCodeEnd":629,"githubUrl":"https://github.com/crossoverJie/JCSprout/blob/fc4c6e5f6d1772c2aec4c0f94cb3c8e7eb04018f/MD/third-party-component/seconds-kill.md#L593-L629","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure Redis stock keys (STOCK_COUNT, STOCK_SALE, STOCK_VERSION) are initialised before the sale begins — via a startup script or an admin endpoint.","Add a null/missing-key guard before Integer.parseInt so you get a clear error instead of a NumberFormatException masking the real issue.","If this fires immediately at sale start, check whether a previous sale drained the Redis keys without resetting them.","Reconcile Redis with the DB periodically or on startup to prevent drift."],"exampleFix":"// before — assumes Redis keys always exist and are numeric\nInteger count = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid));\nInteger sale = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid));\nif (count.equals(sale)) {\n    throw new RuntimeException(\"库存不足 Redis currentCount=\" + sale);\n}\n\n// after — guard missing keys and use >=\nString countStr = redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid);\nString saleStr = redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid);\nif (countStr == null || saleStr == null) {\n    throw new IllegalStateException(\"Stock keys not initialised in Redis for sid=\" + sid);\n}\nint count = Integer.parseInt(countStr);\nint sale = Integer.parseInt(saleStr);\nif (sale >= count) {\n    throw new RuntimeException(\"库存不足 Redis currentCount=\" + sale);\n}","handlingStrategy":"validation","validationCode":"// Validate Redis keys exist and are numeric before the order flow.\nString countStr = redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid);\nString saleStr  = redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid);\nif (countStr == null || saleStr == null) {\n    throw new IllegalStateException(\"Redis stock keys not initialised for sid=\" + sid);\n}\nint count = Integer.parseInt(countStr);\nint sale  = Integer.parseInt(saleStr);\nif (sale >= count) {\n    // stock exhausted — reject early\n    throw new RuntimeException(\"库存不足 Redis currentCount=\" + sale);\n}","typeGuard":null,"tryCatchPattern":"try {\n    int orderId = orderService.createOptimisticOrderUseRedis(sid);\n    return ResponseEntity.ok(orderId);\n} catch (RuntimeException e) {\n    if (e.getMessage() != null && e.getMessage().startsWith(\"库存不足\")) {\n        return ResponseEntity.status(409).body(\"Sold out: \" + e.getMessage());\n    }\n    throw e;\n}","preventionTips":["Initialise Redis stock keys (STOCK_COUNT, STOCK_SALE, STOCK_VERSION) before the sale starts.","Guard against null Redis returns before Integer.parseInt to avoid masking this with a NumberFormatException.","Periodically reconcile Redis stock values with the DB to prevent drift.","Use >= instead of == in the exhaustion check to catch already-oversold state."],"tags":["redis","seconds-kill","stock","cache"],"backgroundTag":null,"analyzedSha":"fc4c6e5f6d1772c2aec4c0f94cb3c8e7eb04018f","analyzedAt":"2026-08-14T05:43:20.992Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}