Redis知识点总结(五)——Redis实现分布式锁
- setnx
- setnx + expire
- setnx + expire + lua脚本
- set nx ex
- set nx ex + 随机值
- set nx ex + 随机值 + lua脚本
- set ex nx + 随机值 + lua脚本 + 锁续期
- Redisson
- RedLock
在Redis的众多应用场景中,分布式锁是Redis比较重要的一个应用场景,今天我们就来了解一下如何用Redis实现一个分布式锁。
setnx
Redis提供了一个setnx命令,它的作用是在给缓存设置值时,判断key是否存在,如果key已经存在,则不执行操作,只有当缓存中不存在指定key时,才设置该缓存。
使用setnx命令,就可以实现一个简单的分布式锁。我们指定一个特定的值作为setnx命令的key,然后当一个线程要获取分布式锁时,就要通过setnx命令在redis中设置该key,如果设置成功,代表获取分布式锁成功,如果设置失败,表示获取分布式锁失败,那么当前线程就要通过自旋加sleep的机制等待别的线程释放锁。
一个线程通过setnx命令获取到分布式锁后,通过del命令释放锁。释放锁之后,其他线程就可以再次尝试获取锁。
但是这种方案有一个严重的问题,就是当一个线程获取到锁后,在处理业务逻辑时发生了异常,或者直接宕机了,没能执行del命令释放锁,那么其他线程就会一直获取不到锁。
setnx + expire
单靠setnx命令实现的分布式锁,存在锁得不到释放的风险。因此,我们可以引入expire命令,给key设置一个过期时间。当一个线程执行setnx命令成功后,就通过expire命令给该key设置一个过期时间。假如该线程执行业务逻辑出错,没能执行del命令,那么等该key的过期时间到后,该key也会被失效清理,锁最终还是能得到释放的。
这样问题就解决了吗?
显然是没有的,因为setnx和expire是两条命令,是非原子性的,如果setnx执行成功,expire命令执行失败,那么锁还是得不到释放。
setnx + expire + lua脚本
setnx命令与expire命令的非原子性问题,可以通过lua脚本去解决,把setnx和expire两条命令包在一个lua脚本里面,就可以保证这两条命令同时成功或者同时失败,这就就具备了原子性。
除了使用lua脚本,Redis还可以通过一条命令实现setnx加expire两条命令的功能。
set nx ex
我们可以通过以下命令实现setnx命令加expire命令的功能,并且可以保证原子性:
set key value nx ex {过期时间}
这样,分布式锁得不到释放的问题就彻底解决了。
但是,这样就没问题了吗?显然不是的,还会出现以下这种情况:
线程1获取分布式锁成功,但是业务处理时间过长,锁早已过期。而线程2随后获取分布式锁成功,进行业务处理。然后这时候线程1处理完了,于是执行del命令释放锁,而此时它释放的是线程2加的锁,并且线程2还没有处理完。线程2的锁被线程1释放后,如果线程3到来,也获取了分布式锁,那么等线程2处理完毕,又会释放掉线程3获取的锁,以此类推,这个分布式锁就失效了。
set nx ex + 随机值
锁错误释放的问题,可以通过给value指定一个随机值来解决,实现方案是这样:
执行set命令时生成一个随机值作为value值,并且当前线程要保存该随机值。等到当前线程处理完业务逻辑要准备释放锁时,先拿着自己保存的随机值跟redis中的value比对一下,如果相等,才执行del命令释放锁,否则就不能执行del命令。
这样看似可以解决锁误删的问题,一般情况下也确实如此,但是在高并发场景下,有可能会出现以下这种情况:
线程1执行完业务逻辑后,判断自己的value随机值与redis中的value相等,正准备释放锁,此时正好锁过期了,然后线程2又刚好获取到了锁,然后线程1才执行del命令,那么线程2的锁又被误释放了。
这是低概率事件,非高并发场景其实不用考虑这个问题,但是在高并发场景,还是有可能出现的。问题的原因是if判断与del命令是两个操作,非原子性。
set nx ex + 随机值 + lua脚本
if判断与del命令的非原子性问题,可以通过lua脚本解决,把if判断与del命令用lua脚本包起来,就可以彻底解决锁误释放的问题。
现在获取锁与释放锁的操作都是原子性的,我们的分布式锁也逐步变得完善,但是还有一个问题,那就是锁过期,也就是业务处理还未结束,锁就过期的这种情况。
我们可以给锁设置一个较长的过期时间,但是我们无法百分之一百保证所有的业务处理都在锁过期前得到释放,如果在某种特殊情况下处理时间比平常的要长,锁还是过期了,那么还是有问题的。
set ex nx + 随机值 + lua脚本 + 锁续期
锁过期的解决办法,就是开一个“看门狗”线程,进行锁续期,这个线程会定时地不断的检测主线程释放处理完毕并且释放了锁,如果主线程还没有处理完毕,那么这个“看门狗”线程就会执行expire命令进行锁续期。
此时,我们的分布式锁就完美了。但是,这样太复杂了吧?我们为了实现一个分布式锁,居然要处理这么多问题,使得原本简单的代码变得非常复杂,那有没有一个开源框架,可以帮我们实现这一切,我们只需要简单的调用一下就完事了呢?
当然是有的,那就是使用Redisson的分布式锁。
Redisson
Redisson是一个用Java语言实现的用于操作Redis的客户端工具包,比起原生的Jedis工具包,Redisson是更好用,功能更强大的。
Redisson就自带了分布式锁的实现,把我们上面说到的获取锁的原子性、释放锁的原子性、利用看门狗线程进行锁续期等一系列的操作都包装起来,对外暴露了简单的获取锁和释放锁的方法,我们无需手动编码实现这复杂的获取锁与释放锁的逻辑,只要使用Redisson提供的分布式锁的方法,就可以实现分布式锁的功能。
使用Redisson,我们就可以轻松简单的实现分布式锁的功能,但是如果Redis是单节点的话,Redis宕机了,那么所有线程都无法获取到锁了。
解决Redis单节点的问题,自然是上多个Redis节点。但是上了多个Redis节点之后,分布式锁又怎么实现呢?
RedLock
Redis的作者提出了一个多redis节点的分布式锁实现方案,那就是RedLock。
首先,我们要启5个redis节点,但是这5个节点是独立的节点,互相之间没有主从关系,也没有组成集群。
当一个线程加锁时,需要按顺序的访问这五个节点,还是利用set nx ex或set nx px命令进行加锁。
当一个线程在至少3个以上的redis节点成功执行了set命令,那么就表示获取锁成功,此时该线程要计算锁的有效期,需要用锁的有效期减去加锁耗费的时间,如果还有剩余时间,表示获取锁成功,如果没有剩余时间了,那么表示获取锁失败。
如果一个线程获取锁失败,要按相反的顺序向所有redis节点执行del命令释放锁。
这个RedLock的功能,同样可以通过Redisson实现,只需要调用Redisson提供的简单的API方法即可,无需我们编写复杂的RedLock代码。