资讯 小学 初中 高中 语言 会计职称 学历提升 法考 计算机考试 医护考试 建工考试 教育百科
栏目分类:
子分类:
返回
空麓网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
空麓网 > 计算机考试 > 软件开发 > 后端开发 > Java

分布式锁的多种实现方式

Java 更新时间: 发布时间: 计算机考试归档 最新发布

分布式锁的多种实现方式

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,即每执行一次命令,进行一次持久化。

转载请注明:文章转载自 http://www.konglu.com/
本文地址:http://www.konglu.com/it/1098365.html
免责声明:

我们致力于保护作者版权,注重分享,被刊用文章【分布式锁的多种实现方式】因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理,本文部分文字与图片资源来自于网络,转载此文是出于传递更多信息之目的,若有来源标注错误或侵犯了您的合法权益,请立即通知我们,情况属实,我们会第一时间予以删除,并同时向您表示歉意,谢谢!

我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2023 成都空麓科技有限公司

ICP备案号:蜀ICP备2023000828号-2