秒杀相关问题解决

news2025/1/16 19:56:46

秒杀

超卖问题

如下,我们先来复现问题,抢购秒杀券的代码逻辑也是很简单,
在这里插入图片描述
先判断优惠券是否开始了,是的化,判断库存是否充足,如果是的化,扣减库存,最后创建订单

如下是代码

@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
    //1.查询优惠券
    SeckillVoucher voucher = seckillVoucher.getById(voucherId);

    if(voucher == null) {
        return Result.fail("优惠券不存在");
    }

    //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("库存不足!");
    }
    //5.扣减库存
    boolean isSuccess = seckillVoucher.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();

    //6.判断是否成功
    if(!isSuccess) {
        return Result.fail("扣减库存失败!");
    }

    //7.创建订单
    VoucherOrder voucherOrder = new VoucherOrder();
    //7.1订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    //7.2用户id
    Long userId = UserHolder.getUser().getId();
    voucherOrder.setUserId(userId);
    //7.3代金券id
    voucherOrder.setVoucherId(voucherId);
    //7.4保存到voucher_order表中
    save(voucherOrder);

    //8.返回订单id
    return Result.ok(orderId);
}

问题出现如下代码
在这里插入图片描述
我们判断是否充足的时候,有可能很多线程进来刚好都通过了,就会有问题

测试

在这里插入图片描述

设置jmeter
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里是加上token头,因为我的系统写了token头,才能通过,你要是没有的化,就不用

测试结果
在这里插入图片描述
如此就是超卖了

乐观锁 & 悲观锁

理解乐观锁 & 悲观锁

乐观锁有可能你还不是很懂,但是你一定要知道这个,乐观锁,实际上没有加锁,主打的就是一个乐观,如果发现没有问题,就不上锁,如果有问题,就通过特殊的手段保证线程的安全,这里的特殊的手段一般来说,就是类似于cas这样的,使用一个标识来判断是否有线程安全问题

悲观锁就很好理解了,就是平常我们加的粒度很大的锁,例如synchronized

乐观锁的思想

乐观锁的实操都是一个统一的思想,就是cas,比较 + 交换
比较的是什么,得到的旧值 和 我们再一次得到的值(理解为新值) 判断是否是一致的,如果不是一致的,那么就代表着有线程安全问题

我们再来理解一下这里的比较的意思,为什么要比较我们得到的值,举个例子

一开始我们拿到 stock = 100
然后过了几s,我们再去获取stock,发现stock = 98
是不是就说明这里的stock被人用过了,那么就有线程安全问题!此时我们就退出,或者人为再去加锁,都是可以的,一般来说,乐观锁不会直接加锁

我们再来想一个问题,为什么要有乐观锁???
我直接加锁不好吗?? 为的是两个字 性能!!!

我们一旦加了大粒度的锁,就会消耗性能,在那等吗,当然消耗了,所以就有了乐观锁的存在,它实际上是没有锁的,所以性能当然高!!!

乐观锁的缺点

那难道说,乐观锁,就那么好,没什么缺点? 肯定是有的, 会有完成率的问题
完成率不高,甚至于说,本来200 人抢100张优惠券的问题,但是由于设置的乐观锁, 再高并发下,很容易很多的线程都没有抢到,这种问题,在我这里也出现了, 解决办法就是改变比较条件就行,实例请看下面

乐观锁解决超卖

在这里插入图片描述
想我这里就是,简单的cas,判断是否是刚刚的库存

乐观锁完成率不高问题

在这里插入图片描述
我们这里更改了条件,只要库存 > 0的化,就可以成功!

这里为什么可以保证原子性,我觉得需要特别说明一下
我们请求打到数据库的时候那个时间点 有条件 stock > 0
因为有事务的原因,mysql这里的写操作是线程安全的,所以这里不会有问题

一人一单问题

一人一单问题,也是可能会有线程安全问题
我们先来看流程图
在这里插入图片描述
再超卖问题解决之下,去判断是否已经下过一单了,是的化,就不去下单

代码如下

    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券
        SeckillVoucher voucher = seckillVoucher.getById(voucherId);

        if (voucher == null) {
            return Result.fail("优惠券不存在");
        }

        //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();

        //5.一人一单
        int count = query().eq("user_id", userId)
                .eq("voucher_id", voucherId)
                .count();
        if(count > 0) {
            return Result.fail("你已经买过了");
        }


        //6.扣减库存
        boolean isSuccess = seckillVoucher
                .update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock",0)
                .update();

        //7.判断是否成功
        if (!isSuccess) {
            return Result.fail("扣减库存失败!");
        }


        //8.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //8.1订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //8.2用户id
        voucherOrder.setUserId(userId);
        //8.3代金券id
        voucherOrder.setVoucherId(voucherId);
        //8.4保存到voucher_order表中
        save(voucherOrder);

        //9.返回订单id
        return Result.ok(orderId);
    }

问题处在这
在这里插入图片描述如果高并发的情况下,就有可能会有问题

复现线程安全问题

jmeter设置: 和超卖问题的复现jmeter设置是一致的

在这里插入图片描述

原先订单数100
在这里插入图片描述

抢购17号优惠券,正常来说,一个用户只能抢1张

测试结果
在这里插入图片描述

下了10单
在这里插入图片描述

这里就是一人一单出了线程安全问题!

加锁解决

    @Autowired
    private SeckillVoucherServiceImpl seckillVoucher;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券
        SeckillVoucher voucher = seckillVoucher.getById(voucherId);

        if (voucher == null) {
            return Result.fail("优惠券不存在");
        }

        //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);
    }    

	@Transactional
    public synchronized Result createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        //5.一人一单
        int count = query()
                .eq("user_id", userId)
                .eq("voucher_id", voucherId)
                .count();
        System.out.println("此时count为" + count);
        if (count > 0) {
            return Result.fail("用户已经购买过一次");
        }

        //6.扣减库存
        boolean isSuccess = seckillVoucher
                .update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();

        //7.判断是否成功
        if (!isSuccess) {
            return Result.fail("扣减库存失败!");
        }


        //8.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //8.1订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //8.2用户id
        voucherOrder.setUserId(userId);
        //8.3代金券id
        voucherOrder.setVoucherId(voucherId);
        //8.4保存到voucher_order表中
        save(voucherOrder);

        //9.返回订单id
        return Result.ok(orderId);
    }

再整个方法上加锁,这样确实是万无一失

测试
在这里插入图片描述

结果是正确的

优化加锁

如果直接再方法上加锁的化,那么锁的是类对象,也就是这里的service类对象,那么单用户情况下就没问题,但是在多用户情况下就会有问题,因为这里的锁是service类,那么相当于锁的是全部人,也就是说,别的用户还得等你抢完了才能枪,所以这里的锁的粒度有问题,应该锁的是对应的用户而不是所有用户!!!

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //只锁住相同用户,所以这里用userId
        Long userId = UserHolder.getUser().getId();
        //这里是更细粒度的锁,这里不能直接用Long userId来锁,因为有可能是同一个对象,jvm知识
        //所以这里用字符串对象,但是Long的toString()里边也是new String(),所以这里要intern()
        //避免相同的用户却有着不同的锁,再字符串池里边找到我们那个唯一的用户string
       synchronized (userId.toString().intern()) {
           //5.一人一单
           int count = query()
                   .eq("user_id", userId)
                   .eq("voucher_id", voucherId)
                   .count();
           System.out.println("此时count为" + count);
           if (count > 0) {
               return Result.fail("用户已经购买过一次");
           }

           //6.扣减库存
           boolean isSuccess = seckillVoucher
                   .update()
                   .setSql("stock = stock - 1")
                   .eq("voucher_id", voucherId)
                   .gt("stock", 0)
                   .update();

           //7.判断是否成功
           if (!isSuccess) {
               return Result.fail("扣减库存失败!");
           }


           //8.创建订单
           VoucherOrder voucherOrder = new VoucherOrder();
           //8.1订单id
           long orderId = redisIdWorker.nextId("order");
           voucherOrder.setId(orderId);
           //8.2用户id
           voucherOrder.setUserId(userId);
           //8.3代金券id
           voucherOrder.setVoucherId(voucherId);
           //8.4保存到voucher_order表中
           save(voucherOrder);

           //9.返回订单id
           return Result.ok(orderId);
       }
    }

这样子锁的才是用户,多人抢的化,就不会相互干涉

事务失效

提到这个我不得不说,这个问题比较难理解,这里的问题相关springboot中的事务

我们来看这里的代码

在这里插入图片描述

在这个方法上我们加上了事务 @Transactional 也就是springboot事务处理
,这个注解默认什么都不写的情况下,事务的隔离级别是数据库的隔离级别,而我这里的数据库是mysql,也就是读已提交
什么是读已提交,也就是说,只能读到已经提交的事务,那些没有提交的事务,别的事务是看不到的

这个隔离级别就是为了解决脏读 + 脏写的问题来着,但是反而在这里会出现问题

我门来看这里的流程

  • 事务开始
  • 上锁
  • 业务代码
  • 释放锁
  • 事务结束

因为这里的锁是嵌套在这个方法里边的,并不是方法上的,所以说,我们释放锁的时候,事务不一定结束!! 换种方法说,就是事务没有提交!

这个问题很关键! 你事务没有提交,意思是别人根本读不到你这里的已经下了单的order,并且你还已经释放锁了,所以别的线程进来,就可以又来下单

所以总的来说,你看这个代码,这个问题的出现就是那么一瞬间的事,但是还是有可能会出现问题的

我们总结一下,为什么会出现这个问题,就是因为释放锁 和 事务的提交不同步,先释放锁了,才去提交事务,这样别人就有可乘之机,所以我们的解决方法就是先去提交事务,再去释放锁

那么我门的锁,就应该锁的是这整个方法了

代码

    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券
        SeckillVoucher voucher = seckillVoucher.getById(voucherId);

        if (voucher == null) {
            return Result.fail("优惠券不存在");
        }

        //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()) {
            return createVoucherOrder(voucherId);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        //5.一人一单
        int count = query()
                .eq("user_id", userId)
                .eq("voucher_id", voucherId)
                .count();
        System.out.println("此时count为" + count);
        if (count > 0) {
            return Result.fail("用户已经购买过一次");
        }

        //6.扣减库存
        boolean isSuccess = seckillVoucher
                .update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();

        //7.判断是否成功
        if (!isSuccess) {
            return Result.fail("扣减库存失败!");
        }


        //8.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //8.1订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //8.2用户id
        voucherOrder.setUserId(userId);
        //8.3代金券id
        voucherOrder.setVoucherId(voucherId);
        //8.4保存到voucher_order表中
        save(voucherOrder);

        //9.返回订单id
        return Result.ok(orderId);
    }

这里还有一个问题,就是这里没有调用事务

这里的 return createVoucherOrder(voucherId);
实际上的写法是这样
return this.createVoucherOrder(voucherId);

是用这个类的对象来调用的,但是由于spring底层是通过aop来实现事务管理的,我们要用代理对象才能发起一个事务,不然还是会有问题!!!

代码

    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券
        SeckillVoucher voucher = seckillVoucher.getById(voucherId);

        if (voucher == null) {
            return Result.fail("优惠券不存在");
        }

        //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);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        //5.一人一单
        int count = query()
                .eq("user_id", userId)
                .eq("voucher_id", voucherId)
                .count();
        System.out.println("此时count为" + count);
        if (count > 0) {
            return Result.fail("用户已经购买过一次");
        }

        //6.扣减库存
        boolean isSuccess = seckillVoucher
                .update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();

        //7.判断是否成功
        if (!isSuccess) {
            return Result.fail("扣减库存失败!");
        }


        //8.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //8.1订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //8.2用户id
        voucherOrder.setUserId(userId);
        //8.3代金券id
        voucherOrder.setVoucherId(voucherId);
        //8.4保存到voucher_order表中
        save(voucherOrder);

        //9.返回订单id
        return Result.ok(orderId);
    }

要设置这个还得加一个依赖

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

在主启动类上

@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {

    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }

}

一人一单(集群)

搭建集群

我这里搭建的集群是jvm的集群,在idea中的jvm集群

idea复用一个8082接口的应用程序

按alt + 8 可以跳出service

然后复制一份应用程序
在这里插入图片描述
更改端口
在这里插入图片描述

nginx配置

打开nginx conf文件下的nginx.conf

这里需要修改就是,下面的把注释打开,并且把上面的8081固定的关闭

这里的意思就是,请求8080,转到http://backend

然后nginx自动会轮询这两个server,一个是8081,一个是8082

在这里插入图片描述


worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/json;

    sendfile        on;
    
    keepalive_timeout  65;

    server {
        listen       8080;
        server_name  localhost;
        # 指定前端项目所在的位置
        location / {
            root   html/hmdp;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }


        location /api {  
            default_type  application/json;
            #internal;  
            keepalive_timeout   30s;  
            keepalive_requests  1000;  
            #支持keep-alive  
            proxy_http_version 1.1;  
            rewrite /api(/.*) $1 break;  
            proxy_pass_request_headers on;
            #more_clear_input_headers Accept-Encoding;  
            proxy_next_upstream error timeout;  
#             proxy_pass http://127.0.0.1:8081;
            proxy_pass http://backend;
        }
    }

    upstream backend {
        server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;
        server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;
    }  
}

更改完之后,要在cmd上重新启动一下

nginx.exe -s reload

在这里插入图片描述

问题出现

将两个应用程序以调试的模式打开

在这里打个断点
在这里插入图片描述

apifox的设置

在这里插入图片描述

第一个用户的配置

第二个用户是一样的

也就是说是同一用户,不过是不同的集群

测试

原先数据库的数据
首先是优惠券表
在这里插入图片描述
然后是优惠券订单表
在这里插入图片描述

是空的

2个接口都发起请求

发现两个应用程序都进去了

在这里插入图片描述
测试发现两个请求都进来了,这和我们的一人一单有问题,他这里会生成两个订单

如下
在这里插入图片描述
在这里插入图片描述
正常来说,我这里设了锁,应该是只能下一单,但是这里再集群情况下,下了两单,所以发生了线程安全问题!

发生问题的有原因

我门要先搞清楚集群的问题,如果是两个集群的化,那么代表的是两个jvm,相当于两个不同的进程,而我们之前那样子加锁,它的范围是jvm的内部,所以这里加锁无效,从这,就引申出分布式锁的概念

简单总结一下,就是没锁上,需要更大范围的锁!

分布式锁

分布式锁有三种实现
-在这里插入图片描述
对于mysql来说,它的互斥锁的实现就是通过事务来实现的,我们再写的时候,会再写上加锁,但我认为这个还是很难的,如果要实现的哈

用redis来实现,比较好实现,就是用setnx,来实现互斥锁

zookeeper 我还不懂,掠过

我这里的获取锁 + 释放锁,已经写好了
代码如下

    /**
     * 尝试获取锁
     * @param pattern key
     * @param value 值
     * @param <T>
     * @return
     */
    public <T> boolean tryLock(String pattern,T value)
    {
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(pattern, value, 2, TimeUnit.MINUTES);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 解锁
     * @param pattern
     */
    public void unlock(String pattern) {
        //删除锁
        redisTemplate.delete(pattern);
    }

给我封装到了我的redis 操作的工具类里边了

问题解决

我们先来看,解决问题的流程,做好一个心里预期
在这里插入图片描述
这个流程还算简洁的

我们直接看解决的代码

@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Autowired
    private RedisCache redisCache;

    /**
     * 抢购秒杀券
     *
     * @param voucherId
     * @return
     */
    @Override
//    @Transactional
    public Long seckillVoucher(Long voucherId) {
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);

        log.info("当前库存为 : {}", voucher.getStock());

        if (Objects.isNull(voucher)) {
            throw new BaseException("优惠券不存在!");
        }

        LocalDateTime nowTime = LocalDateTime.now();

        //优惠券时间是否开始了
        if (voucher.getBeginTime().isAfter(nowTime)) {
            throw new BaseException("优惠券时间还没开始!");
        }

        //是否结束了
        if (voucher.getEndTime().isBefore(nowTime)) {
            throw new BaseException("优惠券时间已经结束了");
        }

        //判断库存是否充足
        if (voucher.getStock() < 1) {
            throw new BaseException("库存不足!");
        }

        Long userId = UserHolder.getUser().getId();

        //锁的value是当前线程id
        long threadId = Thread.currentThread().getId();
        boolean isSuccess = redisCache.tryLock(RedisConstants.LOCK_SECKILL_VOUCHER_KEY, threadId + "", RedisConstants.LOCK_SECKILL_VOUCHER_TTL, TimeUnit.SECONDS);

        if (!isSuccess) {
            throw new BaseException("用户已经买过了!");
        }

        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            String lockId = redisCache.getObject(RedisConstants.LOCK_SECKILL_VOUCHER_KEY);

            //判断是否是一样的锁
            if (StrUtil.isNotBlank(lockId) && lockId.equals(Thread.currentThread().getId() + "")) {
                redisCache.unlock(RedisConstants.LOCK_SECKILL_VOUCHER_KEY);
            }
        }
    }

    @Transactional
    public synchronized Long createVoucherOrder(Long voucherId) {
        Long userId = UserHolder.getUser().getId();

        //一人一单问题
        LambdaQueryWrapper<VoucherOrder> orderWrapper = new LambdaQueryWrapper<>();
        orderWrapper.eq(VoucherOrder::getUserId, UserHolder.getUser().getId())
                .eq(VoucherOrder::getVoucherId, voucherId);

        int count = count(orderWrapper);
        if (count > 0) {
            throw new BaseException("你已经买过了!");
        }

        //扣减库存
        boolean isSuccess = seckillVoucherService
                .update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();

        if (!isSuccess) {
            throw new BaseException("扣减库存失败!");
        }


        long orderId = redisIdWorker.nextId("order");
        VoucherOrder voucherOrder = VoucherOrder.builder()
                .id(orderId)
                .userId(userId)
                .voucherId(voucherId)
                .build();

        save(voucherOrder);

        return orderId;
    }
}

改动的代码如下
在这里插入图片描述

代码很简洁,就是加一个锁,只不过这个锁是再redis中,如果获取失败,那就说明,有人再抢,有人再抢的化,就直接爆错退出,这样才符合一人一单

测试

这里的测试我就不写了,因为没什么意思,最后结果就是一人一单

小问题

这里的小问题,就是如果按照我门上面这种写法,锁住是所有的用户,而我们加锁是对各个用户加锁,做到一人一单,用户之间应该是隔离的才对,所以这里应该再锁上加上用户的标记,这样别的用户就可以进来了,去枪单

这里实现还是很简单的,就是写rediskey的时候加上 userId

在这里插入图片描述

分布式锁误删问题

因为我们加了redis分布式锁,并且这里的分布式锁,是有过期时间的,所以就会延申出这个问题

为什么要设置过期时间

我们首先先声明一点,为什么必须要加过期时间,咱们这个问题的出现就是由于这个过期时间的问题,为什么我们不能做成永久key呢?

这个问题的答案,就是如果我们做成永久key,一个线程拿到了锁,然后突然发生异常了,或者业务阻塞了, 那么就相当于说,锁释放不了了,那么程序的性能就会大大降低,甚至于我们有可能得人为去干预这个问题

虽然说,我们这个例子,理论上来说,是可以用永久key的,但是大部分的业务是不行的,所以这里要设置过期时间

问题的出现

这个问题得想一想才行

这个情况是比较极端的,但是不代表没有可能会出现

在这里插入图片描述
极端情况下,我们线程1占有了锁,然后突然业务阻塞了,但是业务阻塞的时间比锁的过期时间还要长,这就会导致业务还没结束,锁已经被释放了!


在这里插入图片描述
那么在高并发的情况下,另外一个线程2,乘虚而入,拿到了锁,并且开始执行业务

而在这个时候,在线程2拿到了锁,线程1突然醒了过来,执行了业务代码,然后去执行释放锁的代码

那么这里就会出问题了,线程1不知道这个锁已经被换了主人了,他直接就把锁释放掉了

那么会导致什么结果呢???

在高并发的情况下,线程3一看没有锁了,就乘虚而入

在这里插入图片描述

在这里插入图片描述

线程2还没执行完
线程3就拿到了锁

这样下去,当线程2完成了业务,他就释放了锁,那么此时的线程3本来持有锁的,锁被人删了,后面线程4就乘虚而入

这样就像线程1删了线程2的锁
线程2删了线程3的锁
线程3删了线程4的锁

这样子迭代下去,不出问题才怪

这个问题属于是线程安全问题

解决办法

解决办法的思想也很简单,既然你删错锁了,是因为你不知道此时的锁的主人是谁,你以为是自己的,那么我门只要在锁上写上一个标记,代表着此时的锁是谁的.我们去释放锁的时候,就去判断是不是自己的锁,这样就没什么问题了

代码

在这里插入图片描述
按道理来说这里不应该用线程id来当作标识,因为还是有可能会重复,所以应该用uuid来当标识才对

修改如下
在这里插入图片描述
这样就不会有可能是有重复的问题了

原子性问题的出现

我们看上面的解决办法,好像已经很不错了,但是还是有一个漏洞,那就是这里的流程 判断锁是不是自己 和 释放锁不是一个原子操作

在这里插入图片描述
我们来看这个图,就能看明白,这里是一个很极端的情况

首先线程1拿到锁,然后执行完业务,想要释放锁,按照我们的解决方法,我们获取锁,是不是自己,发现是, 就在这个瞬间,突然线程1发生了阻塞

这里的阻塞,有可能是jvm的垃圾回收所导致,或者其他

当我们阻塞的时间超过了锁的过期时间,就会超时释放锁

那么线程2也会乘虚而入,拿到锁

当线程1醒过来的时候,因为前面已经判断过了,所以就会去删线程2的锁

还是会出现误删问题!!!

当然了,出现这个问题的条件是很苛刻的,就是线程1在判断完锁是自己的时候,突然发生阻塞,并且阻塞的时间超过redis锁的过期时间

解决办法

所以我们要想解决这个棘手的问题,我们就要让判断锁是不是自己 和 释放锁变成一个原子操作

这里就引出了redis 的lua脚本,它可以做到原子性!

LUA脚本

简单的介绍lua脚本

它是一个脚本语言,有点类似于js

在这里插入图片描述

在redis中,执行lua脚本
在这里插入图片描述

这里的key 和 value可以不用写死,可以作为参数传递

特别要注意这里的KEYS,和ARGV数组,需要注意的是,这里的数组是从1开始的

解决上面的问题

先写一个lua脚本
在这里插入图片描述
脚本的意思就是判断锁 + 释放锁

原先java代码的改变

    /**
     * 释放锁
     */
    private void unlock(String lockKey,String uuid) {
        DefaultRedisScript<Long> longDefaultRedisScript = new DefaultRedisScript<>();
        longDefaultRedisScript.setLocation(new ClassPathResource("unlock.lua"));
        longDefaultRedisScript.setResultType(Long.class);
        redisCache.execute(
                longDefaultRedisScript,
                Arrays.asList(lockKey),
                uuid
        );
    }

这里不再我的工具类里边写unlock了,这里的unlock比较特殊所以要自己写一个方法在下边

在这里插入图片描述

这样字,这样的代码就十分健壮了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1442315.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

备战蓝桥杯---动态规划之背包问题引入

先看一个背包问题的简单版&#xff1a; 如果我们暴力枚举可能会超时。 但我们想一想&#xff0c;我们其实不关心怎么放&#xff0c;我们关心的是放后剩下的体积。 用可行性描述即可。 于是我们令f[i][j]表示前i个物品能否放满体积为j的背包。 f[i][j]f[i-1][j]||f[i-1][j-v…

数据结构-->线性表-->单链表

链表的定义 链表&#xff1a;链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 与顺序表不同的是&#xff0c;链表里的每节都是独立申请下来的空间&#xff0c;我们称之为“节点、结点”。 节点的组成主要由…

Electron+Vue实现仿网易云音乐实战

前言 这个项目是我跟着官方文档的那个Electron入门教程大致跑了一遍,了解了下Electron开发流程之后的实战项目,所以中间应该是会有很多写法不是很规范,安全性有可能也没考虑到,可实现的各种api也不是很了解,适合初学者。 必须感谢 https://github.com/Binaryify/NeteaseC…

小白水平理解面试经典题目LeetCode 71. Simplify Path【Stack类】

71. 简化路径 小白渣翻译 给定一个字符串 path &#xff0c;它是 Unix 风格文件系统中文件或目录的绝对路径&#xff08;以斜杠 ‘/’ 开头&#xff09;&#xff0c;将其转换为简化的规范路径。 在 Unix 风格的文件系统中&#xff0c;句点 ‘.’ 指的是当前目录&#xff0c;…

每日五道java面试题之java基础篇(二)

第一题. 为什么说 Java 语⾔“编译与解释并存”&#xff1f; ⾼级编程语⾔按照程序的执⾏⽅式分为编译型和解释型两种。 简单来说&#xff0c;编译型语⾔是指编译器针对特定的操作系统将源代码⼀次性翻译成可被该平台执⾏的机器码&#xff1b;解释型语⾔是指解释器对源程序逐…

vue全家桶之状态管理Pinia

一、Pinia和Vuex的对比 1.什么是Pinia呢&#xff1f; Pinia&#xff08;发音为/piːnjʌ/&#xff0c;如英语中的“peenya”&#xff09;是最接近pia&#xff08;西班牙语中的菠萝&#xff09;的词&#xff1b; Pinia开始于大概2019年&#xff0c;最初是作为一个实验为Vue重新…

基于Spark+Springboot的电商用户行为分析系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行交流合作✌ 主要内容&#xff1a;SpringBoot、Vue、SSM、HLM…

Jupyter Notebook如何在E盘打开

Jupyter Notebook如何在E盘打开 方法1&#xff1a;方法2&#xff1a; 首先打开Anaconda Powershell Prompt, 可以看到默认是C盘。 可以对应着自己的界面输入&#xff1a; 方法1&#xff1a; (base) PS C:\Users\bella> E: (base) PS E:\> jupyter notebook方法2&#x…

Python爬虫实战:抓取猫眼电影排行榜top100#4

爬虫专栏系列&#xff1a;http://t.csdnimg.cn/Oiun0 抓取猫眼电影排行 本节中&#xff0c;我们利用 requests 库和正则表达式来抓取猫眼电影 TOP100 的相关内容。requests 比 urllib 使用更加方便&#xff0c;而且目前我们还没有系统学习 HTML 解析库&#xff0c;所以这里就…

[WinForm开源]概率计算器 - Genshin Impact(V1.0)

创作目的&#xff1a;为方便旅行者估算自己拥有的纠缠之缘能否达到自己的目的&#xff0c;作者使用C#开发了一款小型软件供旅行者参考使用。 创作说明&#xff1a;此软件所涉及到的一切概率与规则完全按照游戏《原神》(V4.4.0)内公示的概率与规则&#xff08;包括保底机制&…

vscode +git +gitee 文件管理

文章目录 前言一、gitee是什么&#xff1f;2. Gitee与VScode连接大概步骤 二、在vscode中安装git1.安装git2.安装过程3.安装完后记得重启 三、使用1.新建文件夹first2.vscode 使用 四、连接git1.初始化仓库2.设置git 提交用户和邮箱3.登陆gitee账号新建仓库没有的自己注册一个4…

ElasticSearch之search API

写在前面 本文看下查询相关内容&#xff0c;这也是我们在实际工作中接触的最多的&#xff0c;所以有必要好好学习下&#xff01; 1&#xff1a;查询的分类 主要分为如下2类&#xff1a; 1:基于get查询参数的URI search 2&#xff1a;基于post body的request body search&am…

移动端设置position: fixed;固定定位,底部出现一条缝隙,不知原因,欢迎探讨!!!

1、问题 在父盒子中有一个子盒子&#xff0c;父盒子加了固定定位&#xff0c;需要子盒子上下都有要边距&#xff0c;用margin或者padding挤开时&#xff0c;会出现缝隙是子盒子背景颜色的。 测试过了&#xff0c;有些手机型号有&#xff0c;有些没有&#xff0c;微信小程序同移…

高防服务器出租的优势及特点

高防服务器出租是指租用具备高防御能力的服务器&#xff0c;用于应对网络攻击、保护网站和数据安全。那么为什么会选择高防服务器出租&#xff0c;小编为您整理发布高防服务器出租的优势及特点。 高防服务器通常具备以下特点&#xff1a; 1. 高性能硬件配置&#xff1a;高防服务…

在屏蔽任何FRP环境下从零开始搭建安全的FRP内网穿透服务

背景 本人目前在境外某大学读博&#xff0c;校园网屏蔽了所有内网穿透的工具的数据包和IP访问&#xff0c;为了实现在家也能远程访问服务器&#xff0c;就不得不先开个学校VPN&#xff0c;再登陆。我们实验室还需要访问另一个大学的服务器&#xff0c;每次我都要去找另一个大学…

JUC-并发面试题

一、基础 1、为什么要并发编程 充分利用多核CPU的资源2、并发编程存在的问题 上下文切换:PU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。任务从保…

配置git环境与项目创建

项目设计 名称&#xff1a;KOB 项目包含的模块 PK模块&#xff1a;匹配界面&#xff08;微服务&#xff09;、实况直播界面&#xff08;WebSocket协议&#xff09; 对局列表模块&#xff1a;对局列表界面、对局录像界面 排行榜模块&#xff1a;Bot排行榜界面 用户中心模块&…

神经网络(Nature Network)

最近接触目标检测较多&#xff0c;再此对最基本的神经网络知识进行补充&#xff0c;本博客适合想入门人工智能、其含有线性代数及高等数学基础的人群观看 1.构成 由输入层、隐藏层、输出层、激活函数、损失函数组成。 输入层&#xff1a;接收原始数据隐藏层&#xff1a;进行…

c#安全-nativeAOT

文章目录 前记AOT测试反序列化Emit 前记 JIT\AOT JIT编译器&#xff08;Just-in-Time Complier&#xff09;,AOT编译器&#xff08;Ahead-of-Time Complier&#xff09;。 AOT测试 首先编译一段普通代码 using System; using System.Runtime.InteropServices; namespace co…

Ubuntu22.04 gnome-builder gnome C 应用程序习练笔记(三)

八、ui窗体创建要点 .h文件定义(popwindowf.h)&#xff0c; TEST_TYPE_WINDOW宏是要创建的窗口样式。 #pragma once #include <gtk/gtk.h> G_BEGIN_DECLS #define TEST_TYPE_WINDOW (test_window_get_type()) G_DECLARE_FINAL_TYPE (TestWindow, test_window, TEST, WI…