Redis 分布式锁
基于Redis 的分布式锁
一、实现原理
1.1 基本原理
SETNX key value
当获得锁的进程处理完成业务后,再通过 del
命令将该
通常为了避免死锁,我们会为锁设置一个超时时间,在expire
命令来进行实现:
EXPIRE key seconds
这里我们将两者结合起来,并使用
Long result = jedis.setnx("lockKey", "lockValue");
if (result == 1) {
// 如果此处程序被异常终止(如直接kill -9进程),则设置超时的操作就无法进行,该锁就会出现死锁
jedis.expire("lockKey", 3);
}
上面的代码存在原子性问题,即eval()
方法来执行,并由
1.2 官方推荐
SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
这里我们主要关注以下四个参数:
- EX :设置超时时间,单位是秒;
- PX :设置超时时间,单位是毫秒;
- NX :当且仅当对应的
Key 不存在时才进行设置; - XX:当且仅当对应的
Key 存在时才进行设置。
这四个参数从
jedis.set("lockKey", "lockValue", SetParams.setParams().nx().ex(3));
此时一条命令就可以完成值和超时时间的设置,并且因为只有一条命令,因此其原子性也得到了保证。但因为引入了超时时间来避免死锁,同时也引出了其它两个问题:

- 问题一:当业务处理的时间超过过期时间后(图中进程
A ) ,由于锁已经被释放,此时其他进程就可以获得该锁(图中进程B ) ,这意味着有两个进程(A 和B )同时进入了临界区,此时分布式锁就失效了; - 问题二:如上图所示,当进程
A 业务处理完成后,此时删除的是进程B 的锁,进而导致分布式锁又一次失效,让进程B 和 进程C 同时进入了临界区。
针对问题二,我们可以在创建锁时为其指定一个唯一的标识作为UUID + 线程ID
来作为唯一标识:
String identifier = UUID.randomUUID() + ":" + Thread.currentThread().getId();
jedis.set("LockKey", identifier, SetParams.setParams().nx().ex(3));
然后在删除锁前,先将该唯一标识与锁的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这段脚本的意思是如果
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script,
Collections.singletonList("LockKey"), // keys的集合
Collections.singletonList(identifier) // args的集合
);
接着再看问题一,问题一最简单的解决方法是:你可以估计业务的最大处理时间,然后保证设置的过期时间大于最大处理时间。但是由于业务会面临各种复杂的情况,因此可能无法保证业务每一次都能在规定的过期时间内处理完成,此时可以使用延长锁时效的策略。
1.3 延长锁时效
延长锁时效的方案如下:假设锁超时时间是
以上讨论的都是单机环境下的
二、哨兵模式与分布式锁
哨兵模式是主从模式的升级版,能够在故障发生时自动进行故障切换,选举出新的主节点。但由于
- 由于主从之间的复制操作是异步的,当主节点上创建好锁后,此时从节点上的锁可能尚未创建。而如果此时主节点发生了宕机,从节点上将不会创建该分布式锁;
- 从节点晋升为主节点后,其他进程(或线程)仍然可以在该新主节点创建分布式锁,此时就存在多个进程(或线程)同时进入了临界区,分布式锁就失效了。
因此在哨兵模式下,无法避免锁失效的问题。因此想要实现高可用的分布式锁,可以采取
三、集群模式与分布式锁
3.1 RedLock 方案
想要在集群模式下实现分布式锁,
- 以毫秒为单位记录当前的时间,作为开始时间;
- 接着采用和单机版相同的方式,依次尝试在每个实例上创建锁。为了避免客户端长时间与某个故障的
Redis 节点通讯而导致阻塞,这里采用快速轮询的方式:假设创建锁时设置的超时时间为10 秒,则访问每个Redis 实例的超时时间可能在5 到50 毫秒之间,如果在这个时间内还没有建立通信,则尝试连接下一个实例; - 如果在至少
N/2+1 个实例上都成功创建了锁。并且当前时间 - 开始时间 < 锁的超时时间
,则认为已经获取了锁,锁的有效时间等于超时时间 - 花费时间
(如果考虑不同Redis 实例所在服务器的时钟漂移,则还需要减去时钟漂移) ; - 如果少于
N/2+1 个实例,则认为创建分布式锁失败,此时需要删除这些实例上已创建的锁,以便其他客户端进行创建。 - 该客户端在失败后,可以等待一个随机的时间后,再次进行重试。
以上就是
3.2 低延迟通讯
另外实现
SET key 随机数A EX 3 NX #A客户端
SET key 随机数B EX 3 NX #B客户端
此时可能客户端
因此最佳的实现就是客户端的
3.3 持久化与高可用
为了保证高可用,所有
everysec
,即每秒进程一次持久化,此时能够兼顾性能与数据安全,发生意外宕机的时,最多会丢失一秒的数据。但如果碰巧就是在这一秒的时间内进程
- 方式一:修改
Redis.conf 中appendfsync
的值为always
,即每次命令后都进行持久化,此时会降低Redis 性能,进而也会降低分布式锁的性能,但锁的互斥性得到了绝对的保证; - 方式二:一旦节点宕机了,需要等到锁的超时时间过了之后才进行重启,此时相当于原有锁自然失效(但你首先需要保证业务能在设定的超时时间内完成
) ,这种方案也称为延时重启。