分布式锁的多种实现方式

1、不使用分布式锁

synchronized (this){
      int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
      if (stock > 0) {
          int realStock = stock - 1;
          // 更新库存
          stringRedisTemplate.opsForValue().set("stock", realStock + "");
          // 此处执行业务代码
          System.out.println("扣减成功,剩余库存:" + realStock);
      } else {
          System.out.println("扣减失败,库存不足");
      }
      // 扣减库存操作执行完,删除这个键
      stringRedisTemplate.delete(goodsId);
      return new ResponseResult<>("执行成功", 200);
}

缺陷:
集群环境下并不能解决商品超卖BUG。真正的工作中,线上是一个集群环境(分布式环境),nginx会将请求分发到不同的后端服务上,但是因为synchronized锁是JVM进程级别的锁,也就是说是一个单机锁,并不能跨服务控制线程并发。

2、入门版分布式锁

在这里插入图片描述

@Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/deduct_stock/{goodsId}")
    @ApiOperation("秒杀减库存场景")
    public ResponseResult<?> deductStock(@PathVariable(name = "商品id") String goodsId) {
        // 将商品id作为键,存到redis。每一个线程执行减库存方法时,如果存成功,则执行扣减库存的代码,存失败说明前面有线程正在执行,需等待。
        // setIfAbsent(key,value);如果key不存在,则创建这个key,否则什么也不做
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(goodsId, "分布式锁");

        if (Boolean.TRUE.equals(flag)) {
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                // 更新库存
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
            // 扣减库存操作执行完,删除这个键
            stringRedisTemplate.delete(goodsId);
            return new ResponseResult<>("执行成功", 200);
        } else {
            return new ResponseResult<>("当前商品抢购繁忙,请稍后再试", 210);
        }
    }

缺陷:

  1. 在删除锁之前,如果业务代码有异常,则锁无法删除,死锁!
  2. 如果请求执行到一半宕机了,锁无法删除,死锁!

2.1、优化

  1. 业务代码通过try-catch-finally代码块包裹一下,删除锁的操作放在finally里。
  2. 给锁设个过期时间。
@Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/deduct_stock/{goodsId}")
    @ApiOperation("秒杀减库存场景")
    public ResponseResult<?> deductStock(@PathVariable(name = "商品id") String goodsId) {
        // 将商品id作为键,存到redis。每一个线程执行减库存方法时,如果存成功,则执行扣减库存的代码,存失败说明前面有线程正在执行,需等待。
        // setIfAbsent(key,value);如果key不存在,则创建这个key,否则什么也不做
        // 设置锁过期时间为10s
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(goodsId, "分布式锁", 10, TimeUnit.SECONDS);
        if (Boolean.FALSE.equals(flag)) {
            return new ResponseResult<>("当前商品抢购繁忙,请稍后再试", 210);
        }
        try {
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                // 更新库存
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } catch (Exception e) {

        } finally {
            // 扣减库存操作不管成功与否,都删除这个键
            stringRedisTemplate.delete(goodsId);
        }
        return new ResponseResult<>("执行成功", 200);
    }

但还是有缺陷:
上述操作虽然避免了死锁问题,但不能解决误删锁的问题。因为业务代码的执行时间是不可控的,假设给锁设置过期时间为10s,而业务代码执行完需要15s,就会导致第一个请求还没执行完,锁就已经删掉了。这时第二个请求会创建锁,恰巧执行到一半时第一个请求执行完,删了第二个请求加的锁。这种极端情况下,有锁和没锁一样,很容易造成库存的脏读,导致超卖BUG。

2.2、再优化

  1. 每一次请求都生成一个uuid作为锁的值,删除锁时先判断这个锁是否属于当前线程,如果是则删除这个锁
    @GetMapping("/deduct_stock/{goodsId}")
    @ApiOperation("秒杀减库存场景")
    public ResponseResult<?> deductStock(@PathVariable String goodsId) {
        String clientId = IdUtil.randomUUID();
        // 将商品id作为键,存到redis。每一个线程执行减库存方法时,如果存成功,则执行扣减库存的代码,存失败说明前面有线程正在执行,需等待。
        // setIfAbsent(key,value);如果key不存在,则创建这个key,否则什么也不做
        // 设置锁过期时间为10s
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(goodsId, clientId, 10, TimeUnit.SECONDS);
        if (Boolean.FALSE.equals(flag)) {
            return new ResponseResult<>("当前商品抢购繁忙,请稍后再试", 210);
        }
        try {
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                // 更新库存
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } catch (Exception e) {

        } finally {
            // 扣减库存操作不管成功与否,只要这个锁属于当前线程,都要删除这个锁
            String lockValue = stringRedisTemplate.opsForValue().get(goodsId);
            if (clientId.equalsIgnoreCase(lockValue)) {
                stringRedisTemplate.delete(goodsId);
                log.info("删除锁。。。");
            }
        }
        return new ResponseResult<>("执行成功", 200);
    }

还有BUG:
在这里插入图片描述
解决方案:
锁续命方案,每一次请求开一个分线程执行定时任务,定时查询锁有没有过期,如果没过期则延长过期时间到10s。

3、Redisson实现分布式锁

@GetMapping("/deduct_stock_redisson/{goodsId}")
    @ApiOperation("redisson实现分布式锁")
    public ResponseResult<?> deductStockByRedisson(@PathVariable String goodsId) {
        // 获取锁对象
        RLock redissonLock = redisson.getLock(goodsId);
        // 加锁
        redissonLock.lock();
        try {
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                // 更新库存
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } catch (Exception e) {

        } finally {
            // 释放锁
            redissonLock.unlock();
        }
        return new ResponseResult<>("执行成功", 200);
    }

原理:
在这里插入图片描述
分布式锁是串行化操作,与并发编程的并行执行相违背,但可以采取一些方法优化性能。

  1. 锁的范围粒度越小越好。可以不在锁代码块里的尽量挪出去。
  2. 线程安全的明发map。分段锁

Redisson实现分布式锁并非绝对安全:
因为redis的主从复制功能,线程1需要先向master节点上加锁,master节点加锁成功后会将加锁命令同步到各个slave节点。假设master节点向slave节点同步的时候突然挂了,这时候slave节点并没有加锁成功,那么线程2就会在slave节点上加锁,依旧会出现超卖情况。

4、redLock

在这里插入图片描述

实现原理:
RedLock的实现原理就是使用Redis实现分布式锁,通过搭建多个独立的没有主从关系的redis,每次加锁都要往所有redis上加锁,超过一半(也有说所有)节点加锁成功才算加锁成功。同时每一个独立redis都不要进行主从复制,以免出现主节点宕机造成锁丢失。锁记得加上过期时间避免死锁。redis的持久化也必须要选择always,即每执行一次命令,进行一次持久化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值