用户秒杀中,涉及到的同一个类内部方法调用事务失效的问题

在同一个类下的两个方法中

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
​
    @Resource
    private ISeckillVoucherService seckillVoucherService;
​
    @Resource
    private RedisIdWorker redisIdWorker;

    // 添加此行,引入 ApplicationContext
    @Autowired
    private ApplicationContext applicationContext; 
    
    /**
     * 秒杀优惠券
     * @param voucherId
     * @return
     */
​
    @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("库存不足!");
        }
/**
*用户锁,防止同一个用户秒杀太多订单.这里的intern()是去作用是将字符串添加到
*Java 内存中的字符串常量池(String Pool)中,并返回该常量池中已存在的相同字符串引用
*(如果存在的话),这样才能保证用户ID的锁是唯一锁,要是只是userId.toString(),返回的是
*一个new的字符串对象,这会导致同一个ID也会开启开启不同的锁,导致锁失效
*这里的锁是单个jvm的,在分布式系统中会失效
*/
        Long userId = UserHolder.getUser().getId();
        //1.这里是实现不了createVoucherOrder方法的事物的
       //如果 seckillVoucher 方法没有 @Transactional 注解,那么 createVoucherOrder 方法的事务控制也不会生效,整个秒杀流程将不受事务保护。
       //当 seckillVoucher 方法没有 @Transactional 注解时,意味着该方法本身并不受 Spring 事务管理器的控制,不会开启任何事务。此时,如果 seckillVoucher 方法内部调用 createVoucherOrder 方法,尽管 createVoucherOrder 方法带有 @Transactional 注解,但由于以下原因,事务控制不会生效:
       //Spring AOP 代理机制: Spring AOP 通过代理模式来实现对方法的增强,包括事务管理。当一个方法被标注为 @Transactional 时,Spring 会在运行时为该方法对应的 Bean 创建一个代理对象。当外部代码通过 Spring 容器调用这个 Bean 的方法时,实际上是调用了代理对象的方法。代理对象在执行目标方法前会先开启一个事务,执行完目标方法后根据事务的执行情况决定提交或回滚事务。
       //然而,对于同一个类中的方法调用,由于 Java 引用直接指向的是原始对象而非代理对象,所以在 seckillVoucher 方法内部调用 createVoucherOrder 方法时,实际上是直接调用了原始对象的方法,没有经过代理对象的中介。因此,Spring 事务管理器无法介入,createVoucherOrder 方法的事务控制也就不会生效。
       //简而言之: 由于 seckillVoucher 方法没有 @Transactional 注解,不会开启事务,同时它直接调用 createVoucherOrder 方法时没有经过 Spring AOP 代理的封装,所以 createVoucherOrder 方法的事务控制不会生效。整个秒杀流程将不受任何事务保护,可能导致数据一致性问题。例如,如果在创建订单过程中出现异常,已扣减的库存可能不会恢复,或者在数据库写入失败时,用户已经收到了订单成功的响应。
//     要使 createVoucherOrder 方法的事务控制生效,需确保其调用路径经过 Spring AOP 代理,如将其移至单独的类中,或在 seckillVoucher 方法中使用 TransactionTemplate 或 PlatformTransactionManager 手动控制事务,或使用 AspectJ 提供的更精细的织入支持。
        synchronized (userId.toString().intern()){
            return createVoucherOrder(voucherId);
        }
//---------------------------------------------------------------------------------------分界线
        //2.这通过自注入来解决
        //首先,添加 ApplicationContext 类型的成员变量,并使用 @Autowired 注解自动注入 Spring 应用上下文。
        //然后,在 seckillVoucher 方法中,通过 applicationContext.getBean(VoucherOrderServiceImpl.class) 获取当前类(VoucherOrderServiceImpl)的代理对象。由于当前类中存在 @Transactional 注解的方法,Spring 会为其创建代理对象。通过这种方式获取的代理对象具备事务管理能力。
        //最后,使用代理对象调用 createVoucherOrder 方法。
        // 使用 ApplicationContext 获取当前类的代理对象,并通过代理对象调用 createVoucherOrder 方法
        ynchronized (userId.toString().intern()){
           VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) applicationContext.getBean(VoucherOrderServiceImpl.class);
           return proxy.createVoucherOrder(voucherId);
        }
        //3.通过AopContext来解决
        //还可以通过 AopContext 来解决同一个类内部方法调用事务失效的问题。AopContext 是 Spring AOP 框架提供的一个工具类,用于在切面内部访问当前代理对象。在 seckillVoucher 方法中,您可以使用 AopContext.currentProxy() 获取当前类的代理对象,然后通过代理对象调用 createVoucherOrder 方法,以确保事务管理的有效性。
        //但还需要额外做两步,在最下面
        synchronized (userId.toString().intern()){
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }
​

   

    //被调用的秒杀方法
    @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);
        }
    }
​
}

pom中加入注解

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

启动类上加入@EnableAspectJAutoProxy注解来暴露代理对象

@EnableAspectJAutoProxy(exposeProxy = true)//暴露代理对象
@SpringBootApplication
public class HmDianPingApplication {
​
    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }
​
}

最后2.的自注入也可以这样

   private final IVoucherOrderService self;
​
   @Autowired
   public VoucherOrderServiceImpl(IVoucherOrderService self) {
       this.self = self;
   }
​
   // ...

@Override
public Result seckillVoucher(Long voucherId) {
    // ... 其他逻辑 ...
​
    return self.createVoucherOrder(voucherId);
}
​
@Transactional
public Result createVoucherOrder(Long voucherId) {
    // ... 秒杀下单逻辑 ...
}

  • 19
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值