• Redis(十一) 分布式锁


    一.分布式锁

    1.1 为什么要使用分布式锁

    例如一个简单的用户操作,一个线程去修改用户的状态,首先从数据库中读出用户的状态,然后在内存中进行修改,修改完成后,再存回去。
    如果是在单线程中,这个操作没有问题的。
    如果是在多线程中,在我们进行修改的时候。先读取数据,再修改数据,最后存取数据,这是三个操作并不是一步完成的,所以在多线程中,这样做就有问题了。

    1.2 分布式锁基本用法

    分布式锁实现的思路很简单,就是进来一个线城先占位,当别的线城进来操作时,发现已经有人占位了,就会放弃或者稍后再试。

    注意:分布式锁操作中,我们一般使用Redis的setnx指令先进来的线程先占位,线程的操作执行完成后,再去调用del指令去释放所占用的位置。

    方法一: 用普通多线程的思想去解决这个问题

    实际操作:

    public class LockTest {
        public static void main(String[] args) {
            Redis redis = new Redis();
            redis.execute(jedis->{
                Long setnx = jedis.setnx("k1", "v1");
                if (setnx == 1) {
    //没人占位
                    jedis.set("name", "javaboy");
                    String name = jedis.get("name");
                    System.out.println(name);
                    jedis.del("k1");//释放资源
                }else{
    //有人占位,停止/暂缓 操作
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    方法二: 解决方法一中的抛出异常和线程挂掉的问题

    注意: 上面这样操作是有问题的。

    如果业务执行过程中要抛异常或者挂了,这样就会导致del指令没有被调用,这样的话,k1就无法被释放,导致后来的请求会全部的堵塞在这里,锁没办法被释放。

    问题解决的办法:给这个锁去设置一个过期的时间,确保锁在一定时间后一定会被释放,以防止抛出异常或者直接挂掉所导致的锁被占用。改进后的代码如下:

    public class LockTest {
        public static void main(String[] args) {
            Redis redis = new Redis();
            redis.execute(jedis->{
                Long setnx = jedis.setnx("k1", "v1");
                if (setnx == 1) {
                    //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
                    jedis.expire("k1", 5);
                    //没人占位
                    jedis.set("name", "javaboy");
                    String name = jedis.get("name");
                    System.out.println(name);
                    jedis.del("k1");//释放资源
                }else{
                    //有人占位,停止/暂缓 操作
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    方法三:解决方法二中的服务器挂掉所导致的锁没办法释放的问题

    注意:这么写还是会有问题,就是在获取锁和设置过期时间之间如果服务器突然挂掉了,还是会导致锁被占用无法及时得到释放,还是会造成死锁,因为获取锁和设置过期时间其实是两个操作,不具备原子性。
    为了解决这个问题,从Redis2.8开始,setnx和expire可以通过一个命令来一起执行了,所以我们再进行改进一下。

    public class LockTest {
        public static void main(String[] args) {
            Redis redis = new Redis();
            redis.execute(jedis->{
                String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
                if (set !=null && "OK".equals(set)) {
    //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
                    jedis.expire("k1", 5);
    //没人占位
                    jedis.set("name", "javaboy");
                    String name = jedis.get("name");
                    System.out.println(name);
                    jedis.del("k1");//释放资源
                }else{
    //有人占位,停止/暂缓 操作
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    方法四:解决上述方法中遗留的设置锁时间的问题

    注意:在方法二中,为了去解决业务代码在执行的时候抛出异常,我们给每一个锁通过jedis.expire("k1", 5);设置一个超时时间去设置了一个超时时间,即,当锁被占用的时间超时了之后,锁会被自动释放,例如:在此处我们设置的超时时间是5s,超过了5s,这个锁就被自动释放了。

    但是这也带来了一个新问题:如果要执行的业务非常耗时,可能会出现紊乱。
    举个例子:第一个线程首先获取到锁,然后去执行业务代码,但是我们第一个线程的业务非常麻烦,花费了8s,但是当执行到第五秒不就超时了吗,就会自动去释放第一个线程锁占用的锁。然后在第五秒的时候第二个线程进来占位使用了,可是当第八秒的时候,第一个线程执行完毕了,此时第一个线程就会被释放,就会去执行释放锁的操作,但是他不是没有锁了吗,他就去释放了第二个线程的锁,释放完了之后第三个线程又进来了,冤种线程们就重复执行上面的步骤,越弄越乱。
    那么面对这种问题我们要怎么去解决?

    一般来说有两种方法:

    方法1.尽量避免在获取锁之后,执行耗时操作。
    方法2.可以在锁上做文章,将锁的value设置为一个随机的字符串,这样当每次释放锁的时候,都去比骄傲随机的字符串是否相同,如果相同,就再去执行释放的流程,不如不相同就不用释放。

    对于方法2,由于释放锁的时候,要去第一步要去查看锁的value,第二步要去比较value的值是否正确,第三步释放锁。这三个步骤,但是这三个步骤明显不具备原子性,为了解决这个问题,我们去引入Lua脚本。

    Lua脚本的优势:

    优势1:使用方便,Redis中内置了对Lua脚本的支持。
    优势2:Lua脚本可以在Redis服务端原子的去执行多Redis命令
    优势3:由于网络在很大程度上会影响到Redis的性能,而使用Lua脚本可以让多个命令一次被执行,可以有效的解决网络给Redis造成的性能的问题。

    为什么要使用Lua脚本:尽管Redis在6的时候已经默认使用多线程了,但是本质最核心的还是单线程来进行操作的,就会出现很多问题,但是如果我们在Redis中使用Lua脚本的话,Redis就默认Lua脚本中一系列的操作为一个原子操作。(Redis中默认支持了Lua脚本的支持,我们可以直接使用)

    在Redis中,使用Lua脚本,主要以两种思路:
    1.提前在Redis服务端i下好Lua脚本,然后在java客户端去调用脚本(这里我们推荐这种方法)
    2.可以直接在Java端去写Lua的脚本,写好后需要执行的时候每次将脚本发送到Redis上去执行。

    本次Lua脚本内容如下:

    if redis.call("get",KEYS[1])==ARGV[1] then
    return redis.call("del",KEYS[1])
    else
    return 0
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5

    解释

    第一行解释:
    if redis.call(“get”,KEYS[1])==ARGV[1] then
    首先redis.call 表示redis要去执行接下来的命令
    其次 (“get”,KEYS[1])==ARGV[1] 表示等下我java代码去调用这个Lua脚本的时候要去传递两种参数,第一组参数是keys,第二组参数叫ARGV,而KEYS[1]指的是我等下传的KEYS里面的第一个参数,即等下Redis执行这一段Lua脚本我们JAVA要去传递三个参数,第一个参数是要执行的Lua脚本的参数,第二个参数是KEYS的list集合,第三个参数是ARGV的list集合,1就是访问第一个

    linux上操作的步骤

    1.创一个Redis中创一个lua文件夹
    2.进入创一个xxx.lua 文件
    3.vi进入拷贝入我们上面的代码
    在这里插入图片描述

    在这里插入图片描述

    退到redis路径下去执行

    src/redis-cli -a 123 -x script load < lua/mylua.lua
    lua/mylua.lua解释
    lua是我的文件夹
    mylua.lua是我的lua文件的名字,你的看着更改

    在这里插入图片描述

    java中调用脚本即可

    public static void main(String[] args) {
            new Redis().execute(jedis -> {
    //            Long result = jedis.setnx("k1", "v1");
                //下面这个是 setnx 和 setex 二合一
                String value = UUID.randomUUID().toString();
                String result = jedis.set("k1", value, new SetParams().nx().ex(10L));
                if ("OK".equals(result)) {
    //                jedis.expire("k1", 30);
                    //说明没有人往 redis 存 k1
                    jedis.set("name", "zhangsan");
                    try {
                        Thread.sleep(20000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //如果这两个值相等,说明目前的锁就是我自己加的,如果不相等,说明这个锁是别人加的
    //                if (jedis.get("k1").equals(value)) {
    //                    jedis.del("k1");
    //                }
                    jedis.evalsha("c2ee3882740fd0eff9dc0125fa36eb206831cf94", Arrays.asList("k1"), Arrays.asList(value));
                } else {
                    //说明有人正在操作,停一会重试
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
  • 相关阅读:
    内核IPv4路由选择子系统(简述)
    价值32k!阿里顶级架构师深度解析SpringBoot进阶原理实战手册
    大模型系统和应用——神经网络基础
    Java开发模拟面试记录
    OpenCV(三十八):二维码检测
    什么是研发效能DevOps?研发效能方程式又是什么?
    先进的管理技术将其转化成为有机的、高效的商业模式的管理集成
    IP地址追踪具体位置:技术和隐私考虑
    DSA之图(2):图的存储结构
    纸业供应链协同管理系统:重构纸业智慧供应网络,支撑企业数字化转型升级
  • 原文地址:https://blog.csdn.net/weixin_43189971/article/details/126373143