秒杀库存扣减失败需主动识别并发信号并抛业务异常:用原子sql(如where stock>0)结合影响行数判断,返回0时抛businessexception;可选行锁(for update)防超卖;统一@controlleradvice拦截封装响应;redis预减失败也需捕获并回滚。

秒杀场景下库存扣减失败,通常不是因为传统意义上的“异常”,而是高并发导致的 业务逻辑失败(比如库存不足、超卖、更新行数为 0)。Java 的 try-catch 捕获不到这类问题——它捕获的是 Exception 或 RuntimeException,而库存扣减失败往往静默发生(如 SQL `UPDATE` 影响行数为 0)。真正需要的是:识别并发冲突信号 + 主动抛出业务异常 + 统一处理。
用影响行数判断并发扣减失败
数据库层面最直接的并发控制是乐观锁。在扣减 SQL 中加入版本号或库存条件,并检查执行结果:
- 使用 MyBatis 时,
updateStock()方法返回int(影响行数);若返回 0,说明 WHERE 条件不成立(库存 ≤ 0 或已被抢完) - 不要依赖“查库存 → 判是否 >0 → 扣减”这种两步操作(存在竞态),必须用原子 SQL:
UPDATE seckill_goods SET stock = stock - 1 WHERE goods_id = #{id} AND stock > 0 - 代码中显式判断:
int affected = seckillMapper.updateStock(goodsId);
if (affected == 0) {
throw new BusinessException("库存不足或已售罄");
}
配合数据库行锁/悲观锁防超卖(可选强一致性)
对核心商品 ID 加行级锁,确保同一时刻只有一个线程能进入扣减逻辑:
- MyBatis 中写带
FOR UPDATE的查询(需在事务内):
SELECT stock FROM seckill_goods WHERE goods_id = #{id} FOR UPDATE
- 后续 update 会基于该快照执行,避免幻读和超卖
- 注意:锁粒度要小(按主键),避免长事务阻塞;超时时间需合理设置(如
@Transactional(timeout = 3))
统一异常拦截与响应封装
将业务异常转化为标准 HTTP 响应,避免堆栈暴露给前端:
- 定义自定义异常,如
SeckillException extends RuntimeException - 用
@ControllerAdvice+@ExceptionHandler拦截:
@ExceptionHandler(SeckillException.class)
@ResponseBody
public Result> handleSeckillException(SeckillException e) {
return Result.fail("秒杀失败:" + e.getMessage());
}
- 也可统一处理
BusinessException,区分秒杀失败、重复下单、非法请求等语义
补充:Redis 预减库存失败的异常捕获
若采用 Redis + DB 双层校验(如先 DECR stock),需捕获 Redis 操作异常及业务逻辑异常:
-
redisTemplate.opsForValue().decrement("stock:" + id)返回 null 或负数 → 抛SeckillException("库存已抢光") - 网络异常(
JedisConnectionException等)属于运行时异常,可被 catch 或交由全局异常处理器兜底 - 注意:Redis 预减后 DB 扣减失败,必须回滚 Redis(用 Lua 脚本保证原子性或异步补偿)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











