分布式锁的多种实现方式

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

### 回答1: Java分布式锁实现方式多种,常见的包括: 1. 基于Redis分布式锁:利用Redis单线程的特性,使用SETNX命令创建锁,利用EXPIRE设置锁的过期时间,同时使用DEL命令释放锁,确保锁的释放是原子的。 2. 基于Zookeeper的分布式锁:通过创建临时节点实现分布式锁,当某个服务占用了锁,其它服务将无法创建同名节点,从而保证同一时间只有一个服务占用该锁。 3. 基于数据库的分布式锁:使用数据库表中的一行记录来表示锁状态,使用事务确保锁的获取和释放是原子的。 4. 基于Redisson的分布式锁Redisson是一个开源的Java分布式框架,提供了对分布式锁的支持,使用SETNX和EXPIRE命令实现锁的创建和过期,同时还提供了自旋锁、可重入锁等高级特性。 以上是Java分布式锁实现方式的几种常见方式,不同的实现方式有着各自的特点和适用场景,需要根据实际需求进行选择。 ### 回答2: Java分布式锁分布式系统中实现数据同步和控制的关键技术之一,它用于保证多个分布式进程并发访问共享资源时的数据一致性和安全性。分布式锁与普通的锁相比,需要解决跨进程、跨节点的同步和并发控制问题。 Java分布式锁实现方式有以下几种: 1. 基于Zookeeper实现分布式锁 Zookeeper是一个高性能的分布式协调服务,它可以被用来实现分布式锁。Zookeeper的实现原理是基于它的强一致性和顺序性,可以保证多个进程访问同一个分布式锁时的数据同步和控制。 通过创建一个Zookeeper的持久节点来实现分布式锁,使用create()方法来创建节点,如果创建成功则说明获取锁成功。当多个进程同时请求获取锁时,只有一个进程能够创建节点成功,其它进程只能等待。当持有分布式锁的进程退出时,Zookeeper会自动删除对应的节点,其它进程就可以继续请求获取锁。 2. 基于Redis实现分布式锁 Redis是高性能的内存数据库,可以使用它的setnx()命令来实现分布式锁。setnx()命令可以在指定的key不存在时设置key的值,并返回1;如果key已经存在,则返回0。通过这个原子性的操作来实现分布式锁。 当多个进程同时请求获取锁时,只有一个进程能够成功执行setnx()命令,其它进程只能等待。进程在持有锁期间,可以利用Redis的expire()命令来更新锁的过期时间。当持有分布式锁的进程退出时,可以通过delete()命令来删除锁。 3. 基于数据库实现分布式锁 数据库通过ACID特性来保证数据的一致性、并发性和可靠性,可以通过在数据库中创建一个唯一引来实现分布式锁。当多个进程同时请求获取锁时,只有一个进程能够成功插入唯一引,其它进程只能等待。当持有分布式锁的进程退出时,可以通过删除引中对应的记录来释放锁。 不同的实现方式各有优劣。基于Zookeeper的实现方式可以保证分布式锁的一致性和可靠性,但是需要引入额外的依赖;基于Redis可以实现较高性能的分布式锁,但是在高并发条件下可能会存在死锁等问题;基于数据库的实现方式简单,但在高并发条件下也可能会有锁争抢等问题。 总之,在选择分布式锁实现方式时,需要根据业务场景和需求来综合考虑各种因素,选择最适合自己的方式。 ### 回答3: 分布式系统中的并发控制是解决分布式系统中竞争资源的重要问题之一,而分布式锁作为一种并发控制工具,在分布式系统中被广泛采用。Java作为一种常用的编程语言,在分布式锁实现方面也提供了多种解决方案。下面就分别介绍Java分布式锁实现方式。 1. 基于ZooKeeper的分布式锁 ZooKeeper是分布式系统中常用的协调工具,其提供了一套完整的API用于实现分布式锁实现分布式锁的过程中需要创建一个Znode,表示锁,同时用于控制数据的访问。在这个Znode上注册监听器用于接收释放锁的成功/失败事件,从而控制加锁/解锁的过程。 2. 基于Redis分布式锁 Redis作为一种高性能的Key-Value数据库,其提供了完整的API用于实现分布式锁实现分布式锁的过程中需要在Redis中创建一个Key,利用Redis的SETNX命令进行加锁,同时设置过期时间保证锁的生命周期。在解锁时需要判断是否持有锁并删除对应的Key。 3. 基于数据库的分布式锁 数据库作为分布式系统中常用的数据存储方式,其提供了事务机制用于实现分布式锁。在实现分布式锁的过程中需要在数据库中创建一个表,利用数据库的事务机制实现加锁/解锁,同时需要设置过期时间保证锁的生命周期。 总之,以上三种方式都是常用的Java分布式锁实现方式。选择合适的方法需要综合考虑锁的使用场景、性能需求、可靠性要求等因素。同时,在实现分布式锁的过程中需要注意锁的加锁/解锁的正确性和过期时间的设置,保证分布式系统的并发控制的正确性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值