{"record":{"id":"2254af18326d63e7","repo":"crossoverJie/JCSprout","slug":"error-2254af","errorCode":null,"errorMessage":"并发更新库存失败","messagePattern":"并发更新库存失败","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"warning","filePath":"MD/third-party-component/seconds-kill.md","lineNumber":231,"sourceCode":"    @Override\n    public int createOptimisticOrder(int sid) throws Exception {\n\n        //校验库存\n        Stock stock = checkStock(sid);\n\n        //乐观锁更新库存\n        saleStockOptimistic(stock);\n\n        //创建订单\n        int id = createOrder(stock);\n\n        return id;\n    }\n    \n    private void saleStockOptimistic(Stock stock) {\n        int count = stockService.updateStockByOptimistic(stock);\n        if (count == 0){\n            throw new RuntimeException(\"并发更新库存失败\") ;\n        }\n    }\n```\n\n对应的 XML：\n\n```xml\n    <update id=\"updateByOptimistic\" parameterType=\"com.crossoverJie.seconds.kill.pojo.Stock\">\n        update stock\n        <set>\n            sale = sale + 1,\n            version = version + 1,\n        </set>\n\n        WHERE id = #{id,jdbcType=INTEGER}\n        AND version = #{version,jdbcType=INTEGER}\n\n    </update>","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/crossoverJie/JCSprout/blob/fc4c6e5f6d1772c2aec4c0f94cb3c8e7eb04018f/MD/third-party-component/seconds-kill.md#L213-L249","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Catch the exception at the controller/service boundary and return a 'please retry' response (HTTP 409 Conflict) to the client.","If retry counts are high, consider adding a Redis-based pre-deduction or a distributed lock to reduce DB contention.","Ensure the stock table has an index on (id, version) for fast conflict detection."],"exampleFix":"// before — single attempt, throws on first conflict\nprivate void saleStockOptimistic(Stock stock) {\n    int count = stockService.updateStockByOptimistic(stock);\n    if (count == 0) {\n        throw new RuntimeException(\"并发更新库存失败\");\n    }\n}\n\n// after — retry on version conflict\nprivate void saleStockOptimistic(int sid) {\n    for (int attempt = 0; attempt < 3; attempt++) {\n        Stock stock = stockService.getStockById(sid);\n        if (stock.getSale() >= stock.getCount()) {\n            throw new RuntimeException(\"库存不足\");\n        }\n        if (stockService.updateStockByOptimistic(stock) > 0) {\n            return;\n        }\n    }\n    throw new RuntimeException(\"并发更新库存失败\");\n}","handlingStrategy":"retry","validationCode":"// No static pre-validation prevents this — the conflict is detected at DB write time.\n// The correct defense is a bounded retry loop around read + optimistic update.","typeGuard":null,"tryCatchPattern":"int maxRetries = 3;\nfor (int attempt = 0; attempt < maxRetries; attempt++) {\n    try {\n        return orderService.createOptimisticOrder(sid);\n    } catch (RuntimeException e) {\n        if (!\"并发更新库存失败\".equals(e.getMessage()) || attempt == maxRetries - 1) {\n            throw e;\n        }\n        // version conflict — retry with fresh stock read\n    }\n}","preventionTips":["Wrap the optimistic read-check-update cycle in a bounded retry loop (3-5 attempts).","Catch the exception at the API boundary and return HTTP 409 Conflict with a retry hint for the client.","If contention is very high, add a Redis DECR pre-deduction gate to reduce wasted DB writes.","Ensure the stock table has an index on (id, version)."],"tags":["optimistic-lock","concurrency","seconds-kill","mybatis"],"backgroundTag":null,"analyzedSha":"fc4c6e5f6d1772c2aec4c0f94cb3c8e7eb04018f","analyzedAt":"2026-08-14T05:43:20.992Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}