Redis分布式锁

149 阅读6分钟

日常开发中可以用 synchronized 、Lock 实现并发编程。但是Java中的锁只能保证在同一个JVM进程内中执行。如果在分布式集群环境下用锁呢?

一、Zookeeper实现分布式锁

ZK中的四种节点类型,实现分布式锁使用临时几点:

  1. 持久节点:客户端断开连接zk不删除persistent类型节点;
  2. 临时节点:客户端断开连接zk删除ephemeral类型节点 ;
  3. 顺序节点:节点后面会自动生成类似0000001的数字表示顺序 ;
  4. 节点变化的通知:客户端注册了监听节点变化的时候,会调用回调方法; 大致流程如下,其中注意每个节点只监控它前面那个节点状态,从而避免羊群效应。 image-20240131161550328.png 缺点: 频繁的创建删除节点,加上注册watch事件,对于zookeeper集群的压力比较大,性能也比不上Redis实现的分布式锁。 优点: 相比Redis实现的分布式锁不会出现访问瞬断问题。

二、Redis实现分布式锁

2.1 SETNX实现分布式锁

SETNX(Set If Not Exists),使用:SETNX key value即可实现一个简单的分布式锁,原理是因为如果 key 不存在则set成功返回true,如果key已经存在不进行操作返回0。

问题: 造成死锁。

为什么会造成死锁现象? 在没有设置key的失效时间的情况下,当一个进程抢占一个锁后,其他进程进入阻塞状态,这时如果Redis挂掉了,会导致其他进程一直阻塞下去,也就是造成了死锁,因为一致没有释放掉原来获取到的锁。

改进: 给锁设置超时时间。

2.2 SETEX对SETNX的改进

SETEX key seconds value:将值 value 关联到 key ,并设置 key 的存活时间。如果 key 已经存在,setex命令将覆写旧值。SETEX 是一个原子性操作,可以在实现分布式锁时为锁设定一个合理的锁持有时间,以防止因客户端异常退出而无法释放锁导致死锁的情况发生。

问题

  1. 当锁过期时,线程还在处理任务中;
  2. 当处理完任务后释放了其他线程的锁。

为什么会释放其他线程的锁? 假如有两个线程A和B,A抢到了锁,并且锁的时间是10s,A的任务执行超过了10s,导致在释放锁之前锁到期自动释放。此时线程B获取到了锁,然后线程A继续执行,进行锁的释放,此时导致线程B的锁进行了释放。

改进

  1. 锁续命(Watch Dog)——延长锁持有的时间,并添加子线程每10秒确认线程是否在线,在线则将过期时间重设;
  2. 给锁加一个唯一ID(例如UUID)。

2.3 锁续命并进行锁的唯一判定

问题: 并不能完全解决释放其他线程的锁的问题。

为什么不能完全解决? 因为进行锁的唯一判定和释放锁不是原子性的,在极端情况下,刚判断完ID,还未来得及释放锁,设置的锁的失效时间到期了,此时其他线程抢到了锁,这样还是会造成对其他线程的锁的释放。

改进: 使用Redission,Redission使用lua脚本实现了判断锁的唯一ID和锁释放两步操作的原子性。

注:使用SETNX和SETEX方法的分布式锁不能重入。

三、Redission实现分布式锁

Redisson 是在Redis基础上的一个服务,采用了基于NIO的Netty框架,内部通过使用Lua脚本实现了加锁和解锁的原子性。

Redission的基本原理

image-20240131161655653.png

Redisson加锁解锁 大致流程图如下:

image-20240131161713459.png 问题: 存在线程安全问题——多个线程同时获取到了锁。

为什么会存在这种情况? 存在两个线程A和B,如果A在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。此时,master节点发生故障,slave节点就会升级为master节点。然后,因为新升级为master的节点并没有锁的记录,B就可以获取到同个key的锁了,此时线程A和B拿到了同一个锁,锁的安全性就没了。

改进: 使用Redlock

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

image-20240201101645153.png

RedLock存在的问题

  1. 性能问题:RedLock 要等待大多数节点返回之后,才能加锁成功,而这个过程中可能会因为网络问题,或节点超时的问题,影响加锁的性能。
  2. 并发安全性问题:当客户端加锁时,如果遇到 GC 可能会导致加锁失效,但 GC 后误认为加锁成功的安全事故,例如以下流程:
    1. 客户端 A 请求 3 个节点进行加锁。
    2. 在节点回复处理之前,客户端 A 进入 GC 阶段(存在 STW,全局停顿)。
    3. 之后因为加锁时间的原因,锁已经失效了。
    4. 客户端 B 请求加锁(和客户端 A 是同一把锁),加锁成功。
    5. 客户端 AGC 完成,继续处理前面节点的消息,误以为加锁成功。
    6. 此时客户端 B 和客户端 A 同时加锁成功,出现并发安全性问题。

因为 RedLock 存在的问题争议较大,且没有完美的解决方案,所以 Redisson 中已经废弃了 RedLock,这一点在 Redisson 官方文档中能找到,如下图所示:

image-20240131161836894.png

四、分布式锁的优化

问题:同时有大量请求访问同一个商品。 解决:使用Redission分布式锁;

改进1:分段锁 描述:将热点数据分多个key存储,比如1000个热点数据,分10个key,每个key存100个数据,当有10个线程进入时,根据访问的数据找到对应的key进行访问。分段所的实现可以借鉴CurrentHashMap 1.7版本。

改进2:读写锁 描述:针对读多写少的场景,可以加读写锁进行控制。

改进3:使用Redission的tryLock 描述:如果存在热点缓存重建问题,假如10万个请求进行查询,缓存中查不到,加锁进行缓存重建,为了不让其他线程阻塞,可以让其他线程超时失效,再查询时,缓存已经重建。如果执行时间超过了tyrLock的设置时间会有问题。