秒杀一人一单业务流程图如下
实现代码块如下
@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.一人一单逻辑
// 5.1.用户id
Long userId = UserHolder.getUser().getId();
// 这里不需要查具体数据了,只需要查count值
int count = query().eq("user_id", userId)
.eq("voucher_id", voucherId)
.count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
return Result.fail("用户已经购买过一次!");
}
//6,扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update();
if (!success) {
//扣减库存
return Result.fail("库存不足!");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
如果是单线程那上面的逻辑是没有问题的,问题是在多并发的情况就会出现一人购买多次的情况。为什么呢?这里不是已经加了判断吗?当有两个线程同时去订单表查询数据时,都发现没有数据,所以这两个线程都会扣减库存,创建订单,所以还是会出现一人购买多单的情况。所以需要加锁,由于乐观锁适合更新数据控制版本号,而插入数据就没有办法控制版本号了,所以需要使用悲观锁操作。
优化一
我们可以把实现一人一单的逻辑抽取出来封装成一个方法,然后对这个方法进行加锁。另外,事务的范围是对数据的监控,即扣减库存和创建订单时需要监控,而查询信息是不需要事务的,所以需要将seckillVoucher 方法的事务取消,在抽取的方法上添加事务。
优化二
如果在方法中加锁,那么整个方法就是串行执行。如果多个用户购买商品,需要等待上一个用户购买完才能下单,这效率也太低了。因此,这里加锁不应该是整个service服务,而需要为用户加锁。
@Transactional
public Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
synchronized(userId.toString().intern()){
// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
return Result.fail("用户已经购买过一次!");
}
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
return Result.fail("库存不足!");
}
// 7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 7.2.用户id
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderId);
}
}
我们期望用户id相同的使用同一把锁,但即便是同个用户,每个请求传递过来都是一个全新的对象,那么锁也会改变,没有达到预期效果。此时就需要使用 intern() 函数,intern **它可以去常量池中找与你匹配的值,将常量池中的值返回给你。因此,无论new了多少个对象,只要值一样,返回的结果也是一样的。**这就可以确保用户id一样时,加的锁也是一样的。
优化三
如果你在方法内部加锁,会导致当前方法未提交,锁已经释放了。如果恰好有个线程在锁释放掉后,方法未提交这个空窗期内查询订单,而订单还没写入数据库,也有可能会导致并发问题。只需要将锁加在方法调用处,这样锁会在方法执行完毕后才释放锁。
优化四
事务要生效,spring要对当前的类做动态代理,拿到代理对象,而 this.createVoucherOrder指的是当前类,并不是代理对象 ,所以事务会失效。所以我们要获取原始的事务对象来操作事务,借助AopContext的 currentProxy() 方法拿到当前的代理对象。
Object proxy = AopContext.currentProxy();
使用代理对象来调用 createVoucherOrder方法,而不是使用this,这样的话就会被spring进行管理了,因为这个代理对象是由spring创建的,它是带有事务的函数,这个不存在的原因是因为 VoucherOrderService 接口中是不存在这个函数的,因此我们也将这个函数在 VoucherOrderService 中创建一下。
我们还需要添加 aspectj 依赖,它是一种代理的模式
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
接着在SpringBoot 启动类添加 @EnableAspectJAutoProxy(exposeProxy = true) 注解,暴露代理对象。默认是false ,即不暴露代理对象。这样我们就可以获取在实现类中获取代理对象了。
至此也完成了一人一单的业务。