分布式锁
什么是分布式锁
- 线程锁
- 进程锁
- 分布式锁
分布式锁的设计原则
分布式锁的实现方案
- 基于数据库
- 基于Redis
- 单个Redis实例:set NX PX + Lua
- Redis集群:RedLock
- 基于zookeeper
- 基于Consul
基于Redis实现分布式锁
单个Redis实例 - set NX PX + Lua
- 加锁 set NX PX + 重试 + 重试间隔
- 解锁 采用lua脚本
1 |
|
为什么建议用lua脚本来解锁,而不是直接RedisTemplate.detele(key)
当多个线程同时争抢同一个 Redis 锁时,如果这些线程都是通过使用 RedisTemplate 的 delete() 方法来释放锁的话,可能会出现以下情况:
- 线程 A 成功获取了锁,并设置了一个过期时间;
- 过了一段时间后,锁的过期时间到了,Redis 自动将锁删除;
- 同时,线程 B 也在尝试获取锁,由于此时锁已经被 Redis 删除了,线程 B 成功获取了锁;
- 线程 A 在这个时候调用了 RedisTemplate 的 delete() 方法来释放锁,由于此时 Redis 中已经不存在该锁了,所以线程 A 的操作实际上是删除了线程 B 获取到的锁,从而导致线程 B 的锁失效。
因此,使用 RedisTemplate 的 delete() 方法来释放锁的方式可能存在删除其他线程获取的锁的风险。为了避免这种情况的发生,可以使用 Redis 的 Lua 脚本,在 Redis 中执行删除操作,确保只删除对应值的 key-value 对,避免误删其他线程的锁。
Redis集群 - RedLock
假设有两个服务A、B都希望获得锁,有一个包含了5个redis master的Redis Cluster,执行过程大致如下:
- 客户端获取当前时间戳,单位: 毫秒
- 服务A轮寻每个master节点,尝试创建锁。(这里锁的过期时间比较短,一般就几十毫秒) RedLock算法会尝试在大多数节点上分别创建锁,假如节点总数为n,那么大多数节点指的是n/2+1。
- 客户端计算成功建立完锁的时间,如果建锁时间小于超时时间,就可以判定锁创建成功。如果锁创建失败,则依次(遍历master节点)删除锁。
- 只要有其它服务创建过分布式锁,那么当前服务就必须轮询尝试获取锁。
Redis的客户端 Redisson
Redis的客户端(Jedis, Redisson, Lettuce等)都是基于上述两类形式来实现分布式锁的,只是两类形式的封装以及一些优化(比如Redisson的watch dog)。
以基于Redisson实现分布式锁为例(支持了 单实例、Redis哨兵、redis cluster、redis master-slave等各种部署架构):
- 特色
- redisson所有指令都通过lua脚本执行,保证了操作的原子性
- redisson设置了watchdog看门狗,“看门狗”的逻辑保证了没有死锁发生
- redisson支持Redlock的实现方式。
- 过程
- 线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
- 线程去获取锁,获取失败: 订阅了解锁消息,然后再尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
- 互斥
如果这个时候客户端B来尝试加锁,执行了同样的一段lua脚本。第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在。
接着第二个if判断,判断myLock锁key的hash数据结构中,是否包含客户端B的ID,但明显没有,那么客户端B会获取到pttl myLock返回的一个数字,代表myLock这个锁key的剩余生存时间。
此时客户端B会进入一个while循环,不停的尝试加锁。 - watch dog自动延时机制
客户端A加锁的锁key默认生存时间只有30秒,如果超过了30秒,客户端A还想一直持有这把锁,怎么办?其实只要客户端A一旦加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间。 - 可重入
每次lock会调用incrby,每次unlock会减一。
进一步理解
- 借助Redis实现分布式锁时,有一个共同的缺陷: 当获取锁被拒绝后,需要不断的循环,重新发送获取锁(创建key)的请求,直到请求成功。这就造成空转,浪费宝贵的CPU资源。
- RedLock算法本身有争议,具体看这篇文章How to do distributed locking 以及作者的回复Is Redlock safe?