深入理解Redis分布式锁

前言

为什么需要使用 分布式锁?

传统单体开发,以及集群开发都是 Jvm 进程内的锁如lock锁,synchronized锁,再比如cas原子类轻量级锁
一旦夸 Jvm 进程以及跨机器,这种锁就不适合业务场景,会存在问题。

对此需要一个分布式锁,唯一一把锁,所有服务都只有这一把锁。

分布式锁都有哪些实现方式,这里我们只讨论 Redis 实现的分布式锁的方式以及优缺点,是否是一个严格意义上的分布式锁。

分布式锁

加锁

redis 里提供了一个命令

set key value

将字符串值 value 关联到 key 。
如果 key 已经持有其他值, SET 就覆写旧值,无视类型。

下面代码模拟了下单减库存的场景,我们分析下在高并发场景下会存在什么问题

java复制代码@RequestMapping("/deduct_stock0")
    public String deductStock0() {
        String lockKey = "lock:product_001";
        //锁和过期时间非原子性
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "test");
        //stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
        if (!result) {
            return "error_code";
        }

        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
          stringRedisTemplate.delete(lockKey);

        }


        return "end";
    }

如果单纯的实用这个命令来加锁,很有可能会造成死锁。
这个命令如果没执行完,后面所有的请求都会进不来。

如果这个命令里代码有死循环或者一直执行超时,锁就一直占着,还是会死锁。

对此,使用锁为了防止死锁,需要一个超时时间,去控制防止死锁。

加入过期时间

我们使用给key设置过期时间

java复制代码stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);

但是这两个命令不是原子性,在多线程情况下,还是会存在问题。

原子性加锁

redis 里还提供了另一种锁

setnx key value

将 key 的值设为 value ,当且仅当 key 不存在。

若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

对应到java代码

java复制代码    @RequestMapping("/deduct_stock11")
    public String deductStock11() {
        String lockKey = "lock:product_001";

        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "clientId", 30, TimeUnit.SECONDS);
        if (!result) {
            return "error_code";
        }

        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
                stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }

setIfAbsent这里将设置key和过期时间一起在执行,原子性问题解决了。

但是再多线程下还是存在不安全问题。

我们来分析一下:

深入理解Redis分布式锁_第1张图片


这是一种情况

深入理解Redis分布式锁_第2张图片

这种情况线程加的锁可能会被别的线程释放

还有其他情况暂时不分析了,总的来说这个锁还是不安全。

如何优化?

防止不同的线程删除非自己加的锁

java复制代码    @RequestMapping("/deduct_stock1")
    public String deductStock1() {
        String lockKey = "lock:product_001";
        //防止不同的线程删除非自己加的锁
        String clientId = UUID.randomUUID().toString();
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
        if (!result) {
            return "error_code";
        }

        try {

            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {

              

                stringRedisTemplate.delete(lockKey);
            }
        }


        return "end";
    }

这样看起来似乎完美了,但其实很是存在线程安全问题。

深入理解Redis分布式锁_第3张图片


key存在缓存失效,多线程场景下,这种缓存失效的概率还是很存在的,还是会导致误删锁。

Redisson分布式锁

Redis 提供了一个java客户端 redisson,实现了一个分布式锁

Redisson详情

引入依赖

java复制代码
   org.redisson
   redisson
   3.18.0
  

初始化

java复制代码    @Bean
    public Redisson redisson() {
        // 此为单机模式
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }

Redisson实现分布式锁

用法很简单

java复制代码
    @RequestMapping("/deduct_stock3")
    public String deductStock3() {
        String lockKey = "lock:product_001";
        //获取锁对象
        RLock redissonLock = redisson.getLock(lockKey);
        //加分布式锁
        redissonLock.lock();
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            //解锁
            redissonLock.unlock();
        }


        return "end";
    }

这个锁和我们 Lock 锁的实用方式是一样的,包括很多api也相似

其实Redisson对应juc下很多类都实现了一个分布式的一个api,用法都很相似。

这里截取一部分api目录图

深入理解Redis分布式锁_第4张图片

Redisson lcok锁实现原理

可以看到其内部是实现了一个看门狗,在实例未执行结束之前不断的续锁。

深入理解Redis分布式锁_第5张图片

我们来看一下源码

先看lock()加锁

深入理解Redis分布式锁_第6张图片

tryAcquire 方法

深入理解Redis分布式锁_第7张图片


tryAcquireAsync 方法

深入理解Redis分布式锁_第8张图片

这里保证线程自己的锁,也就上上面uuid那块,这里内部自己定义了uuid+thread id 表示锁是否是自己的锁

深入理解Redis分布式锁_第9张图片

真正加锁逻辑,这里是使用lua表达式 保证多个命令的原子性

深入理解Redis分布式锁_第10张图片

继续看 tryAcquireAsync 加锁方法

深入理解Redis分布式锁_第11张图片


tryLockInnerAsync 方法会异步执行,然后会回调 addListener 方法,进行重试续锁,

看门狗时间默认 30s

深入理解Redis分布式锁_第12张图片

加锁成功 scheduleExpirationRenewal 方法,嵌套执行,表示定时延迟执行

同样是使用lua表达式判断锁是否存在存在则续锁,不存在则返回0

深入理解Redis分布式锁_第13张图片

解锁

深入理解Redis分布式锁_第14张图片

同样可以看到使用 Lua 表达式保证原子性 解锁

深入理解Redis分布式锁_第15张图片

消息监听 释放信号量,唤醒其他阻塞线程

深入理解Redis分布式锁_第16张图片

大概的一个加锁逻辑流程

深入理解Redis分布式锁_第17张图片

RedLock

Redisson锁其实也存在一个问题,在主从或者集群模式下,matser加锁成功,此时还没同步到 slave,然后主节点挂了,从节点选举成功,此时从节点还是会加锁成功。这种场景会产生问题。

如何解决这种问题呢。

Redis 官网退出了 RedLock 红锁,多数节点写入成功就表示加锁成功。

深入理解Redis分布式锁_第18张图片

相比于Redisson 解决了主切换时候从节点没锁的问题,红锁就一定安全吗?

其实在一定场景下红锁也是不安全的。

场景一:redis1 redis2 redis3 加锁成功,redis2挂了,此时redis2的从选举成功,还是继续可以加锁的。
场景 二:redis1 ,2,3 其中 2,3挂了,此时加锁会加不上。如果多增加节点呢?那每个节点都要加锁成功(大多数),节点越多,加锁时间越长,影响性能。

RedLock 也不一定安全,比Redisson肯能要稍微好一点,但是带来的问题也就,节点越多,加锁性能越低,严重影响redis性能,那为什么不直接用zk加锁呢?

RedLock 加锁代码实例

java复制代码  String lockKey = "key";
        //需要自己实例化不同redis实例的redisson客户端
        RLock lock1 = redisson1.getLock(lockKey);
        RLock lock2 = redisson2.getLock(lockKey);
        RLock lock3 = redisson3.getLock(lockKey);

        /**
         * 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
         */
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        try {
            boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
            if (res) {
                //成功获得锁,在这里处理业务
            }
        } catch (Exception e) {
            throw new RuntimeException("lock fail");
        } finally {
            //最后都要解锁
            redLock.unlock();
        }

一般来说推荐使用Redisson加锁,出现问题的概率小点,毕竟技术不够人工来凑。

最后总结

  • 异步方法回调内部嵌套看门狗
  • 内部方法嵌套方式进行重试续锁
  • 使用信号量进行阻塞线程
  • unlock 解锁进行闭环,内部 redis发布监听消息解锁

你可能感兴趣的:(java,redis,分布式,java)