Redis分布式锁

参考:https://www.cnblogs.com/wangyingshuo/p/14510524.html

https://www.bilibili.com/video/BV13R4y1v7sP?p=131&vd_source=152ad2dc192867dca92d66a24472c851

 

介绍

多线程环境下控制对共享资源的访问以保证数据一致性。

特点:跨进程、跨服务、跨服务器   

互斥性 ——任意时刻,只有一个客户持有锁

超时释放——持有锁超时,可以释放,防止死锁

可重入——一个线程获取锁后可以再次请求锁,加几次锁就需要释放几次锁

自动续期——守护线程判断是否需要续期

高可用,高性能——加锁和解锁需要开销尽可能低,同时也要保证高可用

安全性——锁只能被持有的线程删除,不能被其他线程删除

 

 

jvm本地锁有三种情况导致锁失效

多例模式、

事务 读未提交

集群部署

 

 

单机版:lua脚本

按照juc的lock接口规范实现:

  互斥、超时释放(过期)、可重入等特性

lock()

  加锁  在redis中存储k-v

    设置过期时间expire 防止死锁

    使用hash  key field value  实现重入  field作为线程唯一标识(防止删除非自己的锁)  value表示加锁次数(重入

  自旋  未获取到锁尝试自旋 while    需要有停顿时间 不能无间隔自旋

  续期  间隔一定时间进行自动续期  定时器  防止业务未完成就把锁删除了

     原子操作   lua脚本

 

unlock()

  考虑可重入行的递减,加锁几次就解锁几次

  到0了,就del删除

 

setnxex  不存在才会设值并设置过期时间     nx 排他独占    过期时间 防止死锁 (A加了锁,还没解锁就挂掉了)

超时问题:

  • 业务超时锁过期自动删除      过期时间稍微设长一点  更好点需要自动续期   
  • 超时后其他线程进行了加锁,此时业务执行完进行锁删除 ,需要判断是否是自己的锁,以免误删别人的锁     删除锁需要加判断是否是自己的  UUID

判断锁和删除锁 需要是原子操作,因为可能还没来得及删除,redis中锁过期,被其他线程占用了锁,此时删除的依然是其他线程的锁

  解决:redis官方提供了lua脚本来实现判断并删除锁的原子操作

 

=>可重入  hash + lua脚本

使用hash结构  

设置锁:hsetnx lock uuid+线程id value

判断锁是否存在:hexists  lock  uuid+线程id

重入计数加一:hincrby lock  uuid+线程id

 

hset key field value:hset lock uuid+线程id count (count为重入次数)

使用uuid+线程id标识自己的锁,每个服务生成一个uuid 唯一  , 保证重入时锁是同一个锁(重入时流水号不变)

问题:重入时线程id虽然没变,但是UUID变了

 流水号变了的解决方式:

1.使用ThreadLocal,每个线程含有自己的、 

2.将流水号由局部变量变为成员变量并在构造方法中初始化赋值

 

 

 

加锁:

1.判断锁是否被独占(exists),如果没有,直接获取锁(hset/hincrby)并设置过期时间(expire)

2.如果锁被占用,则判断是否是当前线程占用的,如果是则重入(hincrby)并设置过期时间(expire)

3.否则获取锁失败,重试

 

 

 

 

 

 

 

=》自动续期(定时任务

子线程处理自动续期 

Timer定时器+lua脚本

判断锁是否自己的  hexists == 1  , 执行expire重置

 

 

 

 

解锁:

直接del会有误删问题

先判断再删除并保证原子操作:lua脚本

hash+lua脚本;可重入:

  1.判断当前线程的锁是否存在,不存在返回nil,将来抛出异常

  2.存在则直接减1(hincrby -1),判断减1后的值是否为0,为0则释放锁(del),并返回1

  3.不为0,则返回0

重试:递归、循环

 

 

 锁失效

单台redis 故障,锁机制直接不可用

集群下,单点故障同样导致锁机制不可用

  • 线程a进行加锁,redis主节点宕机,还未将状态同步给从节点
  • 从节点上位变成新主,线程b进行加锁成功
  • 锁失效

 

 

分布式下算法:Redlock

redis一般集群部署 =>锁失效场景

如果线程1在master节点拿到锁,但是加锁的key还没有同步到slave节点,恰好这时master发生了故障,一个slave节点就会升级为master节点。

线程2可以获取同一个key的锁,线程1也以为拿到了锁,这样锁就失效了

 

redis提供分布式锁算法:RedLock

很少使用

 

核心思想:

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

 

RedLock加锁:

1.客户端以毫秒为单位获取当前时间的时间戳,作为起始时间

2. 客户端尝试在所有N个实例中顺序使用相同的键名、相同的随机值来获取锁定。每个实例尝试获取锁都需要时间,客户端应该设置一个远小于总锁定时间的超
时时间。例如,如果自动释放时间为10秒,则尝试获取锁的超时时间可能在5到50毫秒之间。这样可以防止客户端长时间与处于故障状态的Redis节点进行通信
如果某个实例不可用,尽快尝试与下一个实例进行通信。

3. 客户端获取当前时间 减去在步骤1中获得的起始时间,来计算获取锁所花费的时间。当且仅当客户端能够在大多数实例(至少3个)中获取锁时,并且获取锁所花费的总时间小于锁有效时间,则认为已获取锁。

4. 如果获取了锁,则将锁有效时间减去 获取锁所花费的时间,如步骤3中所计算。

5. 如果客户端由于某种原因(无法锁定N / 2 + 1个实例或有效时间为负)而未能获得该锁,它将尝试解锁所有实例(即使没有锁定成功的实例)

 

现有redis分布式实现:redisson

加锁:

底层一样是lua脚本,原子的加锁、重入、判断是否是自己的锁,hash结构

pttl:毫秒级的过期时间

 

自动续期: 加锁成功会调用 

 

 底层通过定时器完成重置过期时间

 

 

 

 

 解锁:

 

 

 

 判断是不是自己的锁,不是返回nil

临时变量count判断是否已减到0了,也就是锁解完了

如果锁没有解完,过期时间重置,返回0

如果解完了,删除锁,发布订阅

返回1,告知解锁成功

其他情况返回nil

 

 

解锁完,取消过期时间重置

 

提供看门狗监控锁

如果负责存储分布式锁的redisson节点宕机(服务端宕机),而(redis)中这个锁正处于锁住的状态,没法被解锁=》死锁

redisson提供一个监控锁的看门狗,它的作用是在redisson实例被关闭前,不断延长锁的有效期

看门狗监控器——定时器,提供了自动续期   其实跟死锁没啥关系

 

 

 

 

对于锁的自动续期问题提供更好的解决方案

定时守护线程,每个一段时间检查锁是否还存在,存在则对锁的过期时间进行延长,防止锁提前释放

主要是提供看门狗解决自动续期问题

属于单机版方案

 

 

 

线程加锁成功=》启动一个看门狗(守护线程),每隔10s检查一下

  =》如果线程还持有锁,则延长 =》实现自动续期

 

posted on 2023-03-23 00:09  or追梦者  阅读(52)  评论(0编辑  收藏  举报