// 获取优惠券
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);  
// 判断库存是否充足  
if (seckillVoucher.getStock() <= 0) {  
    return Result.fail("库存不足!");  
}  
// 更新库存  
boolean success = seckillVoucherService.update()  
        .setSql("stock = stock - 1")  
        .eq("voucher_id", voucherId)  
        .update();

上述代码中获取和更新操作是分开的,不是原子性操作,因此可能出现只剩1张优惠券,但几十个线程同时获取到优惠券,此时都判断库存充足,导致库存被扣减几十次变为负数,产生超卖问题。

解决方法


可以使用加锁的方法解决该问题。

悲观锁

悲观锁从获取优惠券之前到更新库存后都锁起来,因此该代码块不再并发,自然不存在超卖问题。

synchronized (lock) {
	// 获取优惠券
	SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);  
	// 判断库存是否充足  
	if (seckillVoucher.getStock() <= 0) {  
	    return Result.fail("库存不足!");  
	}  
	// 更新库存  
	boolean success = seckillVoucherService.update()  
	        .setSql("stock = stock - 1")  
	        .eq("voucher_id", voucherId)  
	        .update();
}

乐观锁

乐观锁在更新库存时检查版本号是否一致,只有版本号没有被修改的才更新。

// 获取优惠券
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);  
// 判断库存是否充足  
if (seckillVoucher.getStock() <= 0) {  
	return Result.fail("库存不足!");  
}  
// 更新库存  
boolean success = seckillVoucherService.update()  
		.setSql("stock = stock - 1")  
		.eq("voucher_id", voucherId)
		.eq("version", seckillVoucher.getVersion())
		.update();

这里也可以使用库存stock作为版本号,

boolean success = seckillVoucherService.update()  
		.setSql("stock = stock - 1")  
		.eq("voucher_id", voucherId)
		.eq("stock", seckillVoucher.getStock())
		.update();

但是这样成功率会下降,因为有许多线程会出现版本号不一致的情况,注意到条件可以改成stock > 0,从而提高性能。

boolean success = seckillVoucherService.update()  
		.setSql("stock = stock - 1")  
		.eq("voucher_id", voucherId)
		.gt("stock", 0)
		.update();