草庐IT

SETNX分布锁详解

对方正在输入……………… 2023-04-20 原文

前提

为何要使用分布式锁?
其实使用分布式锁的目的与平常使用synchronized锁,Lock锁的目的一致,就是为了确保多线程并发时,在某些业务场景中让这些线程串行执行。
通常在一个JVM中让线程串行执行是比较容易实现的,例如synchronized锁,Lock锁等。但是现在为了应对更大的并发量,通常会将服务拆分或者搭建集群,此时synchronized锁,Lock锁就无法让处于不同服务器中的线程互斥,或者说让处于不同JVM中的线程互斥。此时就需要使用分布式锁让这些不同服务中的线程互斥,从而串行执行。

那什么是分布式锁?
分布式锁:在分布式系统下或者集群模式下能够实现多个线程之间互斥并且可见。
分布式锁的思想就是让处于不同服务器中的线程共同使用同一把锁,从而保证这些线程在某些场景下串行执行
分布式锁有那些?
说到分布式锁其实还是有很多的,像mysql分布式锁,redis分布式锁,zookeeper分布式锁等
现在我们采用Redis作为分布式锁,那么将上述图片的中的两个监视器换为一个redis在外部,那么图片就形象生动的出现在脑海中,脑补

SETNX命令的特性

SETNX:向Redis中添加一个key,只用当key不存在的时候才添加并返回1,存在则不添加返回0。并且这个命令是原子性的。
使用SETNX作为分布式锁时,添加成功表示获取到锁,添加失败表示未获取到锁。至于添加的value值无所谓可以是任意值(根据业务需求),只要保证多个线程使用的是同一个key,所以多个线程添加时只会有一个线程添加成功,就只会有一个线程能够获取到锁。而释放锁锁只需要将锁删除即可。
总结:

  1. 获取锁:通过setnx添加
  2. 释放锁:通过del将锁删除

SETNX分布式锁

设置过期时间防止死锁

假设线程1通过SETNX获取到锁并且正常执行然后释放锁那么一切ok,其它线程也能获取到锁。但是线程1现在"耍脾气"了,线程1抱怨说"工作太久有点累需要休息一下,你们想要获取锁等着吧,等我把活干完你们再来获取锁"。此时其它线程就无法向下继续执行,因为锁在线程1手中。这种长期不释放锁情况就有可能造成死锁
为了防止像线程1这种"耍脾气"的现象发生,我们可以设置key的过期时间来解决。设置过期时间过后其它线程可不会惯着线程1,其它线程表示你要休息可以,休息了指定时间把锁让出来然后拍拍屁股走人,没人惯着你。
Redis命令:

在添加时存在则添加,不存在则不添加。同时设置过期时间,单位秒
SET key value NX EX time
原命令及其参数   
SET key value [NX | XX] [GET] [EX seconds | PX milliseconds |
  EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]

JAVA代码:

//通过java代码实现SETNX同时设置过期时间
//key--键   value--值  time--过期时间  TimeUnit--时间单位枚举
stringRedisTemplate.opsForValue().setIfAbsent(key, value , time, TimeUnit);

SETNX分布式锁时误删情况

情况一
设置过期时间线程1被治得服服帖帖,此时线程1又开始不当人了。线程1想既然你抢我得锁,等你获得锁后我就将锁删除毕竟我还要有备用钥匙,让你也锁不住,让其它线程也执行
线程1休息的时间超过了过期时间,此时锁会自动释放。线程2现在脱颖而出抢到了锁然后开心的继续执行。但是现在线程1醒了,发现线程2抢走了锁。线程1表示小子胆挺肥啊,敢抢我的锁,等我执行完了就将你锁删除,让其它"哥们"也进来。此时就会发生蝴蝶效应,线程1删除了线程2的锁,线程2删除了线程3的锁,直到最后一个"哥们:wc,我锁了?"。当然线程是无感知,其实线程1乃至其它线程都不知道删除的是别人的锁,全部线程都以为删除的是自己的锁。直到最后一个线程无锁可删。
这种误删锁的情况让锁的存在荡然无存,本来应该串行执行的线程,在一定程度上都开始并发执行了。
那么误删情况该如何解决了?
我们可以给锁加上线程标识,只有锁是当前线程的才能删除,否则不能删除。在添加key的时候,key的value存储当前线程的标识,这个标识只要保证唯一即可。可以使用UUID或者一个自增数据。在删除锁的时候,将线程标识取出来进行判断,如果相同就表示锁是自己的能够删除,否则不能删除。

获取锁:

//获取线程前缀,同时也是线程表示。通过UUID唯一性
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
//与线程id组合
public boolean tryLock(long timeOut) {
        //获取线程id
        String id =ID_PREFIX+ Thread.currentThread().getId();
        //获取锁
        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, id , timeOut, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(absent);
    }

释放锁:

public void unLock() {
		//获取存储的线程标识
        String value = stringRedisTemplate.opsForValue().get(key);
        //当前线程的线程标识
        String id =ID_PREFIX+ Thread.currentThread().getId();
        //线程标识相同则删除否,则不删除
        if (id.equals(value)){
            stringRedisTemplate.delete(key);
        }
    }

情况二
加入线程标识后,线程一不能随便删除其它线程的锁,但是线程1又开始不当人了。线程1表示判断线程标识和释放锁的操作我可以分开执行,这又不是一个原子性的操作,线程1干完活以后就准备去释放锁,当线程1判断锁是自己的后表示开锁太累了,休息一会在开。此时其它线程就想无所谓,反正过期时间一到锁就会自动释放。但是线程1已经判断了锁是自己的以后就不会执行判断锁的操作(线程1已经执行了if判断,只是没有执行方法体),当线程2获得锁后,线程1仍然能删除线程2的锁。
特别说明:

  1. 线程1执行的时间片到了就会发生线程切换
  2. JVM执行垃圾回收,因为垃圾回收会暂停用户线程,让垃圾回收线程单独执行。尽管现在的CMS,G1垃圾回收器能够做到并发收集,但是CMS和G1的初始标记以及CMS的重新标记,G1的最终标记仍然需要STW(Stop The World)
public void unLock() {
        String value = stringRedisTemplate.opsForValue().get(key);
        //在此处发生垃圾回收,线程1暂停并且超过了过期时间,线程2先获得了锁
        String id =ID_PREFIX+ Thread.currentThread().getId();
        if (id.equals(value)){
        //在此处发生垃圾回收,线程1暂停并且超过了过期时间,线程2先获得锁
            stringRedisTemplate.delete(key);
        }
    }

此时就需要将判断锁的操作和删除锁的操作作为一个整体执行,要么全部成功,要么全部失败,保证删除锁的原子性。

Lua脚本解决多条命令原子性问题

关于Lua语言参考此链接:Lua
导致线程1删除线程2锁的原因就是判断锁和删除锁的操作不是原子性的。此时可以使用Lua脚本保证多条命令的原子性。
Redis提供的函数:

redis.call('命令名称', 'key', '其它参数', ...)
例如:
redis.call('set','name','jack')---->set name jack

Lua脚本: 会将key存储在KEYS数组中,会将value存储在ARVG数组中。下标从1开始

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

JAVA:通过execute()方法执行Lua脚本,DefaultRedisScript加载脚本

private static final DefaultRedisScript<Long> DEFAULT_REDIS_SCRIPT;
    static {
        DEFAULT_REDIS_SCRIPT=new DefaultRedisScript<Long>();
        DEFAULT_REDIS_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        DEFAULT_REDIS_SCRIPT.setResultType(Long.class);
    }
     public void unLock() {
        stringRedisTemplate.execute(DEFAULT_REDIS_SCRIPT, Collections.singletonList(RedisConstants.KEY_PREFIX + name),ID_PREFIX+ Thread.currentThread().getId());
    }

最后

文中提及SETNX作为分布式锁的原理以及存在的问题都是学习过程中的总结。
SETNX分布锁锁存在的问题:

  1. 重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。
  2. 不可重试:是指目前的分布式只能尝试一次,合理的情况是:当线程在获得锁失败后,应该能再次尝试获得锁。
  3. 超时释放:加锁时增加了过期时间,可以防止死锁,但是如果卡顿的时间超长,虽然采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患
  4. 主从一致性: 如果Redis提供了主从集群,当向集群写数据时,主机需要异步的将数据同步给从机,如果在同步过去之前,主机宕机了,就会出现死锁问题。

有关SETNX分布锁详解的更多相关文章

  1. ruby - 分布式事务和队列,ruby,erlang,scala - 2

    我有一个涉及多台机器、消息队列和事务的问题。因此,例如用户点击网页,点击将消息发送到另一台机器,该机器将付款添加到用户的帐户。每秒可能有数千次点击。事务的所有方面都应该是容错的。我以前从未遇到过这样的事情,但一些阅读表明这是一个众所周知的问题。所以我的问题。我假设安全的方法是使用两阶段提交,但协议(protocol)是阻塞的,所以我不会获得所需的性能,我是否正确?我通常写Ruby,但似乎Redis之类的数据库和Rescue、RabbitMQ等消息队列系统对我的帮助不大——即使我实现某种两阶段提交,如果Redis崩溃,数据也会丢失,因为它本质上只是内存。所有这些让我开始关注erlang和

  2. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  3. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  4. ruby - 停止分布式 Ruby 服务 - 2

    我有一个启动DRb服务的脚本,然后生成处理程序对象并通过DRb.thread.join等待。我希望脚本一直运行直到被明确杀死,所以我添加了trap"INT"doDRb.stop_serviceend在Ruby1.8下成功停止DRb服务并退出,但在1.9下似乎死锁(在OSX10.6.7上)。对该进程进行采样显示在semaphore_wait_signal_trap中有几个线程在旋转。我假设我在调用stop_service时做错了什么,但我不确定是什么。谁能给我任何关于如何正确处理它的指示? 最佳答案 好的,我想我已经找到了解决方案。如

  5. 【详解】Docker安装Elasticsearch7.16.1集群 - 2

    开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建

  6. 【Elasticsearch基础】Elasticsearch索引、文档以及映射操作详解 - 2

    文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就

  7. 最强Http缓存策略之强缓存和协商缓存的详解与应用实例 - 2

    HTTP缓存是指浏览器或者代理服务器将已经请求过的资源保存到本地,以便下次请求时能够直接从缓存中获取资源,从而减少网络请求次数,提高网页的加载速度和用户体验。缓存分为强缓存和协商缓存两种模式。一.强缓存强缓存是指浏览器直接从本地缓存中获取资源,而不需要向web服务器发出网络请求。这是因为浏览器在第一次请求资源时,服务器会在响应头中添加相关缓存的响应头,以表明该资源的缓存策略。常见的强缓存响应头如下所述:Cache-ControlCache-Control响应头是用于控制强制缓存和协商缓存的缓存策略。该响应头中的指令如下:max-age:指定该资源在本地缓存的最长有效时间,以秒为单位。例如:Ca

  8. IDEA 2022 创建 Spring Boot 项目详解 - 2

    如何用IDEA2022创建并初始化一个SpringBoot项目?目录如何用IDEA2022创建并初始化一个SpringBoot项目?0. 环境说明1.  创建SpringBoot项目 2.编写初始化代码0. 环境说明IDEA2022.3.1JDK1.8SpringBoot1.  创建SpringBoot项目        打开IDEA,选择NewProject创建项目。        填写项目名称、项目构建方式、jdk版本,按需要修改项目文件路径等信息。        选择springboot版本以及需要的包,此处只选择了springweb。        此处需特别注意,若你使用的是jdk1

  9. 详解Unity中的粒子系统Particle System (二) - 2

    前言上一篇我们简要讲述了粒子系统是什么,如何添加,以及基本模块的介绍,以及对于曲线和颜色编辑器的讲解。从本篇开始,我们将按照模块结构讲解下去,本篇主要讲粒子系统的主模块,该模块主要是控制粒子的初始状态和全局属性的,以下是关于该模块的介绍,请大家指正。目录前言本系列提要一、粒子系统主模块1.阅读前注意事项2.参考图3.参数讲解DurationLoopingPrewarmStartDelayStartLifetimeStartSpeed3DStartSizeStartSize3DStartRotationStartRotationFlipRotationStartColorGravityModif

  10. VMware虚拟机与本地主机进行磁盘共享(详解) - 2

    VMware虚拟机与本地主机进行磁盘共享前提虚拟机版本为Windows10(专业版,不是可能有问题)本地主机为家庭版或学生版(此版本会有问题,但有替代方式)最好是专业版VMware操作1.关闭防火墙,全部关闭。2.打开电脑属性3.点击共享-》高级共享-》权限4.如果没有everyone,就添加权限选择完全控制,然后应用确定。5.打开cmd输入lusrmgr.msc(只有专业版可以打开)如果不是专业版,可以跳过这一步。点击用户-》administrator密码要复杂密码,否则不行。推荐admaiN@1234类型的密码。设置完密码,点击属性,将禁用解开。6.如果虚拟机的windows不是专业版,可

随机推荐