水煮Redisson(十七)-关于SETNX锁的思考

87 阅读3分钟

介绍

Redis原始指令SETNX,因为其独特的设计,同时满足独占性和超时机制,因此很多从业者利用这些特性来实现分布式锁,但是有很多弊端。
在前文Redisson系列-分布式锁简介中有做过简单介绍。

指令

指令格式:SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]

  • EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
  • PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
  • NX : 只在键不存在时, 才对键进行设置操作。
  • XX : 只在键已经存在时, 才对键进行设置操作。

怎么用

在技术领域,有这么一句话:没有不合适的技术,只有不合适的应用场合。确实,SETNX最初设计出来,或许本就不是为了应用于分布式锁。细看官方文档对其的描述,如此精妙的指令设计,在合适的场景下堪比神器。这里举一个不太贴切的例子,用来预防缓存击穿。在指定key过期后,如果此时进来了大量的同类请求,那么这些请求都会去数据库加载数据,造成数据库系统指标不稳定。这时就可以采用NX语义进行阻塞请求,只允许一个线程通过,重置缓存,其他请求则快速失败,直接返回。

蹩脚的锁实现

重入

相同客户端线程如何多次持有锁?
客户端线程A通过NX语义拿到锁之后,并没有标识出是哪个线程ID持有了锁。如果在锁代码块中,还需要再次持有,这个指令就不能满足要求了,毕竟只在键不存在时, 才对键进行设置操作。
解决方法:在value中存储当前持有锁的线程id和持有次数,重入时,对线程id进行判断,如果已经持有,则不需要等待锁释放,持有次数加一。

续命

客户端线程拿到锁之后,在有效时间内未执行完成,如何为其延续锁持有时间?
在Redisson中,客户端线程请求锁时,如果leaseTime设置为-1,在持有锁之后会启动一个定时任务,每隔三分之一的LeaseTime,延长其过期时间。为什么要有这个机制呢?那么考虑这样的情况:拿到锁以后,锁代码块需要执行10秒,但是锁超时时间只有5秒,那么剩下的执行时间,就裸奔了,会导致无法预测的数据异常。
解决方法:开启定时任务,定时刷新超时时间。

公平

setNX默认是非公平的,如何实现公平锁?
为什么默认是非公平的?其实从NX的语义可以看出,对于一个key资源,所有的客户端线程都是在并发争抢,没有FIFO的排队机制。如果锁已经被其他客户度持有,也没有一个重试的机制,客户端只能放弃,或者通过业务代码实现循环尝试。公平锁需要的排序、等待、通知、重试一系列特性,只是通过这样单个指令是无法满足的。

脏锁

本来setnx是独占的,但是在业务使用过程中,锁中的代码执行时间过长,导致超时被Redis回收;如果此时其他客户端线程持有了锁,就会造成锁共享,这时多个客户端线程都可以通过delete指令进行解锁,业务代码的安全性将毫无保障。