You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
1、首先客户端 SET 时,VALUE 要是全局唯一的,也可以使用 UUID,并记下这个 VALUE 值;
2、使用单独的线(协程)程 GET KEY,并对比 VALUE 值是否与前面的记录的值相同,如果相同,说明当前客户端仍然持有锁,通过 EXPIRE 更新 KEY 失效时间;
3、当工作完程,释放锁(删除 KEY)之前,先关闭这个续约线程,并且删除 KEY 之前也要比较 VALUE 是否与本客户端设置的一样,防止释放别的客户端持有的锁;
虽然,现在设计分布式系统,都是要满足分区容错性的情况下,对 CA 进行取舍,但是在传统系统架构中,CA 系统是普遍的,比如数据库分区,不做主备,且将事务内数据放到一个数据库或者干脆允许某条数据记录查询失败。
这是因为,传统系统,节点一般都是在同一网络坏境,发生网络故障的概率很小,所以可以不用考虑分区容忍。但是,大多数场景下,网络故障或超时是很正常,很频繁的,必须要首先考虑分区容错,一般做法就是数据备份到多个节点,也即主备。
分布式锁的实现及原理
概述
锁是在执行多线程时用于强行限制资源访问的同步机制,在分布式系统场景下,为了使多个进程(实例)对共享资源的读写同步,保证数据的最终一致性,而引入了分布式锁。
分布式锁应具备以下特点:
可以基于数据库,缓存,中间件实现分布式锁,比较主流的是使用 Redis 或 Etcd (java 可能更多的是用 ZooKeeper) 来实现,当然也可以基于数据库等支持事务的中间件实现,但相对不够健壮,也不够安全,一般不推荐,这里就不展开说明了。结合以上的四个特点,下面将深入讨论这两种方案的实现方式与原理。
实现方案
基于 Etcd
Ectd 是一个高可用的键值存储系统,具体以下特点:
重要的是,etcd 支持以下功能,正是依赖这些功能来实现分布式锁的:
在实现分布式锁时,多个程序同时抢锁,根据 Revision 值大小依次获得锁,可以避免 “羊群效应” (也称 “惊群效应”),实现公平锁。
就实现过程来说,跟“买房摇号”很相似。
1、定义一个 key 目录(如:
/xxx/lock/
)用于存放客户端(进程)的操作 ID。类似申请买房的号码牌;2、客户端先
put
key/xxx/lock/id
,id 是全局唯一的,可以使用 UUID,并设置过期时间TTL
,防止死锁。记下返回的Revision
值R
。类似你拿到一个选房序号,并规定了进去选房时间,超时还没有选中,就失效了;3、
get
目录/xxx/lock/
下所有的 key 及对应的Revision
值,与上一步返回的Revision
值进行比较:Revision
值 R 小于或等于目录下所有的 key 对应的Revision
,则当前客户端获取到了锁。类似你是排在第一个选房的,不用等了,直接选房就是;/xxx/lock/
。盯紧大屏幕,等待排你前面的人选房;4、当所有靠前的 key 都被删除之后,则意味着的客户端获取到了锁。类似前面的人都选好房或者弃权了,终于轮到你选房了!
但是,这里有两个问题,也是分布式锁实现方案之间的重要区别:
Etcd 和 Redis 给出了不同的答案,后面将会对比阐述。
基于 Redis
Redis 可以使用 SET 命令:
这里的 KEY 是同一个,VALUE 最好是全局唯一的(原因后面会知道),如果执行成功,则意味着获取到了锁;如果失败则循环尝试,类似自旋锁的获取过程,但这里不需要太频繁,可以
Sleep
一段时间,还可以对续约次数进行限制。看起来,这个实现方案比 Etcd 实现要简单很多,区别就是,Etcd 实现的是公平锁。但是,结合上面提的两个问题,就会发现,这只是一个简单的实现,并没有给出问题的答案。
方案对比
对于那两个问题,Etcd 与 Redis 给出了不同的答案。
1)问题一,租约(比工作完成时间)提前到期的问题。
本身支持
KeepAlive
机制,来进行租约续期,在put
操作成功之后,对 KEY 设置 KeepAlive 即可。Etcd 的租约是与 KV 单独分开的,有自己的租约 ID,所以实现起来并不复杂。Redis 本身没有
KeepAlive
的机制,所以,只能客户端自己模拟实现:1、首先客户端 SET 时,VALUE 要是全局唯一的,也可以使用 UUID,并记下这个 VALUE 值;
2、使用单独的线(协程)程 GET KEY,并对比 VALUE 值是否与前面的记录的值相同,如果相同,说明当前客户端仍然持有锁,通过
EXPIRE
更新 KEY 失效时间;3、当工作完程,释放锁(删除 KEY)之前,先关闭这个续约线程,并且删除 KEY 之前也要比较 VALUE 是否与本客户端设置的一样,防止释放别的客户端持有的锁;
两种续约方式,基本原理,效果都类似,Etcd 更优雅一些。
2)问题二,保证节点数据一致性的问题。
这是分布式架构中的基础也是经典问题,一般分布式系统中为了保证分区容错性,节点(数据)都是主备的。
对 etcd 主节点写入时,要保证所有主从节点都写入成功,才会返回写入完成,也即是主从同步复制,这样就可以保证主从节点的数据强一致性。Redis 由于历史原因,刚开始都是单机部署的,后面才支持集群部署,为了保证性能,主从使用异步复制,因此,并不保证节点间数据的强一致性。
Redis 集群一般有多个 Master 节点,数据负载到不同的 Master 节点上(数据分片)。这种场景下,实现分布式锁时更加麻烦,因为,为了保证当前只会出现一把锁,就必须要设置 KV 到所有 Master 节点才行(实际只要超过一半就行)。为了解决这个问题,Redis 作者基于 Redis 设计实现了 Redlock 算法,实现过程过程如下:
当然,也可以取个巧,客户端约定在同一个 Master 申请/释放 锁,但是,这样客户端处理起来又太累赘了,不够通用。
小结
通过深入分析分布式锁的实现,可以发现,由于 Redis 主要是用于数据读写缓存,需要优先保证大流量场景下读写性能,分区容错性以及服务可用性是最重要的;而 Etcd 主要用于配置分发,必须要保证数据强一致性以及分区容错性。
这也就是 CAP 理论实例,根据系统应用场景来做取舍,选择最合适的实现方案。对于分布式锁的使用场景来说,使用 Etcd 来实现分布式锁,要更加的简洁,也更加安全。
虽然 Etcd (V3) 官方已经支持了分布式锁的 API 实现,为了理解的更深刻,我自己也造了个轮子https://github.com/jinhailang/rainforest/tree/master/ivy。此外,因为支持事务(基于软件事务内存机制(STM)实现),所以,还可以使用事务来实现分布式锁,具体参考NewSTM 使用实例。
PS: 事务也是非常常见而且非常重要的概念,我也在文章 #48 较详细的阐述了事务的应用及原理。
参考
The text was updated successfully, but these errors were encountered: