Redis自学之路—分布式锁(四)

news2024/10/5 18:01:43

目录

分布式锁定义

靠谱的分布式锁具备的特征

【互斥性】

【锁超时释放】

【可重入性】

【高性能和高可用】

【安全性】

Redis分布式锁方案

一、SETNX + EXPIRE

二、SETNX + value值是(系统时间+过期时间)

三、使用Lua脚本(包含SETNX+EXPIRE两条指令)

四、SET的扩展命令(SET EX PX NX)

语法解析

六、Redisson框架

七、多机实现的分布式锁Redlock+Redisson

 RedLock的实现步骤


分布式锁定义

         分布式锁是控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。

换句话理解:

        就是在Redis里面占一个“坑”,当别的进程也要来占坑时,发现那里已经有一根“大罗卜”了,就只好放弃或者稍后再试。

靠谱的分布式锁具备的特征

  • 【互斥性】

    • 任意时刻,只有一个客户端持有锁。
  • 【锁超时释放】

    • 持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
  • 【可重入性】

    • 一个线程如果获取了锁之后,可以再次对其请求加锁。
  • 【高性能和高可用】

    • 加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
  • 【安全性】

    • 锁只能被持有的客户端删除,不能被其他客户端删除

Redis分布式锁方案

一、SETNX + EXPIRE

        先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记了释放。

SETNX 是SET IF NOT EXISTS的简写,日常命令格式是SETNX key value,如果key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0.

伪代码例子:

@Autowired
private Jedis jedis;

public void test(){
    String key_resource_id = ""; // 加锁key
    String lock_value = ""; // 锁的值,可以设置任意值
    if(jedis.setnx(key_resource_id,lock_value) == 1){ // 加锁
        jedis.expire(key_resource_id,100); // 设置过期时间
        try{
            // do something  业务请求/逻辑处理
        }catch (Exception e){

        }finally {
            jedis.del(key_resource_id); // 释放锁
        }
    }
}

这个方案中,setnx和expire两个命令分开了,【不是原子操作】。如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash或者要重启维护了,那么这个锁就“长生不老”了,【别的线程永远获取不到锁啦】。

二、SETNX + value值是(系统时间+过期时间)

为了解决方案二,【发生异常锁得不到释放的场景】,可以把过期时间放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。

伪代码例子:

  • @Autowired
    private Jedis jedis;
    
    
    /**
      * SETNX + value值是(系统时间+过期时间)
    */
    public boolean test2(){
        String key_resource_id = ""; // 加锁key
        long expireTime = 1; // 过期时间,单位s
        long expireTimeStr = System.currentTimeMillis() + expireTime; // 锁的值,系统时间+设置的过期时间
        String lock_value = String.valueOf(expireTimeStr);
        // 如果当前锁不存在,返回加锁成功
        if(jedis.setnx(key_resource_id,lock_value) == 1){ // 加锁
            return true;
        }
        // 如果锁已经存在,获取锁的过期时间
        String currentValueStr = jedis.get(key_resource_id);
    
        // 如果获取到过期时间,小于系统当前时间,表示已经过期
        if(StringUtils.isNotBlank(currentValueStr) && Long.parseLong(currentValueStr) <         System.currentTimeMillis()){
            // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis 的getSet命令的小伙伴,可以去官网看看)
            String oldValueStr = jedis.getSet(key_resource_id, lock_value);
            if(StringUtils.isNotBlank(oldValueStr) && oldValueStr.equals(currentValueStr)){
                // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
                return true;
            }
        }
            
        // 其他情况,均返回加锁失败
        return false;
    }

方案二优点:

  • 巧妙移除expire单独设置过期时间的操作,把【过期时间放到setnx的value值】里面来
  • 解决了方案一发生异常,锁得不到释放的问题

方案二缺点:

  • 过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),必须要求分布式环境下,每个客户端的时间必须同步。
  • 如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖
  • 该锁没有保存持有者的唯一标识,可能被别的客户端释放/解锁。

三、使用Lua脚本(包含SETNX+EXPIRE两条指令)

实际上,还可以使用Lua脚本来保证原子性(包含setnx和expire两条指令),Lua脚本如下:

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 0
end;

伪代码例子:

/**
  * 使用Lua脚本(包含SETNX + EXPIRE两条指令)
*/
public boolean test3(){
    String key_resource_id = ""; // 加锁key
    String values = "";
    String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
                " redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
    Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
    //判断是否成功
    return result.equals(1L);
}

这个方案跟方案二对比,哪个好呢?  答:我觉得各有所长。

四、SET的扩展命令(SET EX PX NX)

除了使用方案二、方案三,保证SETNX + EXPIRE两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数!(SET key value[EX seconds][PX milliseconds][NX|XX]),它也是原子性的!

语法解析

SET key value[EX seconds][PX milliseconds][NX|XX]

  • NX:表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
  • EX seconds:设定key的过期时间,时间单位是秒。
  • PX milliseconds:设定key的过期时间,单位为毫秒。
  • XX:仅当key存在时设置值。

伪代码例子:这个语法都不对,不知道为什么(采纳网上的)

if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁
    }
}

存在问题:

  • 【锁过期释放了,业务还没执行完】。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。虽然线程b就可以获得锁成功,也开始执行临界区的代码。 那么问题就来了,临界区的业务代码都不是严格串行执行的了。
  • 【锁被别的线程误删】。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务可能都还没执行完。

五、SET EX PX NX  + 校验唯一随机值,再删除

既然锁可能被别的线程误删,那我们给value值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下,不就OK了嘛。

伪代码例子:这个语法都不对,不知道为什么 (采纳网上的)

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       //判断是不是当前线程加的锁,是才释放
       if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //释放锁
        }
    }
}

在这里,「判断是不是当前线程加的锁」「释放锁」不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。

 为了更严谨,一般也是用lua脚本代替。lua脚本如下:

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;

六、Redisson框架

方案五还是可能存在「锁过期释放,业务没执行完」的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

当前开源框架Redisson解决了这个问题。我们一起来看下Redisson底层原理图吧:

 只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redis就是使用Redisson解决了「锁过期释放,业务没执行完」问题。

七、多机实现的分布式锁Redlock+Redisson

前面六种方案都只是基于单机版的讨论,还不是很完美。其实Redis一般都是集群部署的:

 如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。

为了解决这个问题,Redis作者antirez提出一种高级的分布式算法:Redlock。

Redlock核心思想是:

        搞多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。

 RedLock的实现步骤

  • 获取当前时间,以毫秒为单位。
  • 按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,并且超过时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,跳过该master节点,尽快去尝试下一个master节点。
  • 客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如以上图,10s>30ms+50ms+40ms+50ms)
  • 如果取到了锁,key的真正有效时间就变啦,需要减去获取锁所使用的时间。
  • 如果获取锁失败(没有在至少N/2+1个,master实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。

简化步骤:

  • 按顺序向5个master节点请求加锁
  • 根据设置的超时时间来判断,是不是要跳过该master节点。
  • 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功。
  • 如果获取锁失败,解锁!


作者:筱白爱学习!!

欢迎关注转发评论点赞沟通,您的支持是筱白的动力!

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

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

相关文章

【Web服务器集群】Apache网页优化

文章目录 一、Apache网页优化概述1.优化内容2.网页压缩2.1gzip概述2.2作用2.3Apache的压缩模块概述mod_gzip模块与mod_deflate模块 3.配置网页压缩功能3.1启用网页压缩功能步骤3.2具体操作步骤 4.配置网页缓存功能4.1启用网页压缩功能步骤4.2具体操作步骤 二、Apache安全优化1.…

Unity嵌入AndroidStudio(二) IL2CPP打包

首先建立Unity工程&#xff0c;话不多说直接上图&#xff1a; 导出Android工程&#xff1a; 得到如下文件&#xff0c;备用&#xff1a; 接下来创建安卓项目&#xff1a; 注意包名要和unity里面的一致&#xff0c;sdk版本也要一致 等待编译完成&#xff1a; 打开setting.grade…

2-网络初识——IP地址和端口号

目录 1.IP地址&#xff08;分为IPV4&#xff08;默认情况下&#xff09;和IPV6&#xff09; 1.1.概念 1.2.格式 1.3.特殊IP 2.端口号 2.1.概念 2.2.格式 2.3.注意事项 网络互连的目的是进行网络通信&#xff0c;也即是网络数据传输&#xff0c;更具体一点&#xff0c;是…

SpringMVC源码分析:SpringMVC初始化(一)

一、概述 SpringMVC的初始化大概分为Spring的初始化和SpringMVC的初始化两个部分&#xff0c;他们之间的关系如下图。下面我将按照这个顺序进行详细介绍。 二、Spring初始化 ContextLoaderListener.contextInitialized进行容器的初始化。 继续点进去ContextLoader.initWebApp…

Redis五大数据结构的底层实现(未完成)

一)String类型:可以使用object encoding name就可以查看字符串的编码 SDS&#xff0c;flags的值不同&#xff0c;那么len和alloc所表示的值的数据范围也不同&#xff0c;所以flags的只是为了标识SDS头的总大小&#xff1b; alloc和len刚开始进行申请内存空间的时候都是相同的 S…

简谈你对synchronized关键字的使用

&#x1f468;‍&#x1f393;作者&#xff1a;bug菌 ✏️博客&#xff1a;CSDN、掘金、infoQ、51CTO等 &#x1f389;简介&#xff1a;CSDN|阿里云|华为云|51CTO等社区博客专家&#xff0c;历届博客之星Top30&#xff0c;掘金年度人气作者Top40&#xff0c;51CTO年度博主Top12…

Word控件Spire.Doc 【其他】教程(4):在 Word 中插入上标和下标

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

5.2.1 分类的IP地址

5.2.1 分类的IP地址 通过前面的学习我们知道IPv4协议中包含的内容非常的多&#xff0c;我们学习IPv4又分为几个方面 介绍分类的IP地址IP地址的分配与使用IP分组的格式因特网地址到物理地址的映射&#xff08;ARP协议&#xff09;&#xff0c;用以动态完成IP地址到物理地址映射…

时间序列预测 | Matlab基于灰狼算法优化支持向量机(GWO-SVM)的时间序列预测

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 时间序列预测 | Matlab基于灰狼算法优化支持向量机(GWO-SVM)的时间序列预测 评价指标包括:MAPE、MAE、RMSE和R2等,代码质量极高,方便学习和替换数据。要求2018版本及以上。 部分源码 %----------------

别再纠结页面设计!挑选小程序页面设计模板就对了

小程序页面设计模板可以是一个非常棒的选择&#xff0c;特别是如果你想要快速创建一个优秀的小程序。 以下是一些关于如何选择小程序页面设计模板的建议&#xff1a; 确定你的需求&#xff1a;在开始挑选小程序页面设计模板之前&#xff0c;你需要明确你的需求。确定你的小程…

下载安装LabVIEW

下载安装LabVIEW 介绍下载安装流程下载安装 后续 介绍 LabVIEW 是 工程 师 用来 开发 自动 化 研究、 验证 和 生产 测试 系统 的 图形 化 编 程 环境。Labview作为图形化编程语言&#xff0c;图形控件拖拽式编程&#xff0c;显得更加直观形象&#xff0c;也很容易上手学习。 …

pytorch 绘制一维热力图

热力图 热力图&#xff08;Heat Map&#xff09;是指用 X 轴 和 Y 轴 表示的两个分类字段确定数值点的位置&#xff0c;通过相应位置的矩形颜色去表现数值的大小&#xff0c;颜色深代表的数值大。 热力图是非常特殊的一种图&#xff0c;可以显示不可点击区域发生的事情。热力…

3.5. 异常处理

在Java中&#xff0c;异常是一种用于表示程序在运行过程中遇到的错误或异常情况的对象。Java提供了一套异常处理机制&#xff0c;可以帮助我们更好地处理运行时可能出现的错误和异常。异常处理的主要概念包括&#xff1a; 异常类&#xff1a;Java中的异常类是继承自Throwable类…

L1频段卫星导航射频前端低噪声放大器芯片 AT2659/AT2659S

AT2659 是一款具有高增益、低噪声系数的低噪声放大器&#xff08;LNA&#xff09;芯片&#xff0c;支持L1频段多模式全球卫星定位&#xff0c;可以应用于GPS、北斗二代、伽利略、Glonass等GNSS导航接收机中。芯片采用先进的SiGe工艺制造&#xff0c;采用1.5 mm X 1 mm 0.78 mm的…

招标采购评标专家管理数智化解决方案

评标专家作为评标活动的重要一环&#xff0c;对保证评标活动的公平公正和评标质量&#xff0c;乃至提升营商环境都意义重大。 为了加强招采过程中评标专家的监督管理、健全评标专家库制度&#xff0c;保证评标活动的公平公正&#xff0c;提高评标质量&#xff0c;国家出台了相…

Pytest 高级进阶用法Hook使用pdm打包成插件

系列文章目录 提示&#xff1a;阅读本章之前&#xff0c;请先阅读目录 文章目录 系列文章目录前言一、创建项目二、安装pdm三、使用pdm创建项目四、创建src五、src下面&#xff0c;再创建包名六、编写plugin七、编写配置pyproject.toml八、使用pdm&#xff0c;添加pytest到该插…

如何看待人工智能?——比尔盖茨谈智能时代的机遇与挑战

原创 | 文 BFT机器人 01 比尔盖茨称AI将颠覆搜索、购物网站 “你永远不会去搜索网站了&#xff0c;也不会再去亚马逊了。” 当地时间5月22日&#xff0c;盖茨在出席一场关于AI的活动时表示&#xff0c;未来的顶级AI助理将颠覆现有互联网使用方式&#xff0c;替代人们执行某些任…

PHPMySQL基础(三):处理查询SQL返回的结果集

PHP&MySQL基础&#xff08;一&#xff09;:创建数据库并通过PHP进行连接_长风沛雨的博客-CSDN博客 PHP&MySQL基础&#xff08;二&#xff09;:通过PHP对MySQL进行增、删、改、查_长风沛雨的博客-CSDN博客 目录 一、连接MySQL&#xff0c;处理错误&#xff0c;统一字…

面了个4年经验的测试,自动化都不会,真是醉了····

最近面试了一个 4 年测试经验的测试工程师&#xff0c;简历和个人介绍都提到了会自动化&#xff0c;于是我就问了几个自动化方面的问题&#xff1a; 在自动化测试中&#xff0c;你是如何选择和设计测试用例的&#xff1f;你使用过哪些自动化测试工具&#xff0c;如何选择自动化…

从C语言到C++_14(vector的常用函数+相关选择题和OJ题)

目录 1. vector的常用函数 1.1 vector 的介绍 1.2 vector 的初始化 1.3 vector 的操作和遍历 1.4 vector 的容量和增删查改 2. vector 相关笔试题 3. vector 相关OJ题 136. 只出现一次的数字 - 力扣&#xff08;LeetCode&#xff09; 解析代码&#xff1a; 118. 杨辉…