分布式环境的分布式锁 - Redlock方案和fencing token方案

Redlock方案

前提:

  • 不需要再部署从库和哨兵实例,只部署主库
  • 主库要部署多个,官方推荐至少5个实例

整体流程:

  1. 客户端先获取 当前时间戳time1
  2. 客户端依次向这5个Redis实例发起加锁请求,而且每个请求会设置超时时间,注意这里的超时时间要远远的小于锁的有效时间,如果某一个实例加锁失败(包括网络超时、锁被其他人持有等各种异常情况),向下一个Redis实例申请加锁
  3. 如果客户端从 大于等于3个以上Redis实例加锁成功,则再次获取 当前时间戳time2 ,如果time2−time1<ddltime2-time1 < ddltime2time1<ddl,其中ddl为锁的过期时间,认为客户端加锁成功,因为锁还未过期;否则,加锁失败
  4. 加锁成功,操作共享资源
  5. 加锁失败,向全部节点发起释放锁的请求

重点在于:

  • 客户端必须在多个独立的Redis实例上加锁

    本质是为了容错,因为如果部分实例宕机的话,剩余的实例加锁成功了,整个锁服务依旧可用

  • 必须保证大多数的节点都能加锁成功

    多个Redis实例构成了一个分布式系统,如果只存在故障节点,只要大多数节点正常,整个系统依旧是可以提供正确服务

  • 大多数节点加锁的总耗时,小于锁设置的过期时间

    操作的是多个节点,耗时会比单个实例的耗时多,而且网络层面也有延迟、丢包、超时等情况发生。所以如果大多数的节点加锁情况,但是加锁的累计耗时超过了锁的过期时间,有些实例的锁可能已经失效了,这个锁也没有意义

  • 释放锁,要向全部节点发起释放请求

    清理节点遗留的锁

对Relock的质疑

分布式专家Martin对Relock提出了质疑

分布式锁的目的是什么?

有两个目的:
一是效率,使用分布式锁的互斥功能,避免不必要地做同样地两次工作,例如一些昂贵地计算任务,而锁失效,并不会带来恶性的后果,例如发了2次邮件,问题不大
二是正确性,锁用来防止并发进程互相干扰,如果锁失效,会造成多个进程同时操作同一条数据,产生的后果是数据严重错误、永久性不一致、数据丢失等恶性问题。

如果是为了效率,使用单机版Redis就可以了,即使偶尔宕机/主从切换导致锁失效,都不会产生严重的后果。而Redlock依旧存在锁失效的问题,正确性也无法保证

锁在分布式系统中遇到的问题

分布式系统的异常场景

  • N:Network Delay 网络延迟
  • P:process Pause 进程暂停 GC
  • C:Clock Drift 时钟偏移

下面用一个进程暂停的例子来看Redlock的安全性问题

  1. 客户端1请求锁定节点A~E
  2. 客户端1拿到所以后,进入GC,时间维持久
  3. 所有Redis节点上的锁都过期了
  4. 客户端2获取到了A~E上的锁
  5. 客户端1的GC结束了,也认为自己成功获取到锁了
  6. 客户端2也同样认为 发生了冲突

在这里插入图片描述

假设时钟正确并不合理

当多个Redis节点时钟发生问题时,也会导致Redlock锁失效

  1. 客户端1获取节点A~C上的锁,但是因为网络问题,无法访问D和E
  2. 节点C上的时钟向前跳跃,导致锁到期
  3. 客户端2获取节点C~E上的锁,因为网络问题,无法访问A和B
  4. 客户端1和客户端2都觉得自己加锁成功

机器的时钟发生错误,主要有以下情况

  • 系统管理员手动修改了机器时钟
  • 机器时钟在同步NTP时间时,发生了大的跳跃

fencing token方案

Martin 提出一种被叫作 fencing token 的方案,保证分布式锁的正确性。
流程如下:

  1. 客户端在获取锁时,锁服务可以提供一个递增的 token
  2. 客户端拿着这个 token 去操作共享资源
  3. 共享资源可以根据 token 拒绝后来者的请求
    在这里插入图片描述

这样是建立在异步模型上的,无论哪种情况发生,都可以保证安全性。
他还表示,一个好的分布式锁,无论 NPC 怎么发生,可以不在规定时间内给出结果,但并不会给出一个错误的结果。也就是只会影响到锁的性能,而不会影响它的正确性

Redis作者的反驳

时钟问题的解释

不需要完全一致,允许有误差,只要是不超过误差范围锁失效时间就可以
对于对方提到的时钟修改问题,反驳:

  • 手动修改时钟:停止这种做法
  • 时钟跳跃:通过恰当的运维,保证机器时钟不会大幅度跳跃,每次通过微小的调整来完成

网络延迟和GC问题

在Redlock流程里,如果步骤1~3发生了网络延迟、进程GC等耗时很长的异常情况,那么步骤3里是可以检测出来的,如果time2-time1超过了锁设置的超过时间,会认为加锁失败并释放所有锁。
如果对方认为,发生网络延迟、进程GC是在步骤3以后,也就是 客户端拿到了锁,但是在操作共享资源的过程里出现了问题导致锁失效,那任何的锁服务如zk,都有类似的问题

总结一下就是:

  • 客户端在拿到锁之前,无论经历什么耗时长问题,Redlock 都能够在步骤3检测出来
  • 客户端在拿到锁之后,发生 NPC,那 Redlock、Zookeeper 都无能为力

质疑fecing token机制

第一,这个方案必须要求要操作的共享资源服务器有拒绝旧token的能力
比如,操作MySQL,从锁服务里拿到一个递增数字的token,然后客户端要带着这个token去改MySQL的某一行,就需要利用MySQL的事务隔离性做

UPDATE table T SET val = $new_val, current_token = $token WHERE id = $id AND current_token < $token

但是如果操作共享资源是向磁盘上写文件,或是发HTTP请求呢?这种方案并不可行,大部分要操作的资源服务器,都没有互斥能力
第二,Redlock已经提供了随机值,利用随机值,也可以达到和fencing token相同的效果,类似CAS

  1. 客户端使用 Redlock 拿到锁
  2. 客户端在操作共享资源之前,先把这个锁的 VALUE,在要操作的共享资源上做标记
  3. 客户端处理业务逻辑,最后,在修改共享资源时,判断这个标记是否与之前一样,一样才修改

还是以 MySQL 为例:

  1. 客户端使用 Redlock 拿到锁
  2. 客户端要修改 MySQL 表中的某一行数据之前,先把锁的 VALUE 更新到这一行的某个字段中(这里假设为 current_token 字段)
  3. 客户端处理业务逻辑客户端修改 MySQL 的这一行数据,把 VALUE 当做 WHERE 条件,再修改
UPDATE table T SET val = $new_val WHERE id = $id AND current_token = $redlock_value

两个客户端通过这种方案,先标记再检查+修改共享资源,那这两个客户端的操作顺序无法保证,而用 Martin 提到的 fencing token,因为这个 token 是单调递增的数字,资源服务器可以拒绝小的 token 请求,保证了操作的顺序性
Redis 作者对于这个问题做了不同的解释:分布式锁的本质,是为了互斥,只要能保证两个客户端在并发时,一个成功,一个失败就好了,不需要关心顺序性

基于Zookeeper的锁

  1. 客户端 1 和 2 都尝试创建临时节点,例如 /lock
  2. 假设客户端 1 先到达,则加锁成功,客户端 2 加锁失败
  3. 客户端 1 操作共享资源
  4. 客户端 1 删除 /lock 节点,释放锁

Zk采用了临时节点,保证客户端1拿到锁后,只要连接不断,可以一直持有锁;而如果客户端1异常崩溃了,这个临时节点会自动删除,保证了锁一定会被释放。
如果保证连接不断呢?客户端1会和Zk服务器维护一个Session,这个Session依赖客户端的定时心跳来维持连接。如果Zk长时间收不到客户端的心跳,就认为这个Session过期了,也会把这个临时节点删除。

所以Zk也无法保证进程GC、网络延迟下的安全性

也就是说,一个分布式锁,极端情况下,不一定是安全的

Zk的优点:

  • 不需要考虑锁的过期时间
  • watch机制,加锁失败,可以watch等待锁释放,实现乐观锁

Zk的缺点

  • 性能不如Redis
  • 部署运维成本高
  • 客户端与Zk长时间失联,锁会被释放

总结

  1. 使用分布式锁,在上层完成互斥目的,虽然极端情况下锁失效,但是可以最大程度把并发量阻挡在上层,减轻操作资源层的压力
  2. 对于要求数据绝对正确的业务,在资源层做好兜底,设计思路可以借鉴fencing token
### Redisson与Zookeeper分布式锁机制对比 #### Redisson分布式锁实现 Redisson提供了多种类型的分布式锁,其中最常用的是基于Redis的单节点锁定算法。这种锁通过条件设置(如果不存在则设置)来获取锁,并通过原子删除(仅当值匹配时才删除)释放锁[^1]。 ```java RLock lock = redisson.getLock("anyLock"); lock.lock(); try { // 执行业务逻辑 } finally { lock.unlock(); } ``` 这种方式简单易懂,在大多数情况下能够满足需求。然而需要注意的是,这些锁只是尽力而为的最佳实践,并不完全可靠,可能会偶尔失败。因此建议在代码中清晰记录这一点并考虑其适用场景。 #### Zookeeper分布式锁实现 相比之下,Zookeeper实现了更为严格的分布式协调服务。它利用`znode`版本号作为围栏令牌(fencing token),确保每次写入操作都携带一个严格单调递增的令牌。这使得即使在网络分区或其他异常条件下也能保持数据一致性[^2]。 创建临时顺序节点以构建分布式互斥锁: ```java InterProcessMutex mutex = new InterProcessMutex(client, "/examples/locks"); mutex.acquire(); try { // 执行业务逻辑 } finally { mutex.release(); } ``` 这里的关键在于存储服务器会主动验证令牌的有效性,拒绝任何使令牌回退的操作请求。只要锁服务能生成严格单调增加的令牌,则此方法就是安全可靠的。 #### 两者之间的差异 - **可靠性**: Zookeeper由于采用了更复杂的协议设计以及更强的一致性保障措施,在面对复杂网络环境下的表现更加稳定;而Redisson虽然也具备一定的容错能力,但在极端情况下可能无法保证绝对的安全性。 - **性能开销**: 单纯从效率角度来看,采用简单的SETNX(SET If Not eXists)指令实现加解锁过程显然要比维护整个集群状态要轻量得多。所以对于那些不需要极高一致性的应用场景来说,使用Redisson可能是更好的选择。 - **配置难度**: 设置管理一个多副本组成的Zookeeper群集相对较为繁琐一些,涉及到更多参数调整技术细节处理;相反地,只需要连接到一台Redis实例就可以轻松部署起一套基本可用的Redisson方案来了。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值