简介
在一些分布式系统中,应用与应用之间是相互独立部署的,Java应用运行在不同的JVM中,所以,在操作一些共享资源的时候,使用JDK提供的Lock工具类时就有些力不从心,这时候就需要借助外力来实现分布式一致性问题。
通常会使用以下三种方式进行实现:
- 基于数据库实现分布式锁(悲观锁机制)
- Zookeeper分布式锁
- Redis分布式锁
下面简单对比几种方式的优缺点:
方式 | 优点 | 缺点 |
---|---|---|
数据库 | 实现简单、易于理解 | 对数据库压力大 |
Redis | 易于理解 | 自己实现、不支持阻塞 |
Zookeeper | 支持阻塞 | 需要理解Zookeeper、程序复杂 |
Curator | 提供锁的方法 | 依赖Zookeeper、强一致 |
Redisson | 提供锁的方法、可阻塞 |
安全和活性的保证
Redis官方文档提出以下三点作为分布式锁的最低保证:
- 互斥,在任何给定时刻,只有一个客户端可以持有锁
- 无死锁,最终,即使锁定资源的客户端崩溃或分区,也始终可以获得锁
- 容错能力,只要大多数Redis节点都处于运行状态,客户端就可以获取和释放锁
基于主备架构实现的不足
使用Redis实现分布式锁最简单的方法就是加锁的时候创建一个带有过期时间Key(这样是为了防止出现死锁),当客户端需要释放锁的时候删除这个Key。
从表面上看没有什么问题,但是当Redis出现宕机的时候怎么办?为了解决单点故障问题,我们可以添加一个从节点,当主节点不可用的时候,切换到从节点,但是这样实际上是不可行的,因为Redis使用的是异步复制。
该模型明显存在的竞争条件:
- 客户端A获取主节点的锁
- 主节点将Key同步到从节点之前发生宕机
- 从节点切换成主节点
- 客户端B获取到相同资源的锁,此时A和B同时持有锁,违反了安全性
单实例方案
假设可以克服以上单节点不足的问题,我们可以使用以下命令实现分布式锁:
|
|
该命令仅在Key不存在(NX)、且到期时间(PX)为30000毫秒的情况下才设置Key。Key的值为一个随机数,该值要求必须全局唯一,使用全局唯一值是为了在删除Key的时候,Key的值是我们之前设置的值时,才删除Key。(Tips:我们总不能删除其他客户端设置的Key吧?)
可以使用以下Lua脚本完成,因为Lua脚本可以保证两个操作的原子性。
|
|
RedLock算法
在算法的分布式版本中,我们假设有5个Master节点,这些节点是完全独立的,我们将各个节点部署在不同的服务器中,以保证他们同时出现故障的概率。
为了获取锁,客户端执行以下操作:
- 客户端获取当前时间的时间戳
- 客户端尝试在N(N=5)个节点上以相同的Key和Value获取一个锁(此处和单实例方式相同)
- 客户端通过从当前时间中减去在步骤1中获得的时间戳,来计算获取锁所花费的时间,当客户端能够在大多数实例(至少3个)中获取锁时 ,并且获取锁所花费的总时间小于锁有效时间,则认为已获取锁。
- 如果获取了锁,则将其有效时间视为初始有效时间减去经过的时间
- 如果客户端由于某种原因(无法锁定N / 2 + 1实例或有效时间为负数)而未能获得该锁,它将尝试解锁所有实例
释放锁
释放锁很简单,只需在所有实例中释放锁(即使之前在某个实例中没有获取到锁)。
RedLock注意点
- 先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移
- 对于以N/2+ 1(也就是一半以 上)的方式判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况,从而使锁失效
- 一个client锁定大多数事例耗费的时间大于或接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务) ;只要在TTL时间内成功获取一半以上的锁便是有效锁;否则无效
系统具有活性的三大特征
- 能够自动释放锁
- 再获取锁失败(不到一半以上),或任务完成后能够释放锁,不用等到其自动过期
- 再客户端重试获取锁之前(第一次失败到第二次失败之间的间隔时间)大于获取锁消耗的时间
参考Redis官方文档 https://redis.io/topics/distlock RedLock分析 [http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html][http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html]