Redis分布式锁
分布式应用进行逻辑处理时经常会遇到并发问题。对于单进程的并发场景,我们可以使用语言或者类库提供的锁,而对于分布式场景,我们可以使用分布式锁。
分布式锁的实现方法也有很多,Memcached分布式锁、Zookeeper分布式锁等等,当然,Redis分布式锁也是很有代表性的分布式锁的实现方式。
例子:
某个操作需要修改用户的状态,首先需要读取出用户的状态,在内存中进行修改后再把它存回去。这样的操作如果同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。
ps:原子操作指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何的context switch 线程切换。
这个时候就要使用到分布式锁来限制程序的并发执行,Redis分布式锁使用非常广泛。
如何使用
分布式锁本质上要实现的目标就是在Redis里面占一个座位,当别的进程也要来坐这个位置时,发现座位已经被占了,就只能等待或稍后再查看。
占座位(也就是加锁)一般用 setnx(set if not exists)指令,并且只允许被一个客户端占领,先来先占领,用完了,再使用del指令释放。
// 这里的冒号:就是一个普通的字符,没特别含义,它可以是任意其它字符,不要误解 > setnx lock:codehole true OK ... do something critical ... > del lock:codehole (integer) 1...
但是如果我们加锁后,一个得到锁的进程在执行过程中挂掉或者是我们忘记了释放锁,那么这块资源会被永久地锁住,造成死锁,锁永远得不到释放。
于是我们在拿到锁之后,可以给锁 expire命令 再增加一个过期时间,比如说5s,那么及时中途出现异常亦可以保证5s后锁会自动释放。
> setnx lock:codehole true OK > expire lock:codehole 5 ... do something critical ... > del lock:codehole (integer) 1
但是这样也还是有可能出现问题的。原因就在于setnx 和 expire 这两条指令是 非原子性 的指令。
我们可以设想以下的场景:
节点1中线程A执行了 setnx 指令,获得了锁但是还没有执行 expire 指令,这个时候突然节点1的机器宕机了。
这样一来,这把锁就没有了过期时间,变得永不过期,依旧会出现死锁。
为了解决这个问题,Redis在2.6.12以上的版本中增加了 set指令以及其可选参数 ,取代了setnx指令,使得setnx和expire指令可以一起执行。
> set lock:codehole true ex 5 nx OK ... do something critical ... > del lock:codehole
超时问题
Redis的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,超出了锁的超时限制,就会出现问题。
因为这个时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完之后,会将这个锁给释放掉。
这会导致一个后果:第三个线程会在第二个线程的执行过程之间拿到锁。
为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。
解决超时自动释放的一种方式。
我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁续航。
过去了29s,线程A还没有执行完毕,这个时候守护线程会执行 expire 指令。为这把锁续命20s,守护线程在29s后,每20s执行一次。
线程A执行完之后,则关闭守护线程。