@toc:
- 分布式锁的使用场景?
- 分布式锁的设计要点?
- 如何用Redis实现分布式锁?
➤ 什么情况需要分布式锁:
多个Client操作一个资源并保证互斥性, 进程内的锁机制无法实现
➤ 设计要点:
- 互斥性: A/B Client 同时只能有一个抢占到锁;
- 锁超时: 如果抢到锁的Client挂掉, 锁仍能在超时后被解锁;
- 高可用: Redis节点宕机仍能保证上述特性;
➤ 方案1:
- 加锁:
SET k random_val NX PX 3000
, - 加锁失败: 失败后休眠一段时间再尝试
- 放锁: 释放前先Get, 如果值等于自己的 random_val, 才可以删除K // 避免线程1醒来后, 已经超时的情况, 另一个线程2已经SET了锁, 线程1误删Key的情况
- 释放前为什么要先get, 考虑如下情况, A获取到了锁, 但A处理过程耗时很长(A的锁超时了,但A还在正常运行), A的锁过期并被清理, 线程B抢占到锁, 这时候A处理完任务开始尝试 Del自己的锁…
- 问题:
SET NX PX
这种方式也有潜在的问题, 因为是在单Redis上加锁, 如果master上获取了锁, 但slave还没来得及同步锁, master就宕机了, slave成为新master, 这时候其他Client是可以抢到锁的;
➤ 方案2: // 不推荐
- 加锁:
SETNX k <timestamp + timeout>
, 成功则获得锁; - 超时: 加速的指令可以看到K没有设置过期, 如果获得锁的Client挂掉或者休眠, 需要其他线程来删除K, 具体做法: 其他没抢到锁的客户端, Get K发现已经超时, 则发起
GETSET k <timestamp + timeout>
,GETSET
会返回K之前的值(上个K的过期时间), 考虑多个线程同时去GETSET
的情况:GETSET
返回的时间比当前时间更小, 获取锁成功;- 如果已经有另个线程
GETSET
了Key,GETSET
返回的时间比当前时间更大, 获取锁失败, 继续等待;
- 放锁: 删除Key
- 问题: SETNX 获得锁的线程A因为其他原因阻塞很久, 直到锁已经超时并被其他客户端 GETSET掉, 这时候线程A恢复并del了k
➤ RedLock方案:
- RedLock为了解决什么: 上面提到的分布式锁是基于单Redis实例, RedLock是在多Redis实例上实现的分布式锁(更可靠)
- RedLock还可保证当大多数Redis节点可用, 就可以正常加锁放锁
- 加锁(无锁状态下抢占到锁): 客户端向所有Redis 都发起
SET k client_random_val NX PX 3000
, 这个例子里锁的总超时是3秒, 假设这一阶段消耗了x秒, 那么锁的实际可用时间=3-x
, 如果大多数(N/2 + 1)实例都返回成功, 并且锁的有效时间大于0, 才算是获取锁成功; - 测锁(尝试去加锁,有没有其他Client已经获取了锁): @todo
- 加锁失败: 如果无法在多数实例上获取到锁, 则加锁失败, Client应向已经成功的实例发起
DEL k
, 为了防止过多重试, 客户端获取锁失败需要稍等一会才能再次发起锁请求; - 解锁: 向所有Redis发起 DEL …
- 问题: 假设5个实例, Client从3个实例都取得了锁, 但这时候某个实例重启了且丢失了锁, 解决方法是:
- redis开启fsync, 同步到磁盘, 这样重启的时候可以从磁盘备份恢复数据(但一般情况下锁超时时间远小于重启耗时)
- Client尝试抢锁, 如果发现有实例宕机, 则等待一段时间(大于锁的TTL)
➤ @ref: