Skip to content
DAILY QUOTE

“ ”

一般这种多线程超卖问题可以使用悲观锁、乐观锁两种常见的解决方案。乐观锁一般是用在数据更新时判断数据是否修改,而现在是判断订单是否存在(查询)并且下订单(插入),所以无法像解决库存下单超卖一样使用CAS机制,但是可以使用版本号法,而版本号法需要新增一个字段,所以选择使用悲观锁解决超卖问题。

  • 将下订单的逻辑抽取到createSecKillVoucherOrder()方法中,使用synchronized保证并发安全,使用@Transactional保证事务一致性
java
    /**
     * 处理优惠券秒杀下单请求。
     *
     * 作用:
     * 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加锁,也就是说不同用户加的是不同的锁,这样就可以缩小锁的范围。

java
    /**
     * 创建优惠券订单。
     *
     * 作用:
     * 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进行提交。由于锁已经释放了,其他线程已经可以在提交事务前进来了,而因为事务尚未提交,数据还没有写入数据库,此时其他线程查询订单依然不存在,接着再去下单,因此存在并发安全问题。

我们锁的范围小了,应该把整个方法锁起来。先获取锁,待方法执行完事务提交之后,再去释放锁,保证数据库更新的原子性,确保线程安全:

java
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
java
/**
 * 处理优惠券秒杀下单请求。
 *
 * 作用:
 * 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
java
public interface IVoucherOrderService extends IService<VoucherOrder> {
    Result seckillVoucher(Long voucherId);

    Result createVoucherOrder(Long voucherId);
}
  • 由于使用AOP实现动态代理模式,因此在pom中引入aspectj依赖。
java
<!-- aspectj -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
  • 在启动类中添加@EnableAspectJAutoProxy(exposeProxy = true)注解,允许暴露代理对象,默认是关闭的。
java
@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测试一人一单问题,发现相同优惠券同一个用户最多只能下一单。