在并发情况下,spring事务问题对线程安全的影响(锁在事务未提交时已经释放)

今天实现秒杀优惠券的一人一单功能时,出现了bug,原因是spring的事务没有控制好

源码如下

@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("优惠券已抢光");
        }
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            //创建订单
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //一人一单
        //判断用户是否下过单
        Long userId = UserHolder.getUser().getId();

        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            return Result.fail("用户已经下过单");
        }
        //若库存充足,则stock-1
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();
        if (!success) {
            return Result.fail("优惠券已抢光");
        }
        //创建订单并返回订单id
        VoucherOrder voucherOrder = new VoucherOrder();
        //设置优惠券id
        voucherOrder.setVoucherId(voucherId);
        //生成订单id
        Long orderId = redisIdProducer.produceId("order");
        voucherOrder.setId(orderId);
//        voucherOrder.setUserId(1L);
        voucherOrder.setUserId(UserHolder.getUser().getId());
        save(voucherOrder);
        return Result.ok(orderId);

seckillVoucher 为controller调用的方法,createVoucherOrder为检验用户并且创建订单的方法,在使用jmeter携带token模拟同一个用户的恶意抢券行为时,发现并不能很好地实现一人一单。

原因在于spring的事务控制问题

@Override
    @Transactional
    public Result seckillVoucher(Long voucherId)

我在主体方法上加了@Transactional注解,那么事务的范围扩大到了seckillVoucher上。但是要修改数据库,实现事务控制的方法只有createVoucherOrder。而我加锁的范围是

 synchronized (userId.toString().intern()) {
            //创建订单
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }

如果@Transactional加在方法上,那么spring事务提交的范围就在此方法结束后,并且事务的范围为seckillVoucher,即整个业务逻辑上。那么在创建完订单并返回订单id后,锁已经释放,但是此时spring的事务还未提交完成,即mysql中还没有订单数据,单此时其他携带同一token的线程能够获取锁并执行createVoucherOrder方法,导致线程问题。无法实现用户一人一单。

解决方法:将事务的粒度控制在createVoucherOrder上。即将seckillVoucher的事务注解删掉,

                现在只有在createVoucherOrder订单事务提交后,锁才会释放。

 @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("优惠券已抢光");
        }
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            //创建订单
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //一人一单
        //判断用户是否下过单
        Long userId = UserHolder.getUser().getId();

        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            return Result.fail("用户已经下过单");
        }
        //若库存充足,则stock-1
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();
        if (!success) {
            return Result.fail("优惠券已抢光");
        }
        //创建订单并返回订单id
        VoucherOrder voucherOrder = new VoucherOrder();
        //设置优惠券id
        voucherOrder.setVoucherId(voucherId);
        //生成订单id
        Long orderId = redisIdProducer.produceId("order");
        voucherOrder.setId(orderId);
//        voucherOrder.setUserId(1L);
        voucherOrder.setUserId(UserHolder.getUser().getId());
        save(voucherOrder);
        return Result.ok(orderId);

    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值