濮阳建站建设,网页搜索历史怎么找到,wordpress 提交插件,免费做网站平台目录
前言
基本介绍
演化过程
防死锁
防误删
自动续期
可重入
主从一致
总结 前言
在我们没有了解分布式锁前#xff0c;使用最多的就是线程锁和进程锁#xff0c;但他们仅能满足在单机jvm或者同一个操作系统下#xff0c;才能有效。跨jvm系统#xff0c;无法…目录
前言
基本介绍
演化过程
防死锁
防误删
自动续期
可重入
主从一致
总结 前言
在我们没有了解分布式锁前使用最多的就是线程锁和进程锁但他们仅能满足在单机jvm或者同一个操作系统下才能有效。跨jvm系统无法满足。因此就产生了分布式锁完成锁的工作。
分布式锁是一种用于在分布式系统中实现同步和互斥访问的机制。在分布式系统中多个节点同时访问共享资源可能会导致数据不一致或竞争条件的发生。分布式锁提供了一种保护共享资源的方式以确保在任意时刻只有一个节点可以访问该资源。
本文将会带你梳理基于redis分布式锁的设计演化让你对分布式锁不再恐惧简简单单拿捏它让你跟别人聊的时候做到侃侃而谈有条不紊。
基本介绍
一个好的分布式锁应该满足以下条件
互斥性任意时刻只能有一个客户端才能获取锁防止死锁 分布式锁应该设计成在锁的持有者异常退出或崩溃时能够自动释放以防止死锁的发生。一般通过设置合适的锁超时时间来避免死锁。高可用性 在节点故障时也能正常工作确保锁的可靠性。可重入性允许同一个线程或客户端在持有锁的情况下多次获取同一个锁而不会出现死锁或阻塞的情况。这对于递归函数调用等场景尤其重要。唯一标识 分布式锁应该具备唯一的标识以便客户端可以识别和管理不同的锁。
我们实现分布式锁借助于redis中的命令setnx(key, value)key不存在就新增存在就什么都不做。如果同时有多个客户端发 送setnx命令只有一个客户端可以成功返回1true其他的客户端返回0false。最基础的实现流程
多个客户端同时获取锁setnx获取成功执行业务逻辑执行完成释放锁del其他客户端等待重试
演化过程
防死锁
原因我们试想一个场景假如客户端拿到了锁但在执行业务流程的过程中发生了宕机这个时候业务没有执行完成拿到的锁也是无法释放的导致其他客户端线程一直在阻塞无法获取到锁。
解决给锁添加过期时间如果发生了宕机让它主动释放锁。
给锁设置过期时间自动释放锁。 设置过期时间两种方式
1. 通过expire设置过期时间缺乏原子性如果在setnx和expire之间出现异常锁也无法释放
2. 使用set指令设置过期时间set key value ex 3 nx既达到setnx的效果又设置了过期时间
防误删
原因给锁上了过期时间以后如果设置这个过期时间过短了就可能会出现误删的情况比如一个业务需要执行5s但是锁的过期时间为3s首先线程1获得了锁3s后锁过期自动释放3s后被另外一个线程拿到了锁在第5s时线程1执行业务完成进行锁的释放这个时候就会把线程2的锁的释放掉了。当然这个设置的过期时间本身就不合理的按照道理来说设置的过期时间应大于业务的执行时间如果不确定后面会提到自动续期解决这个问题。
解决setnx获取锁时设置一个指定的唯一值例如uuid释放前获取这个值判断是否自己的锁删除的时候需要满足原子性即判断跟删除是原子的可以通过lua脚本实现。 如果不是原子的话就可能出现以下问题 index1执行删除时查询到的lock值确实和uuid相等index1执行删除前lock刚好过期时间已到被redis自动释放index2获取了lock index1执行删除此时会把index2的lock删除 脚本如下 if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del,
KEYS[1]) else return 0 end 自动续期
原因在使用 redis 分布式锁时为避免持有锁的使用方因为异常状况导致无法正常解锁进而引发死锁问题我们可以使用到 redis 的数据过期时间 expire 机制这种 expire 机制的使用会引入一个新的问题——过期时间不精准因为此处设置的过期时间只能是一个经验值通常情况下偏于保守既然是经验值那就做不到百分之百的严谨性。
试想假如占有锁的使用方在业务处理流程中因为一些异常的耗时如 IO、GC等导致业务逻辑处理时间超过了预设的过期时间就会导致锁被提前释放. 此时在原使用方的视角中锁仍然持有在自己手中但在实际情况下锁数据已经被删除其他取锁方可能取锁成功于是就可能引起一把锁同时被多个使用方占用的问题锁的基本性质——独占性遭到破坏。
解决原生的redis可以Timer定时器 lua脚本实现锁的自动续期也就是另起一个线程开启一个定时任务不断的判断锁的过期时间如果快到了进行自动续期即可同时对redis当然也可以采用redission框架中的看门狗
在执行 redis 分布式锁的上锁操作时通过 setNEX 指令完成锁数据的设置携带了一个默认的锁数据过期时间确认上锁成功后异步启动一个 watchDog 守护协程按照锁默认过期时间 1/4 ~ 1/3 的节奏可自由设置持续地对锁数据进行 expire 续期操作在解锁成功后会负责关闭 watchDog回收协程资源.由于看门狗续期操作会先检查锁的所有权再延期数据因此实际上使用方只要删除了锁数据续期操作就不会生效了. 回收看门狗协程是为了规避协程泄漏问题
需要锁续期的情况
长时间任务 如果获取分布式锁的业务逻辑较为复杂或耗时那么可能需要设置锁续期以防止持有锁的客户端在执行业务逻辑时由于各种原因无法及时释放锁。业务处理时间不确定 如果业务处理时间不确定无法预测锁会持有多长时间那么设置锁续期可以确保在业务逻辑执行期间锁不会过早地被释放。
不需要锁续期的情况
短时间任务 如果获取分布式锁的业务逻辑非常简单且耗时很短可以在执行完业务逻辑后立即释放锁不需要设置锁续期。业务逻辑可控 如果业务逻辑可以控制在一个较短的时间内完成且不会出现无法释放锁的情况也可能不需要设置锁续期。
可重入
原因加锁命令使用了 SETNX 一旦键存在就无法再设置成功这就导致后续同一线程内继续加 锁将会加锁失败。当一个线程执行一段代码成功获取锁之后继续执行时又遇到加锁的子任务代码需要的这把锁就是我们现在拥有的这把锁锁明明是被我们拥有却还需要等待自己释放锁然后再去抢锁这看起来就很奇怪我释放我自己。
解决当线程拥有锁之后往后再遇到加锁方法直接将加锁次数加 1然后再执行方法逻辑。退出加锁方法之后加锁次数再减 1当加锁次数为 0 时锁才被真正的释放。可以使用redis中的Hash数据类型完成。
利用 lua 脚本判断逻辑
加锁
if (redis.call(exists, KEYS[1]) 0 or redis.call(hexists, KEYS[1], ARGV[1]) 1)
thenredis.call(hincrby, KEYS[1], ARGV[1], 1);redis.call(expire, KEYS[1], ARGV[2]);return 1;
elsereturn 0;
end 假设值为KEYS:[lock], ARGV[uuid, expire]如果锁不存在或者这是自己的锁就通过hincrby不存在就新增并加1存在就加1获取锁或者锁次数加1。 解锁
if(redis.call(hexists, KEYS[1], ARGV[1]) 0) then return nil;
elseif(redis.call(hincrby, KEYS[1], ARGV[1], -1) 0) then return 0;
else redis.call(del, KEYS[1]); return 1;
end; 判断 hash set 可重入 key 的值是否等于 0 如果为 nil 代表 自己的锁已不存在在尝试解其他线程的锁解锁失败如果为 0 代表 可重入次数被减 1如果为 1 代表 该可重入 key 解锁成功 主从一致
原因当线程1加锁成功后master节点数据会异步复制到slave节点此时当前持有Redis锁的master节点宕机slave节点被提升为新的master节点假如现在来了一个线程2再次加锁会在新的master节点上加锁成功这个时候就会出现两个节点同时持有一把锁的问题。
解决 可以利用redisson提供的红锁来解决这个问题它的主要作用是不能只在一个redis实例上创建锁应该是在多个redis实例上创建锁并且要求在大多数redis节点上都成功创建锁红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。 如果使用了红锁因为需要同时在多个节点上都添加锁性能就变的很低了并且运维维护成本也非常高所以我们一般在项目中也不会直接使用红锁并且官方也暂时废弃了这个红锁 总结
独占排他setnx 防死锁 redis客户端程序获取到锁之后立马宕机。给锁添加过期时间 不可重入可重入
防误删先判断是否自己的锁才能删除
原子性 加锁和过期时间之间 判断和释放锁之间
可重入性hash lua脚本 自动续期Timer定时器 lua脚本