分布式锁的实现,目前常用的方案有以下三类:
但是在使用过程中还是要留意以下的集中安全性问题。
我们看下面这个典型死锁场景。
一个客户端获取锁成功,但是在释放锁之前崩溃了,此时该客户端实际上已经失去了对公共资源的操作权,但却没有办法请求解锁(删除 Key-Value 键值对),那么,它就会一直持有这个锁,而其它客户端永远无法获得锁。
我们的解决方案是:在加锁时为锁设置过期时间,当过期时间到达,Redis 会自动删除对应的 Key-Value,从而避免死锁。需要注意的是,这个过期时间需要结合具体业务综合评估设置,以保证锁的持有者能够在过期时间之内执行完相关操作并释放锁。
为了避免死锁,可利用 Redis 为锁数据(Key-Value)设置自动过期时间,虽然可以解决死锁的问题,但却存在隐患。
我们看下面这个典型场景。
这时我们可采取的解决方案见下。
my_random_value
,即客户端 A 恢复后,在操作共享资源前应比较目前自身所持有锁的 my_random_value
与 Redis 中存储的 my_random_value
是否一致,如果不相同,说明已经不再持有锁,则放弃对共享资源的操作以避免互斥性失效的问题。为了保证每次解锁操作都能正确进行,需要引入全局唯一变量 my_random_value
。具体而言,解锁需要两步,先查询(GET)锁对应的 Value,与自己加锁时设置的 my_random_value
进行对比,如果相同,则可确认这把锁是自己加的,然后再发起解锁(DEL)。需要注意的是,GET 和 DEL 是两个操作,非原子性,那么解锁本身也会存在破坏互斥性的可能。
下面是典型场景。
下面给出解决方案。
如何保障解锁操作的原子性呢?在实践中,我总结出两种方案。
1. 使用 Redis 事务功能,使用 Watch 命令监控锁对应的 Key,释放锁则采用事务功能(Multi 命令),如果持有的锁已经因过期而释放(或者过期释放后又被其它客户端持有),则 Key 对应的 Value 将改变,释放锁的事务将不会被执行,从而避免错误的释放锁,示例代码如下:
2. Redis 支持 Lua 脚本并保证其原子性,使用 Lua 脚本实现锁校验与释放,并使用 Redis 的 eval 函数执行 Lua 脚本,代码如下:
考虑 Redis 节点宕机,如果长时间无法恢复,则导致锁服务长时间不可用。为了保证锁服务的可用性,通常的方案是给这个 Redis 节点挂一个 Slave(多个也可以),当 Master 节点不可用的时候,系统自动切到 Slave 上。但是由于 Redis 的主从复制(Replication)是异步的,这可能导致在宕机切换过程中丧失锁的安全性。
我们看下典型场景。
解决方案有两个。
my_random_value
和 Redis 服务端中实际存储的 my_random_value
。显然,这里仍存在一个问题。如果校验完毕后,Master 数据尚未同步到 Slave 的情况下 Master 宕机,该如何是好?诚然,我们可以为 Redis 服务端设置较短的主从复置周期,以尽量避免上述情况出现,但是,隐患还是客观存在的。
上面介绍了基于单 Redis 节点的分布式锁在主从故障倒换(Failover)时会产生安全性问题。针对问题场景,Redis 的作者 Antirez 提出了 RedLock,它基于 N 个完全独立的 Redis 节点,其原理基于分布式一致性算法的核心理念:多数派思想,不过,RedLock 目前还不成熟,争议较大,本节仅作简要介绍。
运行 Redlock 算法的客户端依次执行以下步骤,来进行加锁的操作:
my_random_value
,也包含过期时间(比如 PX 30000,即锁的有效时间)。为了保证在某个 Redis 节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(Time Out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个 Redis 节点获取锁失败以后,应该立即尝试下一个 Redis 节点。这里的失败,应该包含任何类型的失败,比如该 Redis 节点不可用。>=N/2+1
)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(Lock Validity Time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。N/2+1
,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有 Redis 节点发起释放锁的操作(即前面介绍的 Redis Lua 脚本)。我们再来了解下解锁步骤。上面描述的只是获取锁的过程,而释放锁的过程比较简单,即客户端向所有 Redis 节点发起释放锁的操作,不管这些节点在获取锁的时候成功与否。
N 个 Redis 节点中的大多数能正常工作,就能保证 Redlock 正常工作,因此理论上它的可用性更高。上述所描述的问题在 Redlock 中就不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的。
从加锁的过程,读者应该可以看出:RedLock 对系统时间是强依赖的,那么,一旦节点系统时间出现异常(Redis 节点不在同一台服务器上),问题便又来了,如下场景,假设一共有 5 个 Redis 节点:A、B、C、D、E。
- END -