一般这种多线程超卖问题可以使用悲观锁、乐观锁两种常见的解决方案。乐观锁一般是用在数据更新时判断数据是否修改,而现在是判断订单是否存在(查询)并且下订单(插入),所以无法像解决库存下单超卖一样使用CAS机制,但是可以使用版本号法,而版本号法需要新增一个字段,所以选择使用悲观锁解决超卖问题。
- 将下订单的逻辑抽取到createSecKillVoucherOrder()方法中,使用synchronized保证并发安全,使用@Transactional保证事务一致性
/**
* 处理优惠券秒杀下单请求。
*
* 作用:
* 1.查询优惠卷;
* 2.判断秒杀是否开始;
* 3.尚未开始;
* 4.判断秒杀是否已经结束;
* 5.已经结束;
*
* @param voucherId优惠券id
* @return处理结果
*/
@Override
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("库存不足");
}
return createVoucherOrder(voucherId);
}
/**
* 创建优惠券订单。
*
* 作用:
* 1.一人一单;
* 2.查询订单;
* 3.判断是否村在;
* 4.用户已经购买过了;
* 5.扣减库存;
*
* @param voucherId优惠券id
* @return处理结果
*/
@Transactional
public synchronized Result createVoucherOrder(Long voucherId){
//5.一人一单
Long userId = UserHolder.getUser().getId(); //直接从ThreadLocal获取ID
//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)
.update();
if(!success){
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder=new VoucherOrder();
//6.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2.用户id
voucherOrder.setUserId(userId);
//6.3.代金卷id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7.返回订单id
return Result.ok(orderId);
}但是这种方案存在的问题就是:synchronized加在方法上,锁的范围是整个方法,锁的对象是this,不管任何一个用户来了都要加这把锁,而且是同一把锁,锁的粒度太大,整个方法是串行执行,性能很差。而一人一单只是同一个用户多次请求下单才判断并发安全问题,如果不是同一个用户则不用加锁。因此我们加锁的对象不应该是VoucherOrderServiceImpl对象,而是给用户id加锁,也就是说不同用户加的是不同的锁,这样就可以缩小锁的范围。
/**
* 创建优惠券订单。
*
* 作用:
* 1.一人一单;
* 2.查询订单;
* 3.判断是否村在;
* 4.用户已经购买过了;
* 5.扣减库存;
*
* @param voucherId优惠券id
* @return处理结果
*/
@Transactional
public Result createVoucherOrder(Long voucherId){
//5.一人一单
Long userId = UserHolder.getUser().getId(); //直接从ThreadLocal获取ID
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)
.update();
if(!success){
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder=new VoucherOrder();
//6.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2.用户id
voucherOrder.setUserId(userId);
//6.3.代金卷id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7.返回订单id
return Result.ok(orderId);
}
}
intern()方法的作用是去Java的“字符串常量池”里找。只要你的用户ID是10086,不管调用多少次,它都会返回常量池里唯一的那同一个字符串对象的引用。这样就完美实现了:只有同一个用户发起多次请求时,才会发生锁排队。
在createSecKillVoucherOrder()方法内部加锁还存在一个问题,就是下完订单后先释放锁,进行事务提交,因为@Transactional加在方法上,是由Spring进行事务管理,所以事务的提交是在方法执行完以后由Spring进行提交。由于锁已经释放了,其他线程已经可以在提交事务前进来了,而因为事务尚未提交,数据还没有写入数据库,此时其他线程查询订单依然不存在,接着再去下单,因此存在并发安全问题。
我们锁的范围小了,应该把整个方法锁起来。先获取锁,待方法执行完事务提交之后,再去释放锁,保证数据库更新的原子性,确保线程安全:
Long userId=UserHolder.getUser().getId();
synchronized (userId.toString().intern()){
return this.createVoucherOrder(voucherId);
}由于@Transactional是加在createSecKillVoucherOrder()上,而不是加在seckillVoucher()上。这里使用this.createSecKillVoucherOrder()调用,this是当前的VoucherOrderServiceImpl对象(原始对象),
Controller
↓
IVoucherOrderService 代理对象
↓
VoucherOrderServiceImpl 原始对象而不是它的代理对象。因为事务是在代理对象那一层做的,所以事务要想生效,其实是Spring对当前的VoucherOrderServiceImpl对象做了动态代理(Spring默认使用JDK动态代理,即对接口做代理,所以代理对象为IVoucherOrderService接口),拿到代理对象后去做事务处理。而当前的this非代理对象,而是目标对象,不具有事务功能。这个场景就是Spring事务失效的几种可能性之一:当前类内部自己调用自己。
- Spring事务失效解决方案 我们需要使用AopContext.currentProxy()方法拿到当前目标对象的代理对象IVoucherOrderService接口,该代理对象被Spring管理,因此使用带有事务功能的代理对象去调用createSecKillVoucherOrder()方法。
大体流程:
AopContext.currentProxy()可以拿到当前对象的 Spring 代理对象
当前方法
↓
拿到 Spring 代理对象
↓
通过代理对象调用 createVoucherOrder()
↓
事务生效- VoucherOrderServiceImpl
/**
* 处理优惠券秒杀下单请求。
*
* 作用:
* 1.查询优惠卷;
* 2.判断秒杀是否开始;
* 3.尚未开始;
* 4.判断秒杀是否已经结束;
* 5.已经结束;
*
* @param voucherId优惠券id
* @return处理结果
*/
@Override
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("库存不足");
}
Long userId=UserHolder.getUser().getId();
synchronized (userId.toString().intern()){
//获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
/**
* 创建优惠券订单。
*
* 作用:
* 1.一人一单;
* 2.查询订单;
* 3.判断是否村在;
* 4.用户已经购买过了;
* 5.扣减库存;
*
* @param voucherId优惠券id
* @return处理结果
*/
@Transactional
public Result createVoucherOrder(Long voucherId){
//5.一人一单
Long userId = UserHolder.getUser().getId(); //直接从ThreadLocal获取ID
//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)
//.eq("stock", voucher.getStock()) //where id=? and stock=?
.gt("stock", 0)
.update();
if(!success){
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder=new VoucherOrder();
//6.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//6.2.用户id
voucherOrder.setUserId(userId);
//6.3.代金卷id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//7.返回订单id
return Result.ok(orderId);
}- IVoucherOrderService
public interface IVoucherOrderService extends IService<VoucherOrder> {
Result seckillVoucher(Long voucherId);
Result createVoucherOrder(Long voucherId);
}- 由于使用AOP实现动态代理模式,因此在pom中引入
aspectj依赖。
<!-- aspectj -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>- 在启动类中添加
@EnableAspectJAutoProxy(exposeProxy = true)注解,允许暴露代理对象,默认是关闭的。
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
@EnableTransactionManagement
@EnableAspectJAutoProxy(exposeProxy = true) // 允许暴露代理对象(显式获取)
public class HmDianPingApplication {
/**
* 执行示例测试。
*
* @param args args参数
* @return无返回值
*/
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}
}- 还原数据库,再次使用JMeter测试一人一单问题,发现相同优惠券同一个用户最多只能下一单。
