redis实现分布式锁导致的问题(第二版)

属性:公平,互斥,可重入(业务涉及不多)

可使用redis,zookeeper,etcd实现

 

redis实现:

一般追求高性能使用redis

redis采用单线程架构,可以保证单个命令的原子性,但是无法保证一组命令在高并发场景下的原子性(引入lua脚本)

 

注意点:

  1. 独占排他:setnx

  2. 防死锁:

    redis客户端程序获取到锁之后,立马宕机。给锁添加过期时间

    不可重入:hash实现 :  lua脚本进行判断,如果锁不存在或者这是自己的锁,就通过hincrby(不存在就新增并加1,存在就加1)获取锁或者锁次数加1。

  3. 防误删:

    先判断是否自己的锁才能删除,加锁时赋值为自己的唯一ID,释放时进行判断

  4. 需保证原子性的阶段:

    (1)加锁和过期时间之间

    (2)判断和释放锁之间

  5. 可重入性:hash(key field value) + lua脚本

  6. 自动续期:Timer定时器 + lua脚本

  7. 在集群情况下,导致锁机制失效:

    1. 客户端程序10010,从主中获取锁

    2. 从还没来得及同步数据,主挂了

    3. 于是从升级为主

    4. 客户端程序10086就从新主中获取到锁,导致锁机制失效

redlock算法引入:

解决方法:本质上是为了「容错」,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用

特点:同步算法,Redlock 只有建立在「时钟正确」的前提下,才能正常工作

前提:

多实例master

客户端需提前几毫秒完成工作,作为对时钟漂移的补偿:只有当持有锁的客户端在锁有效时间达到之前完成工作,互斥性才会得到保证。提前的几毫秒用于补偿进程之间的时钟漂移。 这就跟木桶原理一样,较短的一块板子决定了最大蓄水量,最早的过期时间决定了锁的实际最大生存时长。

重试获取锁设计思想:

  1. 先延迟一个随机时间。
  2. 再使用指数退避法执行重试(redsync没有实现)。

问题:

  1. 客户端A从master获取到锁

  2. 在master将锁同步到slave之前,master宕掉了。

  3. slave节点被晋级为master节点

  4. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。


       根本原因:redis主从复制的异步性

redisSync源码解读

关于redlock的问题:

1. 时钟问题:

Redlock 必须「强依赖」多个节点的时钟是保持同步的,一旦有节点时钟发生错误,那这个算法模型就失效了。

结论:Redis 作者表示,Redlock 并不需要完全一致的时钟,只需要大体一致就可以了,允许有「误差」。

例如要计时 5s,但实际可能记了 4.5s,之后又记了 5.5s,有一定误差,但只要不超过「误差范围」锁失效时间即可,这种对于时钟的精度的要求并不是很高,而且这也符合现实环境。

 

2. 分布式场景问题:

  1. 客户端 1 请求锁定节点 A、B、C、D、E
  2. 客户端 1 的拿到锁后,进入进程暂停(时间比较久)
  3. 所有 Redis 节点上的锁都过期了
  4. 客户端 2 获取到了 A、B、C、D、E 上的锁
  5. 客户端 1 GC 结束,认为成功获取锁
  6. 客户端 2 也认为获取到了锁,发生「冲突」

 

导致原因:分布式系统会遇到的三座大山:NPC

  • N:Network Delay,网络延迟
  • P:Process Pause,进程暂停
  • C:Clock Drift,时钟漂移

redis作者结论:

npc在redlock算法的1-3步骤中发生可以解决,在第3步之后如果发送npc的话(也就是客户端确认拿到了锁,去操作共享资源的途中发生了问题,导致锁失效)解决不了,且其他分布式锁实现方式也解决不了

依据:

redlock流程:

  1. 客户端先获取「当前时间戳 T1」
  2. 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
  3. 如果客户端从 3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳 T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
  4. 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
  5. 加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)

 

 

 

 

zookeeper实现:

1.ZooKeeper的每一个节点,都是一个天然的顺序发号器

2.ZooKeeper节点的递增有序性,可以确保锁的公平

3.ZooKeeper的节点监听机制,可以保障占有锁的传递有序而且高效:释放时删除节点:击鼓传花;具体的方法是,每一个等通知的Znode节点,只需要监听(linsten)或者监视(watch)排号在自己前面那个,而且紧挨在自己前面的那个节点,就能收到其删除事件了。 只要上一个节点被删除了,就进行再一次判断,看看自己是不是序号最小的那个节点,如果是,自己就获得锁

4.ZooKeeper的节点监听机制,能避免惊群效应:所有节点都去监听,然后做出反应,这样会给服务器带来巨大压力

https://juejin.cn/post/7103157951342313479

ZooKeeper 实现分布式锁通常会用到两种类型的节点(znode):持久节点(Persistent Znode)和临时顺序节点(Ephemeral Sequential Znode)。以下是如何使用这两种类型的节点来实现分布式锁:

1. 持久节点:

  • 锁节点: 通常,会有一个持久节点作为锁的根节点。所有试图获取锁的客户端都会在这个节点下创建子节点。

2. 临时顺序节点:

  • 代表客户端的节点: 当一个客户端试图获取锁时,它会在锁的根节点下创建一个临时顺序节点。临时节点确保如果客户端会话结束(例如,客户端崩溃),节点会被自动删除,这有助于避免死锁。顺序节点则确保节点的创建顺序被记住,保证了锁的公平性。

实现过程:

  1. 锁的获取: 客户端试图在锁的根节点下创建临时顺序节点。然后,客户端查看自己创建的节点是否是当前根节点下序号最小的节点。如果是,则客户端成功获取锁。

  2. 等待锁: 如果客户端创建的节点不是序号最小的节点,客户端则监听比自己序号小的最近的一个节点的删除事件。

  3. 锁的释放: 当持有锁的客户端完成其临界区代码后,它会删除代表自己的临时节点,从而释放锁。这将触发等待该锁的下一个客户端的监听事件,使其成为新的锁持有者。

 

 

etcd实现

 

 

对比:

分布式协议:

redis集群,zookeepr,etcd所遵循的分布式协议:Gossip,zab,raft

Gossip和zab都是保证分布式环境下最终一致性协议,raft为强一致性

通讯协议:

redis为RESP协议为一个简单的文本协议。由于其简单性,解析和处理速度很快。

 

ZooKeeper 使用自定义的二进制协议

  • 会话和心跳机制: ZooKeeper 的协议包含会话和心跳机制,这增加了一些开销,但有助于维护系统的稳定性和一致性。

  • 支持观察者机制: ZooKeeper 协议支持观察者机制,可以推送节点变化的通知,但这也增加了协议的复杂性。

etcd:grpc,基于 HTTP/2 和 protobuf序列化

通讯协议对比:

特性/协议
Redis (RESP)
ZooKeeper (自定义协议)
etcd (gRPC/HTTP2 + Protocol Buffers)
基础协议 TCP TCP HTTP/2 (基于 TCP)
数据格式 文本(简单字符串、错误、整数、批量字符串、数组) 二进制 Protocol Buffers (二进制)
性能 高(由于协议简单) 中(协议相对复杂) 高(由于 HTTP/2 和 Protocol Buffers)
多路复用 不支持 不支持 支持
流量控制 不支持 不支持 支持
双向通信 不支持(请求-响应模型) 支持(通过 watch 机制) 支持
批量处理 支持(管道技术) 不明确支持 不明确支持
使用场景 键值存储 分布式协调服务 分布式键值存储

 

实现分布式锁对比:

 

特性
Redis (通常使用 RedLock 算法)
ZooKeeper
etcd
性能
可靠性 高(由于 ZAB 协议) 高(由于 Raft 协议)
易用性 高(有多种客户端库支持) 中(API 相对底层) 中(API 相对底层)
超时和过期 支持 支持 支持
公平性 不保证 保证(通过顺序节点) 不保证

 

 

结论:

 

如果 进程暂停、网络延迟是发生在客户端拿到锁之后,那不止 Redlock 有问题,其它锁服务都有类似的问题。

这里我们可以得出一个结论:

一个分布式锁,在极端情况下,不一定是安全的。

 

对于redlock,不建议使用

原因:

时钟问题:人为,硬件等影响因素,很难保证时钟同步

性能不如单机版 Redis,部署成本也高

优先考虑使用 Redis「主从+哨兵」的模式,实现分布式锁。

那如何正确使用分布式锁

从上面我们也了解到,任何分布式锁都无法完全保证正确性,因此分布式锁时建议

  1. 在上层使用分布式锁完成「互斥」目的,虽然极端情况下锁会失效,但它可以最大程度把并发请求阻挡在最上层,减轻操作资源层的压力。
  2. 但对于要求数据绝对正确的业务,在资源层一定要做好「兜底」,发生极端情况时,也不会对系统造成影响
posted @ 2022-09-12 21:11  山野村夫01  阅读(512)  评论(0编辑  收藏  举报