Skip to content
DAILY QUOTE

“ ”

  • 实现方式一:版本号法

首先我们要为tb_seckill_voucher表新增一个版本号字段version ,线程1查询完库存,在进行库存扣减操作的同时将版本号+1,线程2在查询库存时,同时查询出当前的版本号,发现库存充足,也准备执行库存扣减操作,但是需要判断当前的版本号是否和之前查询时的版本号一致,结果发现版本号发生了改变,这就说明数据库中的数据已经被其他线程修改,需要进行重试(或者直接抛异常中断)

  • 实现方式二:CAS法

由于我们是下订单秒杀业务,需要维护的更新的字段只有库存一个,因此更适合CAS法,即不需要添加version字段,根据库存字段变化来判断是否执行更新,如果数据库查询的库存和当前数据库中的库存数量不一致,则不执行更新操作。

java
/**
 * 处理优惠券秒杀下单请求。
 *
 * 作用:
 * 1.查询优惠卷;
 * 2.判断秒杀是否开始;
 * 3.尚未开始;
 * 4.判断秒杀是否已经结束;
 * 5.已经结束;
 *
 * @param voucherId优惠券id
 * @return处理结果
 */
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
    //1.查询优惠卷
    SeckillVoucher voucher=seckillVoucherService.getById(voucherId);
    //2.判断秒杀是否开始
    if(voucher.getBeginTime().isAfter(LocalDateTime.now())){
        //尚未开始
        return Result.fail("秒杀尚未开始");
    }
    //3.判断秒杀是否已经结束
    if(voucher.getEndTime().isBefore(LocalDateTime.now())){
        //已经结束
        return Result.fail("秒杀已结束");
    }
    //4.判断库存是否充足
    if (voucher.getStock()<1) {
        //库存不足
        return Result.fail("库存不足");
    }
    //5.扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock=stock-1") //set stock=stock-1
            .eq("voucher_id", voucherId)
            .eq("stock", voucher.getStock()) //where id=? and stock=?
    if(!success){
        return Result.fail("库存不足");
    }
    //6.创建订单
    VoucherOrder voucherOrder=new VoucherOrder();
    //6.1.订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    //6.2.用户id
    Long userId = UserHolder.getUser().getId(); //直接从ThreadLocal获取ID
    voucherOrder.setUserId(userId);
    //6.3.代金卷id
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);
    //7.返回订单id
    return Result.ok(orderId);
}
  • 将数据库订单全部删除,库存还原回100,JMeter压测qps200 结果是超卖成负数的问题解决了,但是一共100张库存,订单只卖了20张,库存还有80张没有卖出,还是存在超卖问题。正常来说应该是100个线程买100张,另外100个线程返回库存不足。

这个原因其实就是乐观锁的弊端,成功的概率太低。只要发现数据被修改的不一致就直接终止操作了,而其他线程扣减时的库存和之前从数据库查询到的库存数量很多都不一样,所以没有更新成功。因此我们只需要修改一下判断条件,即只要库存stock > 0满足我们当前的业务逻辑就可以进行修改,而不是库存数据修改就终止更新操作。

java
//5.扣减库存
boolean success = seckillVoucherService.update()
        .setSql("stock=stock-1") //set stock=stock-1
        .eq("voucher_id", voucherId)
        .gt("stock", 0) //限制库存大于0
        .update();
if(!success){
    return Result.fail("库存不足");
}
  • 再次测试,一人下多单情况下超卖问题解决!100张优惠券正常卖出,下单量100,另外100个线程用户没有抢到秒杀券。