分布式锁 Distributed Locks
分布式锁的实现是人们探索的比较多的一个方向,在 Redis 的官方网站上专门有一篇文档介绍 基于 Redis 的分布式锁 ,其中提出了 Redlock 算法,并列出了多种语言的实现案例,这里作一简要介绍。
Redlock 算法着眼于满足分布式锁的三个要素:
- 安全性:保证互斥,任何时间至多只有一个客户端可以持有锁
- 免死锁:即使当前持有锁的客户端崩溃或者从集群中被分开了,其它客户端最终总是能够获得锁。
- 容错性:只要大部分的 Redis 节点在线,那么客户端就能够获取和释放锁。
锁的一个简单直接的实现方法就是用 SET NX 命令设置一个设定了存活周期 TTL 的 Key 来获取锁,通过删除 Key 来释放锁,通过存活周期来保证避免死锁。不过这个方法存在单点故障风险,如果部署了 master/slave 节点,则在特定条件下可能会导致安全性方面的冲突,比如:
在 Redlock 算法中,通过类似于下面这样的命令进行加锁:
- SET resource_name my_random_value NX PX 30000
这里的 my_random_value 为全局不同的随机数,每个客户端需要自己产生这个随机数并且记住它,后面解锁的时候需要用到它。
解锁则需要通过一个 Lua 脚本来执行,不能简单地直接删除 Key,否则可能会把别人持有的锁给释放了:
- if redis.call("get",KEYS[1]) == ARGV[1] then
- return redis.call("del",KEYS[1])
- else
- return 0
- end
这个 ARGV[1] 的值就是前面加锁的时候的 my_random_value 的值。
如果需要更好的容错性,可以建立一个有 N(N 为奇数)个相互独立完备的 Redis 冗余节点的集群,这种情况下,一个客户端获得锁和释放锁的算法如下:
Redlock 算法不需要保证 Redis 节点之间的时钟是同步的(不论是物理时钟还是逻辑时钟),这点和传统的一些基于同步时钟的分布式锁算法有所不同。Redlock 算法的具体的细节可以参阅 Redis 的官方文档,以及文档中列出的多种语言版本的实现。
选举算法
在分布式系统中,经常会有些事务是需要在某个时间段内由一个进程来完成,或者由一个进程作为 leader 来协调其它的进程,这个时候就需要用到选举算法,传统的选举算法有欺负选举算法(霸道选举算法)、环选举算法、Paxos 算法、Zab 算法 (ZooKeeper) 等,这些算法有些依赖于消息的可靠传递以及时钟同步,有些过于复杂,难以实现和验证。新的 Raft 算法相比较其它算法来说已经容易了很多,不过它仍然需要依赖心跳广播和逻辑时钟,leader 需要不断地向 follower 广播消息来维持从属关系,节点扩展时也需要其它算法配合。
选举算法和分布式锁有点类似,任意时刻最多只能有一个 leader 资源。当然,我们也可以用前面描述的分布式锁来实现,设置一个 leader 资源,获得这个资源锁的为 leader,锁的生命周期过了之后,再重新竞争这个资源锁。这是一种竞争性的算法,这个方法会导致有比较多的空档期内没有 leader 的情况,也不好实现 leader 的连任,而 leader 的连任是有比较大的好处的,比如 leader 执行任务可以比较准时一些,查看日志以及排查问题的时候也方便很多,如果我们需要一个算法实现 leader 可以连任,那么可以采用这样的方法:
- import redis
- rc = redis.Redis()
- local_selector = 0
- def master():
- global local_selector
- master_selector = rc.incr('master_selector')
- if master_selector == 1:
- # initial / restarted
- local_selector = master_selector
- else:
- if local_selector > 0:
- # I'm the master before
- if local_selector > master_selector:
- # lost, maybe the db is fail-overed.
- local_selector = 0
- else:
- # continue to be the master
- local_selector = master_selector
- if local_selector > 0:
- # I'm the current master
- rc.expire('master_selector', 20)
- return local_selector > 0
这个算法鼓励连任,只有当前的 leader 发生故障或者执行某个任务所耗时间超过了任期、或者 Redis 节点发生故障恢复之后才需要重新选举出新的 leader。在 master/slave 模式下,如果 master 节点发生故障,某个 slave 节点提升为新的 master 节点,即使当时 master_selector 值尚未能同步成功,也不会导致出现两个 leader 的情况。如果某个 leader 一直连任,则 master_selector 的值会一直递增下去,考虑到 master_selector 是一个 64 位的整型类型,在可预见的时间内是不可能溢出的,加上每次进行 leader 更换的时候 master_selector 会重置为从 1 开始,这种递增的方式是可以接受的,但是碰到 Redis 客户端(比如 Node.js)不支持 64 位整型类型的时候就需要针对这种情况作处理。如果当前 leader 进程处理时间超过了任期,则其它进程可以重新生成新的 leader 进程,老的 leader 进程处理完毕事务后,如果新的 leader 的进程经历的任期次数超过或等于老的 leader 进程的任期次数,则可能会出现两个 leader 进程,为了避免这种情况,每个 leader 进程在处理完任期事务之后都应该检查一下自己的处理时间是否超过了任期,如果超过了任期,则应当先设置 local_selector 为 0 之后再调用 master 检查自己是否是 leader 进程。