在项目的开发过程中,经常会有以下几种场景:
- 用户下单,由于点击速度过快,或者页面卡住导致用户又点击了一次,这个时候后台就给用户生成了两笔订单。
- 消息队列的消费方由于网络抖动导致的超时,或者ack失败,导致消息重发,导致了计算结果重复或者出错。
- 某些需求需要限制n分钟某个用户只能操作一次。
类似这样的场景还有很多很多,每次面对这样的场景的时候,大部分同学都会说,“那我加个锁吧”,然后,你就会发现大部分的实现方式如下:

我们来看看上面的实现有没有问题。
假设在操作del命令的时候进行了网络抖动导致命令操作失败,这个锁就成了死锁,显然是不能接受的。
那我们改进一下,我们将锁的值设置为锁的有效时间
,获取锁失败之后,我们再get一下,判断一下时间是否过期,如果过期了再删除,重新尝试获取锁。
如果没有过期,说明获取锁失败。这样是不是就没问题了?来看一个场景
假设A、B同时获取锁,发现锁被占用,A、B先后get,发现锁过期,A执行del,然后进行了setnx操作,获取锁成功。此时B执行del操作,然后也进行了setnx操作,获取锁成功,问题就来了,A、B同时获取到了锁,如下图:

所以,正确的做法应该是使用getset来替换del,这个命令是原子,如下图

使用getset方法时,如果遇到上面的情况,A执行getset发现过期,执行del。B执行getset,因为getset是原子的,获取到的将是A执行之后的值,此时值将不是过期的,可以有效的避免这个问题。
同时,redis在2.6.12版本后,提供了更强大的set命令

所以我们的加锁过程就变得比较简单,redis官方有详细的介绍,具体如下

这样,即使因为网络抖动,del操作失败,还是有过期时间来保证不会造成死锁。当然,del释放锁操作我们还是要注意两点:1、加锁的value使用线程独有的值;2、使用lua脚本进行del。避免被别的线程将锁释放,造成锁失效的问题。
到这,是不是我们关于redis锁的问题就解决完了?还没有,目前来说,我们都没有关注到redis服务本身上,我们接着往下看。
假设我们的redis是单机,好吧,单机就是个问题,如果redis服务挂了呢,这个锁的实现就出现了问题
。
好,那来看看redis主从模式有没有问题,使用主从模式就能避免redis主服务挂了之后造成的单点故障。但是,仔细看redis官方文档

注意红圈圈住的部分,使用redis主从模式并不可靠,因为redis主从的复制是异步的。官方文档传送门好的,接下来,我们看看redis主从复制的实现方式,为什么会造成这种方式实现锁不可靠。主从复制官方文档传送门

从文档中我们看到,redis主从同步默认使用异步模式,如下图

- ClientA请求redis master,redis master操作后返回成功,因为异步同步到slave,此时还没有将数据同步到redis slave。
- 在同步到redis slave之前,redis master挂掉了,redis slave中并没有刚刚Client A写入的数据。
- 此时,redis slave切换成master,Client B请求新的master也就是原来的redis slave,成功。
好吧,这个锁服务还是不可靠。(具体redis主从复制的细节可以看redis哨兵及集群相关的官方文档
)正因为这个原因,redis的做着提出了RedLock的实现方式,来提高redis锁的可靠性,接下来一起看看什么是RedLock。
假设有多个redis master节点,这些节点是完全独立的
- clientA获取当前的系统时间(毫秒数)。
- 使用同一个语句对所有的redis实例进行加锁操作,我们对加锁操作设置超时时间,这个时间要远远小于锁的失效时间。
- clientA计算加锁消耗了多长时间(当前时间减去第一步获取的时间),只有当在大多数redis上操作成功,并且获取锁的时间小于锁的失效时间,我们认为获取锁成功。
- 如果锁获取成功,则锁的有效时间为设置的时间减去加锁消耗的时间。
- 如果锁没有获取成功,则要在所有的节点上操作解锁。
如下图:

我们看到,如果要使用RedLock,需要额外的部署多套redis服务,然后同时在这些服务上去获取锁,然后跟进获取锁的结果,来判断是否获取锁成功。是不是使用RedLock就解决了redis锁不可靠的问题呢?并不是,接着往下看。Martin Kleppmann这个人,针对RedLock写了一篇文章(传送门),指出了RedLock存在的问题:
- 如果应用发生了长时间的GC,这个GC时间超过了锁的有效时间,这个时候GC完成,接着去执行业务,但是此时,锁其实已经失效了。
- 因为RedLock算法依赖于机器的时间,如果机器的时间发生了跳跃(比如人为的进行了修改),锁也将变得不可靠。
- 同样的,长时间的IO等耗时操作,也会造成问题。

当然,redis作者针对Martin Kleppmann的文章也进行了反驳(传送门)。
说到这里,不得不提一下CAP原则,CAP指的是Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),是说分布式系统只能同时满足这三个原则中的两个,Redis是基于AP模型的。所以如果对一致性有严格要求的系统,不能基于redis来使用redis锁。当然,如果对一致性要求不高,但是对高可用要求比较高的系统,可以选用redis,最最重要的,一定是根据自己的业务来选择合适的技术。脱离业务的设计都是耍流氓。
发表回复