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

推荐文章:

    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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一零六四、世界杯数据可视化分析(阿里云天池赛)

目录 赛制官方链接 活动背景 活动时间:即日起-12月31日17点 数据说明 世界杯成绩信息表:WorldCupsSummary 世界杯比赛比分汇总表:WorldCupMatches.csv 世界杯球员信息表:WorldCupPlayers.csv 代码实现 赛制官方链接 世界杯…

Git 学习笔记

Git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多! Git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。…

使用typora+PicGo+Gitee简单实现图片上传功能

本文通过配置PicGoGitee来实现typora图片上传功能,系统是window 注意下载的清单有:PicGo,node.js,配置有:PicGo,node.js,gitee,typora 看着复杂实际上并不难,只是繁琐&am…

ADC 的初识

ADC介绍 Q: ADC是什么? A: 全称:Analog-to-Digital Converter,指模拟/数字转换器 ADC的性能指标 量程:能测量的电压范围分辨率:ADC能辨别的最小模拟量,通常以输出二进制数的位数表示,比如&am…

HttpClient使用MultipartEntityBuilder上传文件时乱码问题解决

HttpClient使用MultipartEntityBuilder是常用的上传文件的组件,但是上传的文件名称是乱码,一直输出一堆的问号: 如何解决呢?废话少说,先直接上代码: public static String doPostWithFiles(HttpClient http…

scripy其他

持久化 # 爬回来,解析完了,想存储,有两种方案 ## 方案一:一般不用 parse必须有return值,必须是列表套字典形式--->使用命令,可以保存到json格式中,csv中scrapy crawl cnblogs -o cnbogs.j…

【精华】maven 生命周期 + 依赖传递+ scope【依赖范围】 + 排除依赖 可选依赖

目录 一 . lifecycle 生命周期 二. 依赖 与 依赖传递 三. scope 依赖范围 scope指定依赖范围 依赖传递依赖与原依赖冲突 四 maven的可选依赖与排除依赖 可选依赖 全部 排除依赖 显式的指定 maven官网技术文档: 一 . lifecycle 生命周期 * clean&…

基于appium的常用元素定位方法

目录 一、元素定位工具 1.uiautomatorviewer.bat 2.appium检查器 二、常用元素定位方法 1.id定位 2.class_name定位 3.accessibility_id定位 4.android_uiautomator定位 5.xpath定位 三、组合定位 四、父子定位 五、兄弟定位 总结: 一、元素定位工具 app应…

postgresql regular lock常规锁申请与释放 内幕 以及fastpath快速申请优化的取舍

​专栏内容: postgresql内核源码分析 手写数据库toadb 并发编程 个人主页:我的主页 座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物. 定义 每种常规锁都需要定义几个要素,它由结构体 Lo…

边缘检测之loG算子

note // 边缘检测之loG算子:对高斯函数求二阶导数 // G(x,y) exp(-1 * (x*x y*y) / 2 / sigma / sigma) // loG(x,y) ((x*x y*y - 2 * sigma * sigma) / (sigma^4)) * exp(-1 * (x*x y*y) / 2 / sigma /sigma) /* [ 0,0,-1,0,0; 0,-1,-2,-1,0; -1,-2,16,-2…

(栈队列堆) 剑指 Offer 09. 用两个栈实现队列 ——【Leetcode每日一题】

❓ 剑指 Offer 09. 用两个栈实现队列 难度:简单 用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead …

Shikra:新一代多模态大语言模型,理解指向,说出坐标

“ Shikra:解锁多模态语言模型参考对话的魔法” Shikra和用户的对话案例 在人类的日常交流中,经常会关注场景中的不同区域或物体,双方都可以通过说话并指向这些区域来进行高效的信息交换。我们将这种对话模式称为参考对话(Referen…

C语言 替换gets函数

目录 替换gets函数gets()用处gets()的危险之处gets()的几种替代方法一、用%c循环输入直到遇到换行结束二、用getchar()循环输入直到遇到换行结束三、scanf的另一种用法四、c中的getline()方法五、解决方案使用fgets代替 替换gets函数 gets()用处 gets从标准输入设备读字符串函…

C# Linq 详解四

目录 概述 二十、SelectMany 二十一、Aggregate 二十二、DistinctBy 二十三、Reverse 二十四、SequenceEqual 二十五、Zip 二十六、SkipWhile 二十七、TakeWhile C# Linq 详解一 1.Where 2.Select 3.GroupBy 4.First / FirstOrDefault 5.Last / LastOrDefault C# Li…

truffle 进行智能合约测试

本方法使用了可视化软件Ganache 前两步与不使用可视化工具的步骤是一样的(有道云笔记),到第三步的时候需要注意: 在truffle插件下找到networks目录,提前打开Ganache软件 在Ganache中选择连接或者新建,我在…

软件测试测试用例

等价类:把输入的数据可以分为有效的数据和无效的数据 被测试的对象输入的数据: 1、有效的数据 2、无效的数据 测试一个产品,需要考虑它的正确场景,也需要考虑它的异常场景 边界值:边界值测试用例是针对等价类测试用例方法的补…

每天一道C语言编程:排队买票

题目描述 有M个小孩到公园玩,门票是1元。其中N个小孩带的钱为1元,K个小孩带的钱为2元。售票员没有零钱,问这些小孩共有多少种排队方法,使得售票员总能找得开零钱。注意:两个拿一元零钱的小孩,他们的位置互…

Thymeleaf + Layui+快速分页模板(含前后端代码)

发现很多模块写法逻辑太多重复的&#xff0c;因此把分页方法抽取出来记录以下&#xff0c;以后想写分页直接拿来用即可&#xff1a; 1. 首先是queryQrEx.html&#xff1a; <!DOCTYPE html> <html xmlns:th"http://www.w3.org/1999/xhtml"> <head>…

zabbix监控自己

目录 一、实验环境准备 二、server端 1、配置阿里云yum源 2、部署lamp环境 3、启动lamp对应服务 4、准备java环境 5、源码安装zabbix 6、mariadb数据库授权 7、创建zabbix程序用户并授权防止权限报错 8、修改zabbix配置文件 9、配置php与apache 10、web安装zabbix …

Qgis3.16ltr+VS2017二次开发环境搭建(保姆级教程)

1.二次开发环境搭建 下载osgeo4w-setup.exeDownload QGIShttps://www.qgis.org/en/site/forusers/download.html 点击OSGeo4W Network Installer 点击下载 OSGeo4W Installer 运行程序 osgeo4w-setup.exe&#xff0c;出现以下界面&#xff0c;点击下一页。 选中install from i…