In-depth understanding of Redis distributed locks

In-depth understanding of distributed locks

foreword

Why do you need to use distributed locks?

Traditional single development and cluster development are all locks in the Jvm process, such as locklocks, synchronizedlocks, and cas原子类lightweight locks
. Once the Jvm process is exaggerated and cross-machine, this kind of lock is not suitable for business scenarios, and there will be problems.

For this, a distributed lock is required, the only lock, and all services have only this lock.

What are the implementation methods of distributed locks? Here we only discuss the methods and advantages and disadvantages of distributed locks implemented by Redis, and whether it is a distributed lock in the strict sense.

distributed lock

lock

A command is provided in redis

set key value

Associates the string value value to key.
If the key already holds another value, SET overwrites the old value, regardless of the type.

The following code simulates the scenario of placing an order and reducing inventory. Let's analyze what problems will exist in the high concurrency scenario

@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";
    }
复制代码

If you simply use this command to add a lock, it is likely to cause a deadlock.
If this command is not executed, all subsequent requests will not be able to enter.

If the code in this command has an infinite loop or has been executed overtime, the lock will always be occupied, and it will still be deadlocked.

In this regard, in order to prevent deadlocks by using locks, a timeout period is required to control and prevent deadlocks.

add expiration time

We use to set the expiration time for the key

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

But these two commands are not atomic, and there will still be problems in multi-threaded situations.

atomic locking

Another lock is provided in redis

setnx key value

Set the value of key to value if and only if key does not exist.

If the given key already exists, SETNX does nothing.
SETNX is an abbreviation for "SET if Not eXists" (SET if not present).

Corresponding to java code

    @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";
    }
复制代码

setIfAbsentHere, the key will be set and the expiration time will be executed together, and the atomicity problem will be solved.

But there are still insecurity problems under multi-threading.

Let's analyze it:

在这里插入图片描述
this is a situation

在这里插入图片描述

In this case, the lock added by the thread may be released by other threads

There are other situations that will not be analyzed for the time being. Generally speaking, this lock is still not safe.

How to optimize?

Prevent different threads from deleting locks not added by themselves

    @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";
    }
复制代码

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

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

Redisson分布式锁

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

Redisson详情

引入依赖

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.18.0</version>
</dependency>  
复制代码

初始化

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

Redisson实现分布式锁

用法很简单


    @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目录图
在这里插入图片描述

Redisson lcok锁实现原理

可以看到其内部是实现了一个看门狗,在实例未执行结束之前不断的续锁。
在这里插入图片描述

我们来看一下源码

先看lock()加锁

在这里插入图片描述

tryAcquire 方法

在这里插入图片描述
tryAcquireAsync 方法

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

继续看 tryAcquireAsync 加锁方法
在这里插入图片描述
tryLockInnerAsync 方法会异步执行,然后会回调 addListener 方法,进行重试续锁,

看门狗时间默认 30s
在这里插入图片描述

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

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

在这里插入图片描述

解锁

在这里插入图片描述

同样可以看到使用 Lua 表达式保证原子性 解锁
在这里插入图片描述

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

在这里插入图片描述

大概的一个加锁逻辑流程
在这里插入图片描述

RedLock

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

如何解决这种问题呢。

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

在这里插入图片描述

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

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

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

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

RedLock 加锁代码实例

  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发布监听消息解锁

深入理解信号量Semaphore

Guess you like

Origin juejin.im/post/7231804369523015740