使用原生Redis命令实现分布式锁

news2024/11/27 4:21:44

推荐文章:

    1、springBoot对接kafka,批量、并发、异步获取消息,并动态、批量插入库表;

​    2、SpringBoot用线程池ThreadPoolTaskExecutor异步处理百万级数据;

    3、java后端接口API性能优化技巧

    4、SpringBoot+MyBatis流式查询,处理大规模数据,提高系统的性能和响应能力;

   5、SpringBoot整合多数据源,并支持动态新增与切换(详细教程)

一、为什么需要分布式锁?

       传统单体/集群开发都是 Jvm 进程内的锁如:lock锁,synchronized锁,再比如cas原子类轻量级锁,但是对于跨 Jvm 进程以及跨机器,这种锁就不适合业务场景,会存在问题。并且JDK原生的锁可以让不同线程之间以互斥的方式来访问共享资源,但若想要在不同进程之间以互斥的方式来访问共享资源,JDK原生的锁就无能为力了(对于多线程程序,避免同时操作一个共享变量而产生数据问题,我们通常会使用一把锁来互斥以保证共享变量的正确性,其使用范围是在同一个进程中,如果换做是多个进程,需要同时操作一个共享资源,如何互斥呢?现在的业务应用通常是微服务架构,这也意味着一个应用会部署多个进程,例如:多个进程如果需要修改MySQL中的同一行记录,多个进程同时启动定时任务更新数据等等,为了避免操作乱序导致脏数据,此时就需要引入分布式锁了)。

        因此,想要实现分布式锁,须借助一个外部系统,所有进程都去这个系统上申请加锁。而这个外部系统,必须要有互斥能力,即:两个请求同时进来的时候,只能给一个进程加锁成功,另一个失败。这个外部系统可以是数据库,也可以是Redis或Zookeeper,但考虑到性能,我们通常会选择使用Redis或Zookeeper来实现。

二、Redis分布式锁如何实现?

    核心思想:set ex px nx + 校验唯一随机值,再删除

        若实现分布式锁,必须要求Redis有互斥的能力。

Redis实现分布式锁的核心命令如下:

SETEX key value

       SETEX:SET IF NOT EXIST,如果指定的key不存在,则创建并为其设置值,然后返回状态码1;如果指定的key存在,则直接返回0。如果返回值为1,代表获得该锁;此时其他进程再次尝试创建时,由于key已经存在,则都会返回0,代表锁已经被占用。

// 1、加锁SETNX lock_key 1// 2、实现业务逻辑DO THINGS// 3、释放锁DEL lock_key

        当获得锁的进程处理完成业务后,再通过del命令将该key删除,其他进程就可以再次竞争性地进行创建,获得该锁。但是存在以下问题:

    1、程序处理步骤二:实现业务逻辑发生异常,没及时释放锁;

    2、进程挂了,没机会释放锁。

       以上情况会导致已经获得锁的客户端一直占用锁,其他客户端永远无法获取到锁,也即“死锁”。

三、如何解决死锁问题?

        最容易想到的方案是在申请锁时,在Redis中实现时,给锁设置一个过期时间,假设操作共享资源的时间不会超过10s,那么加锁时,给这个key设置10s过期即可。在Redis 2.6.12之后,Redis扩展了SET命令的参数,可以在SET的同时指定EXPIRE时间,这条操作是原子的,例如:以下命令是设置锁的过期时间为10秒。

命令:SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]例如:SET lock_key 1 EX 10 NX
  • EX seconds-设置指定的终止时间,以秒为单位。

  • PX milliseconds-设置指定的终止时间(以毫秒为单位)。

  • NX - 仅在不存在的情况下设置key。

  • XX - 仅设置key(如果已存在)。

但是,还有锁过期/释放了别人的锁问题:

    1、线程1加锁成功,开始操作共享资源;

    2、线程1操作共享资源耗时太久,超过了锁的过期时间,锁失效(锁被自动释放);

    3、线程2加锁成功,开始操作共享资源;

    4、线程1操作共享资源完成,在finally块中手动释放锁,但此时它释放的是客户端2的锁。

问题分析:

    1、锁过期问题:评估操作共享资源的时间不准确导致的,若只是增大过期时间,只能缓解问题降低出现问题的概率,仍然无法彻底解决问题。原因在于客户端在拿到锁之后,在操作共享资源时,遇到的场景是很复杂的,既然是预估的时间,也只能是大致的计算,不可能覆盖所有导致耗时变长的场景。

    2、释放了别人的锁问题:原因在于释放锁的操作并没有检查这把锁的归属,这样解锁不严谨。

四、如何避免锁被别人给释放?

        客户端在加锁时,设置一个只有自己才知道的唯一标识进去,例如:可以是自己的线程ID/UUID产生的值,之后在释放锁时,要先判断这把锁是否归自己持有,只有是自己的才能释放它。

4.1、使用Lua脚本

//释放锁 比较unique_value是否相等,避免误释放if redis.get("key") == unique_value(线程ID/UUID产生的值) then    return redis.del("key")

       GET + DEL两个命令需要使用Lua脚本,保证原子的执行。因为Redis处理每个请求是单线程执行的,在执行一个Lua脚本时其它请求必须等待,直到这个Lua脚本处理完成,这样一来GET+DEL之间就不会有其他命令执行了。

unlock.script脚本如下:

//Lua脚本语言,释放锁 比较unique_value是否相等,避免误释放if redis.call("get",KEYS[1]) == ARGV[1] then    return redis.call("del",KEYS[1])else    return 0end

      其中,KEYS[1]:lock_key,ARGV[1]:当前客户端的唯一标识,这两个值都是我们在执行 Lua脚本时作为参数传入的。

在java代码中的运用:

     /**     * 解锁脚本,原子操作     */    private static final String unlockScript =            "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"                    + "then\n"                    + "    return redis.call(\"del\",KEYS[1])\n"                    + "else\n"                    + "    return 0\n"                    + "end";

使用:

   /**
     * 功能描述:使用Lua脚本解锁
     * @MethodName: unlock
     * @MethodParam: [name, token]
     * @Return: boolean
     * @Author: yyalin
     * @CreateDate: 2023/7/17 18:41
     */
    public boolean unlock(String name, String token) {
        byte[][] keysAndArgs = new byte[2][];
        keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8")); //lock_key
        keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8")); //token的值,也即唯一标识符
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        try {
            Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")),
                    ReturnType.INTEGER, 1, keysAndArgs);
            if(result!=null && result>0)
                return true;
        }finally {
            RedisConnectionUtils.releaseConnection(conn, factory);
        }
        return false;
    }

五、代码实现

5.1、RedisLock类

/**
 * 功能描述:redis的分布式锁:解决并发问题
 * @Author: yyalin
 * @CreateDate: 2023/7/18 10:17
 */
@Repository
public class RedisLock {
    /**
     * 解锁脚本,原子操作
     */
    private static final String unlockScript =
            "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
                    + "then\n"
                    + "    return redis.call(\"del\",KEYS[1])\n"
                    + "else\n"
                    + "    return 0\n"
                    + "end";
​
    private StringRedisTemplate redisTemplate;
​
    //有参构造函数
    public RedisLock(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
​
    /**
     * 加锁,有阻塞
     * @param name key的值
     * @param expire 过期时间
     * @param timeout 加锁执行超时时间
     * @return
     */
    public String getLock(String name, long expire, long timeout){
        long startTime = System.currentTimeMillis(); //获取开始时间
        String token;
        //规定的时间内,循环获取有值的token
        do{
            token = tryGetLock(name, expire);  //获取秘钥Key
            if(token == null) {
                if((System.currentTimeMillis()-startTime) > (timeout-50))
                    break;
                try {
                    Thread.sleep(50); //try 50毫秒 per sec milliseconds
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return null;
                }
            }
        }while(token==null);
        return token;
    }
    /**
     * 加锁,无阻塞
     * @param name 设置key
     * @param expire
     * @return
     */
    public String tryGetLock(String name, long expire) {
        //获取UUID值为value
        String token = UUID.randomUUID().toString();
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        try{
            Boolean result = conn.set(name.getBytes(Charset.forName("UTF-8")),  //设置name为key
                    token.getBytes(Charset.forName("UTF-8")),  //设置token为value
                    Expiration.from(expire, TimeUnit.MILLISECONDS), //设置过期时间:MILLISECONDS毫秒
                    RedisStringCommands.SetOption.SET_IF_ABSENT); //如果name不存在创建
            if(result!=null && result)
                return token;
        }finally {
            RedisConnectionUtils.releaseConnection(conn, factory);
        }
        return null;
    }
​
    /**
     * 功能描述:使用Lua脚本解锁
     * @MethodName: unlock
     * @MethodParam: [name, token]
     * @Return: boolean
     * @Author: yyalin
     * @CreateDate: 2023/7/17 18:41
     */
    public boolean unlock(String name, String token) {
        byte[][] keysAndArgs = new byte[2][];
        keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8")); //lock_key
        keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8")); //token的值,也即唯一标识符
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        try {
            Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")),
                    ReturnType.INTEGER, 1, keysAndArgs);
            if(result!=null && result>0)
                return true;
        }finally {
            RedisConnectionUtils.releaseConnection(conn, factory);
        }
        return false;
    }
}

5.2、控制层调用

@ApiOperation(value="添加学生", notes="add")    @PostMapping("/add")    public void addStudent() throws InterruptedException {        String token = null;        try{            //设置锁并获取唯一值            token = redisLock.getLock("lock_name", 10*1000, 11*1000);            if(token != null) {                System.out.println("我拿到了锁哦:"+token);                // 开始执行业务代码                Thread.sleep(3*1000L);            } else {                System.out.println("我没有拿到锁唉");                //1000毫秒后过一会在尝试重新获取锁                Thread.sleep(5*1000L);                System.out.println("我开始重试来了。。。。。");                addStudent();            }        }finally {            if(token!=null) {                //用完进行释放锁                redisLock.unlock("lock_name", token);            }        }    }

六、总结

   基于Redis实现的分布式锁,一个严谨的流程如下:(set ex px nx + 校验唯一随机值,再删除)

    1、加锁时要设置过期时间SET lock_key unique_value EX expire_time NX

    2、操作共享资源(业务代码);

    3、释放锁:Lua脚本,先GET判断锁是否归属自己,再DEL释放锁。

      到此原生的redis实现分布式加锁、解锁流程就更加严谨了,可以满足大部分场景,用来解决大部分的并发问题。

更多详细资料,请关注个人微信公众号或搜索“程序猿小杨”添加。

参考:

https://huaweicloud.csdn.net/63355ebdd3efff3090b546db.html?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Eactivity-1-119251590-blog-127391210.235%5Ev38%5Epc_relevant_sort_base1&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Eactivity-1-119251590-blog-127391210.235%5Ev38%5Epc_relevant_sort_base1&utm_relevant_index=2

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/769920.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【雕爷学编程】Arduino动手做(22)——8X8 LED点阵MAX7219屏8

37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的。鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这…

Failed to initialize NVML: Driver/library version mismatch (解决)

问题描述 运行nvidia-smi报错: Failed to initialize NVML: Driver/library version mismatch解决方法 只需一步:下载一个安装包,运行一个命令来重新安装cuda driver和cuda toolkit(在一个包里)。 到这里&#xff1…

JVS开源基础框架:用户管理介绍(支持同步钉钉、企微、微信等)

在企业内部系统中,用户管理是指对系统内的用户进行管理、授权和权限管理的过程,这里主要介绍用户的创建与基本信息的管理,权限、登录等详细介绍请参考相关章节。 用户管理界面 点击平台管理-用户管理,界面上展示了组织管理与组织…

磁盘分区形式MBR与GPT介绍

磁盘分区形式MBR与GPT介绍 磁盘分区形式有两种: 1、MBR(主启动记录)形式,它是存在于磁盘驱动器开始部分的一个特殊的启动扇区; 2、GPT(GUID分区表)形式,它是一种使用UEFI启动的磁盘…

网络操作系统详解

网络操作系统的概念 操作系统是计算机系统中用来管理各种软硬件资源,提供人机交互使用的软件。网络操作系统可实现操作系统的所有功能,并且能够对网络中的资源进行管理和共享。网络操作系统(Network Operation System,NOS)是使网络上各种计算…

ai绘画怎么弄?这份ai绘画教程你必须知道

我有一个关系非常好的女性朋友,她的闺蜜即将过生日,她想给她闺蜜准备一份特别的礼物。但是,她对绘画毫无天赋。因此,我给她推荐了几款好用的ai绘画工具,这些神奇的ai绘画工具能帮助她创作出栩栩如生、美丽动人的女生形…

从零开始基于go-zero的go web项目实战-01项目初始化

从零开始基于go-zero搭建go web项目实战-01项目初始化 简介 导语 Go 是 Google 开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言,go语言的特点: 语法简洁Go语言简单易学,学习曲线平缓代码风格统一执行性能…

Spring Boot进阶(56):使用 ECharts 绘制各种花哨的统计图 | 超级详细,建议收藏

1. 前言🔥 在前几期,我们重点学习了如何在Spring Boot中使用模板引擎Thymeleaf开发Web应用的基础。接下来,我们介绍一下后端开发经常会遇到的一个场景:可视化图表。比如如下: 通常啊,这类业务在客户端应用中…

Meta与微软联手推出开源大型语言模型Llama 2;程序员如何优雅地做副业

🦉 AI新闻 🚀 Meta与微软联手推出开源大型语言模型Llama 2 摘要:Meta和微软近期合作发布了名为Llama 2的开源大型语言模型。该模型旨在帮助开发者和组织构建生成式人工智能工具和体验。Azure客户可以更轻松、安全地在Azure平台上微调和部署…

6、Java入门教程【数组】

数组是用于存储同种类型的多个数据的容器。 一、声明 //语法 dataType[] arrayRefVar; // 首选的方法 或 dataType arrayRefVar[]; // 效果相同,但不是首选方法//示例 double[] myList; // 首选的方法 或 double myList[]; // 效果相同&…

7.6Java EE——Bean的生命周期

Bean在不同作用域内的生命周期 Bean的生命周期是指Bean实例被创建、初始化和销毁的过程。在Bean的两种作用域singleton和prototype中,Spring容器对Bean的生命周期的管理是不同的。在singleton作用域中,Spring容器可以管理Bean的生命周期,控制…

2023 年最全的 5 款免费 PDF 编辑器

这里,我们回顾并比较了一些最好的开源 PDF 编辑器,以及每个编辑器的主要功能和优缺点: 寻找免费的 PDF 编辑器很困难,特别是如果您正在寻找开源 PDF 编辑器。但您不必担心在线搜索 PDF 编辑器的研究和分析,因为我们已…

Apikit 自学日记:测试模板

在 APIKIT 中,你可以将重复的测试步骤添加到测试模板库中,并且在测试用例中引用测试模板来实现复用测试步骤的目的。如将用户登录、清理数据库等操作作为测试模板,并将该用例引入到多个测试用例中,减少不必要的工作。 添加测试模板…

通达信组合副图指标公式-通达信公式

A:REF((C>O),2); A1:REF((C>O),1); A2:C>O OR CO; A3:REF(O,1)<REF(C,2); A4:O<REF(C,1); A5:REF((C-O),1)<REF((C-O),2); A6:(C-O)<REF((C-O),1); A7:REF(C,2)>REF(O,7)*1.05; 节奏1:A AND A1 AND A2 AND A3 AND A4 AND A5 AND A6 AND A7; 节奏2:A AN…

FTP与HTTP: 哪种协议更适合大文件传输?

随着互联网技术的发展&#xff0c;网络传输已成为了现代社会中不可或缺的一部分。无论是文本、图像、音频、视频等各种类型的数据&#xff0c;相应的传输协议也在不断地发展和更新。FTP&#xff08;File Transfer Protocol&#xff09;和HTTP&#xff08;Hyper Text Transfer P…

手写IOC

IOC原理(手写IOC) Spring框架的IOC是基于反射机制实现的。 反射回顾 Java反射机制是在运行状态中&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所有属性和方法&#xff0c;对于任意一个对象&#xff0c;都能够调用它的任意方法和属性&#xff1b;这种动态获取信息…

开放式蓝牙耳机推荐哪款?开放式蓝牙耳机排行榜推荐

​说到开放式耳机&#xff0c;想必很多人听着还是陌生&#xff0c;普通耳机久戴&#xff0c;会出现耳朵疼痛问题&#xff0c;而开放式蓝牙耳机没有&#xff0c;不入耳的设计更加的干净&#xff0c;不会对耳道造成的伤害&#xff0c;下面我给大家推荐几款很不错的开放式耳机&…

C++学习——类和对象(二)

紧接着我们上一部分类和对象的讲解之后&#xff0c;我们再来学习一下类当中的几大特点&#xff0c;以及使用方法。 一&#xff1a;实例化对象赋初值 首先我们需要学习的就是该如何为我们实例化出的对象赋初值。 1.外部赋值 对于对象赋初值我们有很多的形式&#xff0c;我们甚至…

【lesson1】Linux发展史

文章目录 推动技术进步的基本模式为什么科技一直在进步&#xff1f;科技进步动力是什么&#xff1f; 理解操作系统的发展计算机的发明操作系统的发展Linux的发展 开源为什么有人愿意参加开源项目呢&#xff1f;开源的好处 Linux的应用为什么Windows更好用&#xff1f;OS(操作系…

CGT Asia嘉年华|2023第四届亚洲细胞与基因治疗 创新峰会(广州站)10月升级启航

近年来&#xff0c;全球CGT发展突飞猛进&#xff0c;为遗传罕见病、难治性慢性病和肿瘤患者带来了新的希望&#xff0c;也成为整个国际领域科技竞争的未来焦点。国家发改委发布的《“十四五”生物经济发展规划》明确指出要重点发展基因诊疗、干细胞治疗、免疫细胞治疗等新技术&…