库存超卖问题
2026年2月21日大约 4 分钟
库存超卖问题
库存超卖问题是指在高并发的情况下,多个用户同时购买同一件商品,导致库存数量被错误地更新,从而出现库存为负数的情况。
- 问题核心在于,多线程下对共享资源(库存)的访问和修改没有进行有效的同步,导致数据不一致。如下图所示:

图:库存超卖问题的示例
解决库存超卖问题
库存超卖问题也就是多线程安全问题,常见的解决方法就是加锁:
- 悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。
- 例如Synchronized、Lock都属于悲观锁。
- 乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。
- 如果没有修改则认为是安全的,自己才更新数据。
- 如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。

图:悲观锁和乐观锁的区别
提示
悲观锁实际上就是在java基础中线程里面线程同步的内容,这里就不再赘述了,下面讲解乐观锁的实现方式。
乐观锁的实现方式
版本号法:
在数据表中添加一个版本号字段,每次更新数据时先查询当前版本号,然后在更新时带上版本号进行判断,如果版本号不一致说明有其它线程修改了数据,此时可以重试或异常。
- 简单来说就是,在更新数据时,和原来的版本号进行比较,看是否被修改过,如果没有被修改过就更新,否则说明有线程修改了数据,此时可以重试或异常。
CAS法:
CAS算法:CAS(Compare And Swap)算法是一种无锁的乐观锁实现方式,主要通过CPU提供的原子操作来实现线程安全。CAS算法包含三个操作数:内存位置V、旧的预期值A和新的值B。CAS操作会比较内存位置V的当前值与预期值A,如果两者相等,则将内存位置V的值更新为B;如果不相等,则说明有其他线程修改了数据,此时可以重试或异常。
- 简单来说就是,在更新数据时,和原来的数据进行比较,如果没有被修改过就更新,否则说明有线程修改了数据,此时可以重试或异常。
提示
下面以CAS法为例,介绍一下如何使用CAS算法来解决库存超卖问题:
- 这是一个优惠券秒杀的业务场景,用户在秒杀开始后可以抢购优惠券,抢购成功后会扣库存并创建订单。
@Override
@Transactional
public Result secKillVoucher(Long voucherId) {
// 1. 查询优惠券信息
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2. 判断是否在秒杀时间内
// 2.1 不在则直接返回
if(voucher.getBeginTime().isAfter(LocalDateTime.now()) ) {
return Result.fail("秒杀还未开始");
}
if(voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
// 3. 判断库存是否充足
if(voucher.getStock()< 1){
return Result.fail("库存不足");
}
// 4. 扣库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).eq("stock", voucher.getStock()) // CAS 解决超卖问题
.update();
if(!success) {
return Result.fail("库存不足");
}
// 5. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId)
.setUserId(UserHolder.getUser().getId()) // 用户id
.setVoucherId(voucherId); // 代金券id
save(voucherOrder);
// 6. 返回订单ID
return Result.ok(orderId);
}注意
在多线程测试时会发现,在CAS这个条件下.eq("stock", voucher.getStock())抢购失败率很高。
- 这是因为多线程情况下,在同一时刻多个线程同时查询到库存充足,然后同时执行扣库存的操作,导致只有一个线程能够成功扣库存,其他线程因为CAS条件不满足而失败。
因此在抢购这种有库存的业务场景下,可以不用判断库存是否和之前查询到的一样,而是直接判断库存是否大于0,如下所示:
// 4. 扣库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0) // 判断库存是否充足
.update();
if(!success) {
return Result.fail("库存不足");
}