一、概述
首先引入分布式锁指的是应用程序引入,不是Redis本身引入,Redis作为中间件可以作为分布式锁的一个典型实现方案,同时也有一些其他的实现方案。分布式锁指的是一个/组程序,使用Redis实现的话就是通过添加一个特殊的Key-Value,在其他服务器想要进行操作时(例如买票操作),会先检查这个Key-Value是否存在,如果存在就会加锁失败,而Redis的Set NX操作正好满足,它只有在不存在才能设置成功,解锁命令则可以使用del命令删除对应的Key-Value即可。但是这些都是在理想的情况下,在实际场景中,可能出现的情况则复杂多样。
二、可能存在的问题及参考方案
1.服务程序在加锁后还未完成操作就崩溃了
这个问题在单线程中没什么关系,但是在分布式系统中就会影响其他的服务器程序获取锁,参考方案就是加锁时引入超时时间,即Set NX PX命令,保证即使一个程序出现突发情况也不会影响其他程序的使用,值得注意的是这个操作必须写成一步,不能先加锁,再给锁设置超时时间,因为Redis事务的原子性比较弱(现在可能直接说没有原子性)。
2.锁通用导致误解锁
设想一下这个场景,A先加锁,B执行解锁,原本它们是各做各的,但是在它们之间有个C在尝试加锁,B的解锁操作就因为不知道是C加的锁而直接将C刚加上的锁给误删了。
参考解决方法就是给每个服务器程序都进行编号,如使用UUID或者进程号+线程号等方式,在设置锁时就可以将Key设为针对哪个资源进行加锁,Value则使用服务器的唯一编号。
3.解锁操作无原子性导致的误解锁
在解锁时无法向加锁一样使用一条指令就可以完成,需要1.通过Get获取锁。2.执行Del操作。这么问题来了,在1和2之间如果有一个新的服务器程序对这个资源进行了加锁,2操作就会导致该服务器新加的锁无了。这个问题的主要原因就是解锁操作无原子性。
参考解决方法是引入Lua脚本来实现Get和Del操作,Lua脚本的好处是它可以在一个脚本中执行多条指令,但是Redis本身将它视为一个操作,这样就保证了原子性。
4.超时时间续约问题
因为超时时间是提前设置好的,太长会影响其他程序,太短又会导致操作还没完成锁就过期了,这样的话就很麻烦。
参考解决方法是在每次加锁成功后就唤醒一个守护线程,一般称为“看门狗”线程,该线程会持续检查剩余超时时间,比如设置超时时间1000ms,让该线程在超时时间剩余300ms时进行超时时间重置,当客户端完成操作或者奔溃后就停止续约线程。
5.集群模式下的情况
单点模式下如果Redis挂了,那就是挂了,没什么好方法。如果是集群模式下,一个主节点完成加锁操作后,在还没来得及向从节点同步时就挂了,那么当从节点称为新的主节点后,因为没有加锁的命令,就会导致重复加锁操作。
参考解决方法是引入Redlock算法,该算法会向所有的主节点发送加锁指令,当有超过半数节点都认为加锁成功了,那即使有部分节点加锁失败,也认为是加锁成功了,当解锁时向所有节点发送Del命令,即使该节点没有加锁。