分布式锁学习笔记

安全性与活跃性(liveness)保证

  • 安全性:互斥。在任意时刻,仅存在唯一客户拥有锁。
  • 活跃性A:死锁释放。最终肯定可以获取到一个锁,即便拥有着锁的客户崩溃宕机。
  • 活跃性B:容错。只要大多数节点正常,客户就能够获取和释放锁。

为什么基于故障转移(容错转移)的实现还不够?

基于REDIS的分布式锁库的简单实现方案:一个锁对应一个键的实例。利用REDIS的超时机制,最终肯定可以获取到锁。锁不存在,即未占用,存在即被占用。锁释放时,删除键即可。
但是这种方案存在问题:如果REDIS主节点崩溃,即便是在REDIS主从架构的集群,无法保证安全性(互斥性),因为REDIS的主从复制是异步的。
以下是该方案中一个触发竞争条件(race condition)的场景:

  1. 客户A从主节点获取锁
  2. 在主节点复制该锁数据到从节点前,主节点就崩溃了
  3. 从节点晋升为主节点
  4. 客户B从(新)主节点获取相同的锁(违反安全性)

单实例方案的正确实现

在竞争条件可接受的情况下,单实例方案是一个可行的方案。
获取锁

SET resource_name my_random_value NX PX 30000
注: 仅当键不存在时,该命令会创建键,30000ms超时。键值应该是全局(客户间)唯一的值,为了释放锁的权力仅有唯一且就是创建锁的客户所有(除超时)。

解锁

if redis.call("get", KEY[1]) == ARGV[1] then
	return redis.call("del", KEYS[1])
else
	return 0
end

分布式方案实现

实现方案其实就是参照分布式一致性算法的,与PAXOS非常类似的。

初始化:

  • 设存在N个(N为奇数)节点,每个节点互相独立,不存在任何协同。

过程:

  1. 获取当前时间(ms)
  2. 尝试按顺序地从N个节点获取锁,使用相同的键名和键值。
    2.1. 客户请求每个节点时也会有一个超时,而该超时的值也有要求。要求是与锁自动释放的超时相比,请求超时要足够小(大概小于千分之五)。这是为了防止客户持续来自已崩溃的节点的响应。
  3. 客户计算获取到锁所花费的时间(当前时间 - 第一步所获得的时间)。向每个节点获取锁的所花费的总时间 小于 锁的自动超时 才认为锁是成功地被获取了。
  4. 如果锁被获取了,则 锁的超时时间 = 初始超时时间 - 获取锁所花费的时间
  5. 如果获取锁失败,则客户需要向所有节点发送释放锁的请求(即便被认为是获取锁失败的节点)

其他:

  • 对每个节点获取锁和释放锁的过程,同单实例版本一致。

算法是安全的吗?

假设一个客户能够从大多数节点中获取锁。所有的节点将会包含一致存活时间的一个键。但是,在异步的环境下,每个键被设置的时间是不相同的,因此它们超时的时间也是不相同的。
假设客户在第一个节点设置键的时间点是T1(请求第一个服务器的那一刻),在最后一个节点设置成功的时间是T2(从最后一个节点接收到设置成功的响应的一刻),那么,我们可以保证第一个被设置的键的最低有效存活时间是MIN_VALIDITY = TTL - (T2 - T1) - CLOCK_DRIFT。 ?

理论的严谨性我们可以参考分布式共识算法PAXOS,在此不作详述。

活跃性参数(Liveness arguments)

系统的活跃性基于三个主要的特点:

  • 自动释放锁:锁最终可以重新被获取和锁定。
  • 事实上,获取锁的客户,在崩溃的时候,都会释放自身所获取的锁以使锁可以被重新获取,而无需等待自动释放的时间。
  • 当客户需要重新请求获取锁时(前面一次尝试失败),它需要等待一段比从多数节点获取锁锁需要的时间。

在遇到网络分裂(network partition)的情况时,我们需要付出TTL时长的可用惩罚。如果存在连续分区,则需要付出无限的惩罚。这个情况经常发生在,客户获取锁了以后,被网络分裂,导致无法释放锁。
基本来说,如果系统内存在无限个连续的网络分区,系统会在无限长的一段时间中不可用。

性能、宕机恢复、持久化和延迟启动

性能

用户使用REDIS作为锁服务器的原因在于,它能提供获取/释放锁的超低延迟。
为了达到这一点,请求N个REDIS服务器节点的策略应该使用多路复用(multiplexing)。

宕机恢复和持久化

设计到宕机恢复的问题,持久化是一个经常被考虑的补救措施。通过宕机的节点重启后,重新读取前面持久化了的原数据镜像。
但是如果这种措施,会非常消耗性能。

延迟启动

实际上,重启后的节点,是对宕机前已存在的锁无感知。对于新的获取锁的请求,是可以正常工作的。
为了避免持久化造成大量性能消耗,在确保锁安全性的同时,我们可以使用延迟启动。宕机重启的节点,保持不可用一段时间,该时长应该比最大TTL(锁的生存时间)大一点。
使用该方法,甚至无需任何持久化措施,也能够保证系统的安全性。
但是,这样会牺牲系统的可用性,因为节点宕机恢复是牺牲了TTL时长的可用性的。如果短期内大量(过半数)节点崩溃,则在TTL时间段内,会产生锁系统全局不可用的情况,因为超半数节点处于宕机或延迟启动的不可用状态。

拓展锁——让算法更可靠

当锁的生存时间接近殆尽时,所获取锁的客户端可以向所有节点请求延长锁的生存时间。
延长锁的算法与请求锁的算法实际上是很类似的。
但是,为了防止违反性质-活跃性A,防止某个客户死锁,延长锁生存时间的申请是有上限的,不能无限申请。

参考

  • https://redis.io/topics/distlock
### 关于 Redis 分布式的教程和笔记 #### 使用 Redisson 实现分布式锁 在 Redis 集群环境中,为了确保分布式系统的高可用性和一致性,推荐采用成熟的库如 Redisson 来实现分布式锁功能。Redisson 提供了一套简单易用的 API 接口用于操作 Redis,并内置支持多种数据结构以及高级特性比如分布式锁、队列等[^1]。 ```java // 创建 RedissonClient 对象实例化配置类 Config 并设置单节点地址连接池大小超时时间等参数 Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RLock lock = redisson.getLock("myDistributedLock"); try { // 加锁并指定最大等待时间和保持时间单位为秒 boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS); } finally { // 解锁释放资源 lock.unlock(); } ``` #### Redlock 协议的理解与应用 Antirez (即 Salvatore Sanfilippo),作为 Redis 的主要开发者之一,在其博客文章中提出了名为 "Redlock" 的算法来解决多个独立 Redis 实例间的一致性问题。该方案通过增加额外的安全措施防止脑裂现象发生从而提高整个集群环境下的可靠性[^2]。 #### CAP理论下Redis的选择考量 当涉及到构建分布式的存储系统时,CAP 定理是一个重要的指导原则。它指出任何网络分区容忍性的服务都无法同时提供强一致性和高度可利用性;因此设计者需要在这两者之间做出取舍。对于像 Redis 这样的内存数据库而言,通常会倾向于 AP 或 CP 方向发展取决于具体应用场景的需求[^3]。 #### 利用Lua脚本增强原子性保障 除了依靠第三方工具外还可以借助 Lua 脚本来加强自定义逻辑执行过程中的事务安全性。例如可以将加解锁动作封装成一段完整的命令序列交给服务器端一次性完成以此减少中间状态暴露的风险[^4]。 ```lua local key = KEYS[1] local value = ARGV[1] if(redis.call('exists',key)==0)then redis.call('setex',key,'10',value) else return nil; end return true; ``` #### Spring Boot 中集成 AOP 拦截器模式管理锁机制 最后值得一提的是现代 Java 应用程序开发框架往往提供了便捷的方式帮助程序员更好地控制业务流程走向。以 Spring Boot 为例可以通过创建切面组件围绕目标函数调用周期实施环绕通知进而达到自动获取/释放关联对象的目的[^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值