黑马点评-秒杀

乐观锁解决商品超卖问题:

增加一个版本号,如果修改时的版本号与查询获得的版本号不同,则下单失败。(失败率太高)

改进:不用版本号,改用商品库存,如果修改时的库存大于0,则下单成功。

悲观锁解决一人一单问题:

 //5.一人一单,UserHolder是对ThreadLocal操作的封装
    Long userId=UserHolder.getUser().getId();
    //锁:1.以userId作为锁,由于userId是线程私有变量,这样加锁等于没加,每个线程获取的锁都是单独的
    //2.以userId.toString()为锁,由于toString()方法是new了一个新的String对象,每个线程进来时仍是不同的锁对象
    //intern()则是设置了一个字符串常量池,保证userid相同时,一定是同一个锁对象
    //但在这里,它是先释放锁,再进行事物提交,就有可能造成事务还未提交,其他线程获得锁穿插进来执行,仍会导致一人一单失败
    //改进,在调用此方法之前先获得锁,方法结束事物提交后释放锁
    //synchronized(userId.toString().intern()){
    //5.1订单id
    Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    //5.2判断是否存在
    if(count>0){
        return Result.fail("用户已经购买过一次!");
    }
    //6.扣减库存->6,1 CAS法,乐观锁 stock=原查询的结果,弊端:失败率大大增加
    //->6.2stock>0
    boolean sucess = seckillVoucherService.update()
            .setSql("stock=stock-1")
            .eq("voucher_id", voucherId)
            .gt("stock",0)
            .update();
    if(!sucess){
        //扣减失败
        return Result.fail("库存不足!");
    }
    //7.创建订单
    VoucherOrder voucherOrder=new VoucherOrder();
    //7.1订单id,ID生成器
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    voucherOrder.setUserId(userId);
    //7.3代金卷id
    voucherOrder.setVoucherId(voucherId);
    iVoucherOrderService.save(voucherOrder);
    return Result.ok(orderId);
    //}
}

在Spring框架中,@Transactional 注解用于声明一个方法应该运行在事务环境中。当你将 @Transactional 注解应用于一个方法时,Spring会在运行时通过代理机制(基于JDK动态代理或CGLIB代理)来拦截对该方法的调用,并在这个调用上应用事务逻辑。

synchronized(userId.toString().intern()){
    //相当于this.createVoucherOrder(voucherId),该方法添加注解@Transactional,
    //直接this调用没有走代理对象
    return createVoucherOrder(voucherId);
}
  1. Spring的代理机制:Spring通过代理来拦截对bean的方法调用,并在这个调用上执行额外的逻辑(如事务管理)。然而,当你通过this引用在类内部调用一个方法时,这个调用是直接的,不通过代理。因此,代理无法拦截这个调用,也就无法在该调用上应用@Transactional注解所指定的事务逻辑。

  2. 解决方法

    • 自我注入:一种常见的解决方法是在你的类中自我注入(self-inject)自己。这意味着你通过Spring来注入你自己的bean实例到一个字段中,然后你可以通过这个字段来调用方法,这样调用就会通过代理进行。但这种方法并不推荐,因为它增加了代码的复杂性和维护难度。
    • 重构代码:更好的做法是将需要事务支持的方法放在单独的bean中,并通过Spring管理的bean来调用它们。这样可以确保所有通过Spring注入的bean之间的方法调用都会通过代理,从而可以正确地应用@Transactional注解。

解决方案:

添加依赖:

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

在启动类添加注解:当 exposeProxy 设置为 true 时,Spring AOP 会在当前代理对象内部暴露一个代理对象的引用。这意味着在目标对象的方法内部,你可以通过某种方式获取到这个代理对象本身,进而调用代理对象的其他方法,即使这些方法是通过 AOP 增强的。

@EnableAspectJAutoProxy(exposeProxy = true)
synchronized(userId.toString().intern()){
    //获取代理对象(事务)
    
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}

然而在服务器集群情况下,不同的服务器的JVM不也同,其字符串常量也不同(即锁监视器不同),这就导致之前一人一单解决的方案失效,故需要一个跳出服务器集群的锁监视器来满足要求,即分布式锁。

悲观锁+乐观锁+redis+lua脚本解决秒杀问题:

初始思路:

  • 我们利用redis的SETNX方法,当有多个线程进入时,我们就利用该方法来获取锁。第一个线程进入时,redis 中就有这个key了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁(返回了0)的线程,等待一定时间之后重试

Redis分布式锁误删情况说明

  • 逻辑说明
    • 持有锁的线程1在锁的内部出现了阻塞,导致他的锁TTL到期,自动释放
    • 此时线程2也来尝试获取锁,由于线程1已经释放了锁,所以线程2可以拿到
    • 但是现在线程1阻塞完了,继续往下执行,要开始释放锁了
    • 那么此时就会将属于线程2的锁释放,这就是误删别人锁的情况
  • 解决方案
    • 解决方案就是在每个线程释放锁的时候,都判断一下这个锁是不是自己的,如果不属于自己,则不进行删除操作。

分布式锁的原子性问题

  • 更为极端的误删逻辑说明
  • 假设线程1已经获取了锁,在判断标识一致之后,准备释放锁的时候,又出现了阻塞(例如JVM垃圾回收机制)
  • 于是锁的TTL到期了,自动释放了
  • 那么现在线程2趁虚而入,拿到了一把锁
  • 但是线程1的逻辑还没执行完,那么线程1就会执行删除锁的逻辑
  • 但是在阻塞前线程1已经判断了标识一致,所以现在线程1把线程2的锁给删了
  • 那么就相当于判断标识那行代码没有起到作用
  • 这就是删锁时的原子性问题
  • 因为线程1的拿锁,判断标识,删锁,不是原子操作,所以我们要防止刚刚的情况
  • 解决方案
    • 利用Lua语言实现redis多条命令执行时的原子性:EVAL "return redis.call('命令名称','key','其他参数'...)"key类型参数个数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值