IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> Redis(十一) 分布式锁 -> 正文阅读

[大数据]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{
//有人占位,停止/暂缓 操作
            }
        });
    }
}

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

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

如果业务执行过程中要抛异常或者挂了,这样就会导致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{
                //有人占位,停止/暂缓 操作
            }
        });
    }
}

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

注意:这么写还是会有问题,就是在获取锁和设置过期时间之间如果服务器突然挂掉了,还是会导致锁被占用无法及时得到释放,还是会造成死锁,因为获取锁和设置过期时间其实是两个操作,不具备原子性。
为了解决这个问题,从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{
//有人占位,停止/暂缓 操作
            }
        });
    }
}

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

注意:在方法二中,为了去解决业务代码在执行的时候抛出异常,我们给每一个锁通过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

解释

第一行解释:
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 {
                //说明有人正在操作,停一会重试
            }
        });
    }
}
  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 19:12:48  更:2022-08-19 19:14:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 20:02:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码