crossoverJie/JCSprout · warning · RuntimeException
并发更新库存失败
Error message
并发更新库存失败
What it means
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.
Source
Thrown at MD/third-party-component/seconds-kill.md:231
@Override
public int createOptimisticOrder(int sid) throws Exception {
//校验库存
Stock stock = checkStock(sid);
//乐观锁更新库存
saleStockOptimistic(stock);
//创建订单
int id = createOrder(stock);
return id;
}
private void saleStockOptimistic(Stock stock) {
int count = stockService.updateStockByOptimistic(stock);
if (count == 0){
throw new RuntimeException("并发更新库存失败") ;
}
}
```
对应的 XML:
```xml
<update id="updateByOptimistic" parameterType="com.crossoverJie.seconds.kill.pojo.Stock">
update stock
<set>
sale = sale + 1,
version = version + 1,
</set>
WHERE id = #{id,jdbcType=INTEGER}
AND version = #{version,jdbcType=INTEGER}
</update>View on GitHub (pinned to fc4c6e5f6d)
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.
Example fix
// before — single attempt, throws on first conflict
private void saleStockOptimistic(Stock stock) {
int count = stockService.updateStockByOptimistic(stock);
if (count == 0) {
throw new RuntimeException("并发更新库存失败");
}
}
// after — retry on version conflict
private void saleStockOptimistic(int sid) {
for (int attempt = 0; attempt < 3; attempt++) {
Stock stock = stockService.getStockById(sid);
if (stock.getSale() >= stock.getCount()) {
throw new RuntimeException("库存不足");
}
if (stockService.updateStockByOptimistic(stock) > 0) {
return;
}
}
throw new RuntimeException("并发更新库存失败");
} Defensive patterns
Strategy: retry
Validate before calling
// No static pre-validation prevents this — the conflict is detected at DB write time. // The correct defense is a bounded retry loop around read + optimistic update.
Try / catch
int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
return orderService.createOptimisticOrder(sid);
} catch (RuntimeException e) {
if (!"并发更新库存失败".equals(e.getMessage()) || attempt == maxRetries - 1) {
throw e;
}
// version conflict — retry with fresh stock read
}
} Prevention
- 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).
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of crossoverJie/JCSprout@fc4c6e5f6d (2026-08-14).
Data as JSON: /api/errors/2254af18326d63e7.
Report an issue: GitHub.