Redis实现分布式锁总结

分布式锁特性

分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资 源。

  • 互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
  • 高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布 式锁的服务以集群的方式部署;
  • 防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕 机或者网络不可达时产生死锁;
  • 独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了。

redis实现分布式锁:

优点:Redis 锁实现简单,理解逻辑简单,性能好,可以支撑高并发的获取、释放锁操作。

缺点:

  • Redis 容易单点故障,集群部署,并不是强一致性的,锁的不够健壮;
  • key 的过期时间设置多少不明确,只能根据实际情况调整;
  • 需要自己不断去尝试获取锁,比较消耗性能。

mysql实现分布式锁:

缺点:

  • 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
  • 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
  • 这把锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程 并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
  • 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。可解决。

zookeeper实现分布式锁:

优点:zookeeper 天生设计定位就是分布式协调,强一致性,锁很健壮。如果获取不到锁,只需要添加一 个监听器就可以了,不用一直轮询,性能消耗较小。

缺点:在高请求高并发下,系统疯狂的加锁释放锁,最后 zk 承受不住这么大的压力可能会存在宕机的风 险。

redis实现分布式锁

setnx
  • 加锁:使用setnx来加锁。key是锁的唯一标识,按业务来决定命名

    setx key testvalue这里设置为test。

    当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;当一个线程执行setnx返回 0,说明key已经存在,该线程抢锁失败;

  • 解锁:当得到的锁的线程执行完任务,需要释放锁,以便其他线程可以进入:del key

  • 锁超时:setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一段时间后自动 释放:expire key 30

存在的问题:

  • SETNX 和 EXPIRE 非原子性

    • 某一个线程刚执行 setnx,成功得到了锁。此时 setnx 刚执行成功,还未来得及执 行 expire 命令,节点就挂掉了。此时这把锁就没有设置过期时间,别的线程就再也无法获得该锁。

    • 解决办法:可以使用 set 指令,后面加入参数 EX 和 NX 就可以实现加锁的同时,设置过期时间!(即把 EX 和 NX 变成了一个原子操作)

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

      SET lockKey requestId NX PX 30000

      • EX second: 设置过期时间,设置键的过期时间为 second 秒;
      • PX millisecond:设置过期时间,设置键的过期时间为 millisecond 毫秒;
      • NX:互斥,只在键不存在时,才对键进行设置操作;
      • XX:非互斥,只在键已经存在时,才对键进行设置操作;
  • 锁误删:

    • 如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期 自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的是线程 B 加的锁。
    • 解决方法:删除锁时获取锁标识并判断是否一致。在 del 释放锁之前加一个判断,验证当前的锁是不是自己加的锁。 具体在加锁的时候把当前线程的 id 当做 value,可生成一个 UUID 标识当前线程,在删除之前验证 key 对应的value 是不是自己线程的 id。还可以使用 lua 脚本做验证标识和解锁操作以保证原子性。 (不能使用默认的线程 id,因为在分布式情况下,可能多个线程访问不同的服务器,使得具有相同 的线程 id)
  • 释放锁的问题:非原子操作

    包含了【获取该锁的值】,【判断是否是自己加的锁】,【删除锁】这三个操作,万一这三个操作中间的某个时刻出现阻塞(业务处理、GC、操作系统等原因),就会导致问题。

    正确的释放锁姿势——锁的判断和删除都在服务端(Redis),使用 lua 脚本保证原子性。

    释放锁的操作为什么要使用 lua 脚本?

    释放锁其实包含三步操作:‘GET’、判断和‘DEL’,用 Lua 脚本来实现能保证这三步的原子性。

基于setnx实现的分布式锁问题:

  • 不可重入:同一个线程无法多次获取同一把锁
  • 不可重试:获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放。
  • 主从一致性:如果Redis提供主从集群,主从同步存在延迟,当主机宕机时,如果从库未同步主库的数据,则会出现锁失效问题。
Redisson
  • 解决不可重入问题:(类似于 ReentrantLock,用 hash 结构存储线程标识以及重入次数,使用 lua 脚本确保原子性)

  • 解决不可重试问题:指定参数,该时间内重试获取。

  • 解决锁超时问题:如果客户端 1 请求锁成功了,但是由于业务处理、GC、操作系统等原因导致它处理时间过长,超过了锁的时间,这时候 Redis 会自动释放锁,这种情况可能导致问题。通过续期解决

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eztQj4tj-1683457938703)(images/image-20230222201122230.png)]

    watchdog(看门狗):加锁时没有指定加锁时间时会启用 watchdog 机制,默认加锁 30 秒,每 10 秒钟检查一次,如果存在就重新设置 过期时间为 30 秒(即 30 秒之后它就不再续期了)。

    • 默认值:30000,可以通过修改 Config.lockWatchdogTimeout 来另行指定。
    • 如果负责储存这个分布式锁的 Redisson 节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现死锁的情况。为了避免这种情况的发生,Redisson 内部提供了一个监控锁的看门狗(WatchDog),它 的作用是在 Redisson 实例被关闭前,不断的延长锁的有效期。适用于分布式锁的加锁请求中未明确使用 leaseTimeout() 参数的情况。如果该看门狗未使用 lockWatchdogTimeout 去重新调整一个分布式锁的 lockWatchdogTimeout 超时,那么这个锁将变为失效状态。
RedLock

【为什么有RedLock?】

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

【Redlock算法的基本思路】

让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和 半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁 失败。这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。

加锁:

  • 客户端获取当前时间
  • 客户端按顺序依次向 N 个 Redis 实例执行加锁操作。
    • 使用 SET 命令,带上 NX,EX/PX 选项
    • 包含客户端的唯一标识(防止锁误删除),
    • 为了保证在某个 Redis 节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),客户端在向某个 Redis 节点获取锁失败以后,应该立即尝试下一个 Redis 节点。
  • 如果客户端从大多数 Redis 节点(大于等于 N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功。
  • 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算。等于最初有效时间减去获得锁消耗的时间。
  • 如果最终获取锁失败了(可能由于获取到锁的 Redis 节点个数少于 N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有 Redis 节点发起释放锁的操作(使用 Redis Lua 脚本)。

解锁:客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。

  • 为什么要在多个实例上加锁?

    本质上为了**容错,**部分实例异常宕机,剩余实例只要超过 N/2+1 依旧可用。

  • 为什么加锁成功之后,还要计算加锁的累计耗时?

    考虑网络延迟、丢包、超时等情况发生,如果加锁的累计耗时已经超过了锁的过期时间,那么此时的锁已经没有意义了。

  • 为什么释放锁,要操作所有节点,对所有节点都释放锁?

    因为当对某一个 Redis 节点加锁时,可能因为网络原因导致加锁“失败”。注意这个“失败”,指的是 Redis 节点实际已经加锁成功了,但是返回的结果因为网络延迟并没有传到加锁的线程,被加锁线程丢弃了,加锁线程误以为没有成功,于是加锁线程去尝试下一个节点了。所以释放锁的时候,不管以前有没有加锁成功,都要释放所有节点的锁,以保证清除节点上述发生的情况导致残留的锁。

崩溃恢复(AOF 持久化)对 Redlock 算法影响:

  • 问题:假设 Rodlock 算法中的 Redis 发生了崩溃-恢复,那么锁的安全性将无法保证。假设加锁线程在 5 个实例中对其中 3 个加锁成功,获得了这把分布式锁,这个时候 3 个实例中有一个实例被重启了。重启后的实例将丢失其中的锁信息,这个时候另一个加锁线程可以对这个实例加锁成功,此时两个线程同时持有分布式锁。锁的安全性被破坏。

  • 解决:持久化配置中设置fsync=always,但性能大大降低

Redlock 算法存在的问题:

  • 节点重启:N 个 Redis 节点中如果有节点发生崩溃重启,会对锁的安全性有影响的。具体的影响程度跟 Redis 对数据的持久化程度有关。
    • 解决:延迟重启:一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。
  • 时钟变迁::Redis 的过期时间是依赖系统时钟的,如果时钟漂移过大时会影响到过期时间的计算。

字节技术-聊聊分布式锁
黑马程序员Redis入门到实战教程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Guanam_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值