Redis如何实现分布式锁


实现分布式锁首先要保证这把锁不能使某个客户端本地的锁,不然其他客户端拿不到这把锁。

分布式锁可以和单机上的加锁释放锁操作一致,加锁时候同样需要判断锁变量的值,根据锁变量的值来判断能否加锁成功;释放锁的时候将锁变量的值设置为0,表示客户端不再拥有锁。和单机系统不同的是,分布式锁需要一个共享存储系统来维护,只有这样,多个客户端才能通过访问共享存储系统来访问锁变量。这样一来加锁、释放锁就变成了读取、判断和设置共享存储系统中的锁变量值。

从而我们在实现分布式锁的时候就有两个要求

1、分布式锁的加锁和释放锁的过程涉及到多个操作。所以在实现分布式锁的时候我们要保证这些锁操作的原子性。
2、共享存储系统保存了锁变量,如果共享存储系统发生故障或者宕机,那么客户端就无法进行锁操作了。从而,在实现分布式锁时,我们要保证共享存储系统的可靠性,进而保证锁的可靠性。

单个Redis节点实现分布式锁

Redis使用键值对进行数据保存,我们可以给锁变量赋予一个变量名,也是存储数据中的key,而锁变量就是键值对中的value,这样一来Redis就可以保存锁变量了。客户端也是通过Redis命令操作来实现锁操作的。

加锁操作分析,当有客户端A、B同时请求加锁,因为Redis底层是单线程处理请求的,所以即使A、B两个线程同时把加锁请求发送给Redis,Redis也会串行处理他们的请求。

例如A首先进行处理,读lock_key的值,发现lock_key的值为0,Redis就把lock_key的值设置为1,表示加锁成功了,当Redis处理客户端B的请求时,发现lock_key的值已经为1,所以就返回加锁失败的信息。

加锁包含了三个操作,读取锁变量,判断锁变量,设置锁变量为1。如何保证原子性呢?一个是SETNX、一个是使用lua脚本。

执行SETNX key value时,发现key不存在就会创建key并设置为value,如果存在就不会采取任何操作,释放锁的时候会DEL key,这样就不用担心锁变量被删除后,其他客户端无法请求加锁了。

但是 SETNX 和 DEL两个命令的组合使用存在两个风险,一个是当SETNX后,操作数据时异常,没有完成DEL操作,锁就不会被释放,其他客户端就无法拿到锁。这个问题的解决方法就是给锁变量设置一个过期时间。还有一个问题就是当A客户端SETNX加锁后,B客户端执行了一个DEL释放了锁,C客户端又SETNX,这样一来,A和C同时拥有了锁,对共享变量进行操作,这个在业务层是不能接受的。这个的解决办法就是区分不同的客户端,给不同的客户端一个唯一值当做锁变量中的value来标识不同的客户端。

释放锁就是将锁变量的值设置为0,表示没有客户端持有锁。
SET操作后面NX表示不存在就创建 EX、PX用来设置过期时间

多节点Redis实现高可靠的分布式锁

开发者提出了分布式锁算法Redlock,其基本思路就是让客户端和多个独立的Redis实例依次请求加锁,如果客户端能够和一半以上的实例成功完成加锁操作,那么我们就认为客户端成功的获得分布式锁了,否则就是加锁失败。加锁过程如下三步

1、客户端获取当前时间
2、客户端依次向N个Redis实例执行加锁操作,这里的加锁和单机节点一样,用SEX 带上NX,EX/PX选项以及客户端唯一标识,如果某个Redis实例发生故障,为了保证Redlcok算法能继续运行,我们需要给加锁操作设置一个超时时间。如果在一个实例中一只请求加锁,到超时也没成功,则和下一个Redis实例继续请求加锁,加锁操作的超时时间需要远远地小于锁的有效时间,一般也就设置为几十毫秒。
3、一旦客户端完成了和所有Redis实例的加锁操作,客户端就计算整个加锁过程的总耗时。
A 客户端只要在满足下面两个条件时才认为加锁成功。
B 客户端从超过半数(>= N/2+1)的实例上成功获取到了锁
C 客户端获取锁的总耗时小于锁的有效时间