Redis锁在面试中是Redis绕不开的话题,关于Redis锁,网上很多文章,大多都是这个方案:
1、单机Redis
2、RedLock
3、Redission分布式锁
本文基于这三个点,延伸出几个问题,同时介绍下Martin和RedLock实现作者Salvatore的论点。
当然分布式锁并不只限于这两种,还有基于ZooKeeper的分布式锁的实现、Chubby的分布式锁、Mysql分布式锁、基于 Etcd、Hazelcast 分布式锁的等等,其中关于基于ZooKeeper的分布式锁的内容将放在下一次进行讲解。
一 单机Redis
单节点的分布式锁,网上已经有很多介绍了,很容易找到这样一句代码:
SET key value NX PX30000
如果上面的指令成功执行,那也就意味着获得了锁,此时可以对共享资源进行操作,如果指令执行失败,则意味着获取锁失败。
虽然这个指令非常简单,其中的几个问题还是值得思考的
1 px有必要吗。为什么?
首先想如果没有PX,会发生什么情况,假设一个进程A获得了这把锁,但是这个进程挂了,此时无法释放这把锁,那是不是这个进程就会一直持有这把锁呢?这样就导致其他进程无法获得共享资源。
所以需要这个PX对锁设置一个过期时间,防止其他进程无法获得锁。
思考另外一个问题,由于我们的进程运行在机器上,而机器运行的状态我们无法确定。进程A此时获得了锁,此时可能因为GC停顿导致了程序的卡顿,亦或是调用其他接口因为拥塞网络问题导致请求变慢,也许其他进程发送SIGSTOP信号,总之此时因为一些其他情况造成进程暂停,又有一个进程B获得了锁,如果此时进程A的过期时间到达,使用DEL删除这个记录,那么就会把进程B所拿到的锁进行删除,这样就会产生一个不安全的事故,所以PX的设置还是有必要的。
(超时后只有对key执行DEL命令或者SET命令或者GETSET时才会清除)
进程A的Stop-The-World GC时间结束,发现锁过期,反手DEL删除了锁,此时也就是删除了进程B所持有的锁。
那么如何解决这个问题呢,Martin给了一个解决方案,采用一个单调递增的数字来确定延迟到来的请求,其实也就相当于版本号,如果接收到低版本的请求,便拒绝就好了。
但是这样还是会有一个问题:如果因为两个客户端都发生了GC,但是版本号到达的顺序是正确的,那么是不是又有问题了?
2 过期时间设置多少合适,超时之后,共享资源是不是失去保护了呢?
Redis锁的过期时间设置,也是可以思考的点,例如Redis锁过期了,但是业务逻辑却没有执行完成,那该怎么办呢?
如果把Redis锁的过期时间再设置长一点怎么样?这样还是有问题,如果此时业务逻辑执行的非常快,过长的Redis锁过期时间的设置反而会降低效率。
如果此时客户端因为一些原因导致锁过期了,那么这里的客户端访问接下来的共享资源还安全吗?答案是否定的,共享资源此时已经失去了保护。
3 随机字符串有必要吗,为什么?
首先随机字符串是必要的,这保证了客户端释放所必须是自己所持有的锁。我们可以假设一个场景,如果这个字符串是一个固定不变的字符串,那么还是会产生类似问题一的结果:A所属的锁被B释放。因此这个随机字符串还是有必要的。
4 指令可以拆成两条指令实现吗,缺点有什么?
加锁的指令确实可以通过两条指令进行实现,例如:
SETNXkey valueEXPIRE key 30
这样做弊端很明显,就是没有原子操作,另外还有一个需要说的点,对于SETNX指令,在Redis官方文档上SET指令介绍有这样一句话:
大意是SET指令可以替换SETNX、SETEX、PS