解决超卖问题
解决思路:乐观锁
乐观锁的实现
1.版本号法
- 在数据表中添加一个版本号字段(version),并且默认为0。
- 更新数据前,读取需要更新的数据行,并取得当前版本号。
- 更新时,携带版本号并将版本号+1。
- 执行更新时,指定更新条件为版本号等于读取到的版本号。
- 如果版本号相同,则更新成功,并将版本号加 1。
- 如果版本号不同,则更新失败,表示数据已经被其它线程修改。
- 更新失败的线程可以选择重试更新,重新读取版本号然后更新。
2.CAS法(自旋锁)
- 获取当前共享变量的值A
- 根据A计算出一个新的变量值B
- 使用CAS操作Transaction,将变量值变更为B,只有当当前变量的值仍等于A时,CAS才会成功
- 如果CAS失败,说明其他线程已经更新了变量值,则当前线程可以重新获取变量值然后重试CAS操作
改进乐观锁
问题:当线程1执行完毕后,库存stock变为99,当线程2变更库存时,发现当前线程之前查询库存stock为100,不等于99,于是认为线程不安全,停止执行。
改进:当线程2发现库存stock不符时,不应该立即停止线程,应该判断目前库存stock值是否符合现实(大于0),以此判断线程是否安全
核心实现:
//5.扣减库存
boolean isSuccess = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock", 0).update();
完整代码:
/**
* 秒杀优惠券——超卖问题
* @param voucherId
* @return
*/
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("秒杀尚未开始!");
}
//3.判断秒杀是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
//秒杀结束
return Result.fail("秒杀已经结束!");
}
//4.判断库存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("库存不足!");
}
//5.扣减库存
boolean isSuccess = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock", 0).update();
if (!isSuccess) {
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1订单ID
long orderId = redisWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2用户ID
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//6.3代金券ID
voucherOrder.setVoucherId(voucherId);
//将订单存储到数据库
save(voucherOrder);
//7.返回订单id
return Result.ok(orderId);
}
}