【遇见青山】项目难点:解决超卖问题
- 1.乐观锁方案
- 2.悲观锁方案
1.乐观锁方案
原始实现下单功能的方法:
/**
* 秒杀实现
*
* @param voucherId 秒杀券的ID
* @return Result
*/
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 判断秒杀是否已经开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始呢!");
}
// 判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束啦!");
}
// 判断库存是否充足
// 库存不足!
if (voucher.getStock() < 1) {
return Result.fail("库存不足啦!");
}
// 扣减库存
boolean isDeduction = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).update();
if (!isDeduction) {
return Result.fail("库存不足啦!");
}
// 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(UserHolder.getUser().getId());
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 返回订单id
return Result.ok(orderId);
}
这种方案会发生超卖问题,我们尝试使用CAS方法改进,来解决超卖问题
CAS方法要求我们在执行扣减库存的同时判断此时的库存数量是否大于0
// 扣减库存
boolean isDeduction = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
这样可以完美解决超卖问题!
2.悲观锁方案
上述的程序依然存在一个问题:同一个用户可以抢多张优惠券:
基于这个问题,我们对代码进行改进,加入判断用户是否已经秒杀过的逻辑:
// 得到用户ID
Long userId = UserHolder.getUser().getId();
// 判断用户是否已经抢过该优惠券
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("已经抢过啦!");
}
使用JMeter测试,发现多线程依然存在并发安全问题,但由于此时我们的目标是新增数据,新增数据无法使用乐观锁来实现,所以我们只得被迫使用悲观锁!
首先,封装创建订单的类:
/**
* 创建新订单,基于悲观锁实现一人一单功能
*
* @param voucherId 优惠券id
* @return Result
*/
@Transactional
public Result createVoucherOrder(Long voucherId) {
// 得到用户ID
Long userId = UserHolder.getUser().getId();
// 判断用户是否已经抢过该优惠券
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("已经抢过啦!");
}
// 扣减库存
boolean isDeduction = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
if (!isDeduction) {
return Result.fail("库存不足啦!");
}
// 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
给该方法添加悲观锁,同时要注意防止事务失效的问题(需要使用代理对象调用方法):
// 创建新订单,基于悲观锁实现一人一单功能
synchronized (userId.toString().intern()) {
// 为了防止事务失效,这里使用代理对象调用方法
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
// 返回订单id
return proxy.createVoucherOrder(voucherId);
}
添加动态代理的支持:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
启动类上暴漏代理对象:
@EnableAspectJAutoProxy(exposeProxy = true)
解决!