Safety and Liveness
通常意义下的锁需要满足:
- Safety:互斥访问
- Liveness:
- 有限等待
- 空闲让进
分布式环境下多了一些properties
- Liveness:容错:集群节点的大多数节点都存活,那么客户端就可以获取锁和释放锁
基于Redis单节点的锁
获取锁
Redis客户端向Redis节点发送命令:
SET resource_name my_random_value NX PX 30000
命令仅在resource_name
不存在的情况下才会成功。成功才表示获取锁成功。下面解释各个参数:
my_random_value
:客户端随机生成的一个字符串。这个字符串需要保证:在足够长的一段时间内全局唯一,不能有其他客户端使用相同的字符串来申请锁。NX
表示只有当resource_name
对应的key值不存在的时候才能SET
成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。PX 30000
表示这个键值对在30秒后自动过期(被自动删除)。锁的过期时间我们接下来会讨论。
释放锁
当客户端执行下面的Redis Lua脚本来释放锁:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
ARGV[1]
:这个值应当是获取锁时使用的my_random_value
KEYS[1]
:获取锁时使用的resource_name
实现分布式锁的挑战
持锁节点崩溃导致锁丢失
锁需要设置一个过期时间。
当一个客户端获取锁成功之后崩溃了,或由于网络分区(network partition)无法和Redis节点通信了,那么它就会一直持有这个锁,而其它客户端永远无法获得锁了。
锁获取和释放的原子性
- 锁获取分两步:第一步设置值,第二步设置过期时间
- 如果客户端在执行完
SETNX
后崩溃了,就不会执行EXPIRE
了,导致它一直持有这个锁。 - 因此锁获取需要使用一个原子的命令(如上所属,或者开启redis的mini transaction,或者使用redis lua)
- 如果客户端在执行完
- 锁的释放分三步:
- 查询锁的状态
- 判断获得的锁是不是和自己持有的那把
- 释放锁
- 问题:
- 客户端1准备释放锁,执行了
GET
操作获取value。 - 客户端1将value与
my_random_value
进行对比。 - 客户端1由于网络原因,
DEL
操作一直没到redis上。 - 过期时间到了,锁自动释放。
- 客户端2获取到了对应同一个资源的锁。
- 客户端1的
DEL
操作到达redis,释放掉了客户端2持有的锁。
- 客户端1准备释放锁,执行了
- 因此上述的三步需要保证原子性,保证不会有其他人的指令插入
时延导致释放他人持有的锁
- 设置一个随机字符串
my_random_value
很有必要,可以保证一个客户端释放的锁必须是自己持有的那个锁 - 如果是一个固定值,我们假设释放锁的操作是原子的
CompareAndDel
那么:- 客户端1获取锁成功。
- 客户端1的
CompareAndDel
操作一直在网络传输中 - 过期时间到了,锁自动释放了。
- 客户端2获取到了对应同一个资源的锁。
- 客户端1的
CompareAndDel
操作到达,由于只判定固定值,因此可以释放掉客户端2持有的锁。
主备集群同步导致锁非互斥
- 客户端1从Master获取了锁。
- Master宕机了,存储锁的key还没有来得及同步到Slave上。
- Slave升级为Master。
- 客户端2从新的Master获取到了对应同一个资源的锁。
锁过期时间长度权衡
锁的有效时间(lock validity time)
- 过期时间太短:锁在操作共享资源前就过期了,访问失去保护;
- 过期时间太长:持有锁的客户端crash,那么整体需要等待锁过期,效率不高
在访问共享资源时锁过期
- 难点:这个问题棘手在即使分布式锁服务完全没有问题,访问资源的这个操作还是会有一定的问题
- 描述:客户端1在获取到锁后,对共享资源的访问实质位于实质的锁过期之后
- 这种情况可能是因为系统调度、语言GC或对共享资源的网络访问延时
一个解决手段
- 手段:自增的token,服务收到小的token就拒绝掉
- 麻烦点:需要改造服务
- 潜在的问题点:client2如果也产生了GC,到达Storage的请求还是保持fence token小的先到,那么是否就一定没问题呢?
Redlock 算法流程
算法条件:
- 假设我们有 n = 5 个 Redis master
- 我们认为基于Redis单节点的获取分布式锁为
LockAcquire(resourceName, random_value)
- 我们认为基于Redis单节点的释放分布式锁为
LockRelease(resourceName, random_value)
获取锁算法流程:
- 以毫秒为单位获取当前时刻
t
- 对 n 个节点依次顺序执行
LockAcquire(resourceName, random_value)
,- 所有的请求
resourceName
和random_value
保持一致 - 假设锁过期时间是E,客户端和 redis 交互的超时时间阈值认为是T,那么
T << E
。- 比如 过期时间是10s,那么超时应该是 50ms 左右
- 一旦 redis 在超时时间内没有返回,那么立马和下一个redis节点通信
- 所有的请求
- 如果客户端获得了 redis cluster 的 majority 回复,且
当前时刻 - t
小于锁的过期时间E
,那么认为锁获取成功。 - 如果获取锁失败,那么客户端顺序解锁
释放锁算法流程:
对每个 redis master 发送 LockRelease
- 为什么是每个?因为可能发送 LockAcquire 时,返回包丢失了;为了保证锁的清理,需要向每个节点发送
LockRelease
Redlock 解决了什么问题
Redlock只解决了分布式锁的提供者的单点问题。
Redlock 的问题
- 持久化失败 & 宕机 导致的互斥失效问题(默认情况下,Redis的AOF持久化每秒执行fsync写一次盘)
- 问题描述
- 假设一共有5个Redis节点,客户端A靠前三个节点的正确回复获得了资源N的锁;
- 第三个节点持久化失败,并宕机重启
- 客户端B靠后三个节点的正确回复获得了资源N的锁
- 问题解决
- 延迟重启,第三个节点宕机后,等待一个锁的过期时间后再重启
- 问题描述
- 锁过期时间长度权衡问题并没有解决
- 在访问共享资源时锁过期的问题没有解决
- Time skew 导致的锁资源提前释放
- 描述
- 假设一共有5个Redis节点,客户端A靠前三个节点的正确回复获得了资源N的锁;
- 第三个节点收到了一个NTP的时钟校准,导致锁提前释放
- 客户端B靠后三个节点的正确回复获得了资源N的锁
- 问题解决
- 把锁周期设大一点,这样time skew发生了也在合理范围内(额
- 运维手段避免大的time skew
- 描述
基于Zookeeper的不可扩展锁
基于Zookeeper
// Aquire Lock
LOCK():
WHILE TRUE:
IF CREATE("f", data, ephemeral=TRUE):
RETURN // 创建成功就返回,说明获得了锁
IF EXIST("f", watch=TRUE):
WAIT // wait 语义是有watch提供的
UNLOCK():
DELETE("f")
问题:
- 惊群效应。并发度的提高,效果越严重。每次锁释放通知所有人,然后所有人都会要执行一边CREATE的RPC,代价很高
基于Zookeeper的可扩展锁
基于Zookeeper
// Aquire Lock
LOCK():
// 可以获得一个带有全局序列号的文件
CREATE("f", data, sequential=TRUE, ephemeral=TRUE)
WHILE TRUE:
// 列出了所有以“f”开头的文件,也就是所有的Sequential文件
// 也就是正在竞争锁的所有role
LIST("f*")
// 如果自己的file最小,说明自己最早到
// FIFO 获取锁
IF NO LOWER #FILE:
RETURN
// 等待比自己次小的文件被删除,重新检查锁的获取情况
IF EXIST(NEXT LOWER #FILE, watch=TRUE):
WAIT
- 为什么需要循环LIST?
- 比自己小的可能已经挂了,因此需要时常检查一下
- 和上面的区别是什么?
- 上面的WATCH变化的RPC通知,是和进程数量成正比的
- 下面的只需要等比自己次小的FILE的WATCH通知即可,是一个常数级
关于Zookeeper的锁
- Zookeeper的锁并不能解决上文中提到的「在访问共享资源时锁过期」的问题。
- 但是Zookeeper的watch操作,可以尽可能大限度的告诉agent,锁已经过期了,让agent可以有办法对相关情况进行处理
基于Chubby的锁
这里只谈谈「在访问共享资源时锁过期」这个问题
- 分布式锁服务自己提供了一个全局递增的 lock generation number
- 有效性检查:
CheckSequencer(sequencer)
共享资源服务器可以通过这个API可以帮助检查锁是否有效- 组织锁获取:即拿锁的人失联了不知死活,Chubby就会直接组织其他人获取同一把锁直到锁过期。很保守但相对保证正确性的手段。
Q&A
- 分布式锁和内存锁的区别和关系
- Safety一致,Liveness有扩展
- 分布式锁和 linearizibility 之间的关系
- 一个实现了 linearizibility 的系统解决了分布式锁的集群问题,提高了分布式锁服务的容错能力
- kleppmann所提到的fence token 和 内存锁的mem fence/barrier 之间的关系和区别
- 一点点类似Load-Load barrier禁止顺序的冲排序
- fence token会把旧的给拒绝掉(禁止错误的顺序)
- 分布式锁的实现方式有哪些,需要注意什么
- 见「实现分布式锁的挑战」
- 实现分布式锁一定要有CAS的能力在
- 实现分布式锁,最好有能通知申请锁的客户端的能力
- 想想看,进程间的mutex的实现,是把锁放在可写的共享内存(mmap)上
- 如果持有锁的进程退出了,有一个全知全能的OS会告诉其他等在mutex的进程,「持有锁的进程退出了」这个信息
- Windows下是:
WaitForSingleObject(...)
返回WAIT_ABANDONED
- Linux下是:
pthread_mutex_lock(...)
返回EINVAL
- Windows下是:
- 但是分布式锁服务没有一个 perfect 的 failure detector 去探知某个agent是否已经死透了
- 因此和agent保持联系,在它重启之后告诉他「锁已经过期了」这件事是最不坏的结果
- Refs:https://stackoverflow.com/questions/13110661/can-exiting-from-a-process-that-is-locking-a-mutex-cause-a-deadlock
- 如何去衡量一个分布式锁的安全性
- 即要看 Safety & Liveness 有没有受损。
- 一个良好的设计应当保证 Safety,在特殊情况下可以有限损害 Liveness
- 分布式锁的性能相关 应该怎么衡量
- 好问题。知道的不是很确切。。几个考量的点:
- 同时能承接多少资源的发锁的请求
- Response Time
- 好问题。知道的不是很确切。。几个考量的点:
小结
- 从「不存在一个完美的 failure detector」的角度出发,几乎可以断定不存在一个像内存锁版完美简洁的分布式锁
- 实现分布式锁应当考虑效率和正确性
- 使用分布式一致性协议,保证的是容错性
- 如果考虑到效率,简单的单机 Redis 就已经足够了
- 实现分布式锁的系统需要:
- 【必须】系统能够提供原子的 CAS 语义
- 【必须】系统必须为锁提供 lease 机制
- 【可选】系统能够提供通知 agent 的机制
- 【可选】系统能够提供 linearizability 的能力,以避免服务的单点故障
- 【可选】系统需要能够提供一种fence机制,保证旧指令的无效
- Zookeeper & Redis & etcd & 数据库 都是实现分布式锁的不坏的选择
REFS
- SOFAJRaft-RheaKV 分布式锁实现剖析 | SOFAJRaft 实现原理https://www.sofastack.tech/blog/sofa-jraft-rheakv-distributedlock/
- 基于Redis的分布式锁到底安全吗(上)? http://zhangtielei.com/posts/blog-redlock-reasoning.html
- Distributed locks with Redis https://redis.io/topics/distlock
- https://hn.matthewblode.com/item/11065933
- How to do distributed locking https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
- https://hn.matthewblode.com/item/11059738
- Is Redlock safe?
- https://web.archive.org/web/20160528200726if_/https://storify.com/martinkl/redlock-discussion
- The Chubby lock service for loosely-coupled distributed systems
- Youtube: The Chubby lock service for loosely-coupled distributed systems
- Chubby分布式锁服务总结 - helices的文章 - 知乎 https://zhuanlan.zhihu.com/p/64554506
- 分布式锁真的安全吗? - buckethead的文章 - 知乎 https://zhuanlan.zhihu.com/p/51562218
- https://stackoverflow.com/questions/13110661/can-exiting-from-a-process-that-is-locking-a-mutex-cause-a-deadlock