关于Redis分布式锁对思考 - 图1
    分布式锁是在分布式环境下对共享资源保护对一种手段,通过redis,我们可以实现分布式锁,但是其中挺多细节需要我们关注。

    如果要实现分布式锁,最简单对思路是我们通过 set get del指令:

    1. if(get lock == nil ){#1
    2. set lock foobar#2
    3. ...dosomething#3
    4. del lock#4
    5. }

    上面的方法,很明显对问题 get 后 set 不是原子操作,两个线程同时操作时可能在第一步判断时通过走到第二步,等于这个资源没有被保护好,所以这样加锁对方式是错误的。
    对于上面这种情况我们有一个解决办法 通过setnx,setnx的意思是不存在这个key则设置,否则操作失败。这个指令保证了get和set的原子性。基于setnx的操作如下:

    1. if(setnx lock foobar){#1
    2. ...dosomething#2
    3. del lock#3
    4. }

    但是这种办法就天衣无缝了吗? 显然不是。我们假想一下,如果上述过程进行到#2,但是机器宕机或者网络中断或者其他原因无法与redis通信并且正确执行#3。那么这个时候可能会导致这个锁一直得不到释放,而其他请求一直无法获取到这个资源造成死锁。生产环境这种情况绝对是不允许的。那么有什么更好的解决办法呢?
    如果我们在setnx的时候能加个超时时间就好了,到了时间,不管有没有del操作,这个锁都会被释放掉,这样就能够解决死锁这一情况。redis提供了expire指令可以设置key的失效时间。改造后如下:

    1. if(setnx lock foobar){#1
    2. expire lock 3000#2
    3. ...dosomething#3
    4. del lock#4
    5. }

    很明显这个也存在和上面一样的问题,因为setnx和expire不是原子操作 ,那么#2还没执行到可能就宕机,网络中断等,那么就退化成上面一样的情况了。幸运的是redis对#1#2两个步骤也提供了原子操作的指令。
    SET key value NX PX 30000
    这样#1#2就合并成了一步:

    1. if(set lock foobar nx px 3000){#1
    2. ...dosomething#2
    3. del lock#3
    4. }

    感觉上已经比之前好了很多,但是这种方式仍然存在问题。因为失效时间是个很尴尬的事,如果客户端A在#2执行时间太长了,而key已经失效了,这个时候线程2就又能成功获取到锁,而此时客户端A再执行#3就会把线程B刚加的锁给释放掉,这个时候如果再来一个客户端C又能获取到锁,而后客户端B又可能将客户端C的锁给del掉,导致了一种错乱,同时锁也是不可用的。正对这种问题我们可以尝试在value中做点手脚。如果每次我们设置的value都是一个随机值,这个值由获取锁的线程生成,而在del key之前 我们获取这个值并与当前线程对比,如果相等,则说明这个锁是当前线程持有的,则进行del操作,如果不相等则可能是其他线程的锁,则不进行del,这样就不会误删其他线程的锁了:

    1. random_value=random()
    2. if(setnx lock random_value nx px 3000){#1
    3. ...dosomething#2
    4. if(get lock == random_value){#3
    5. del lock#4
    6. }
    7. }

    上面的这个方法仍然存在缺陷,可以想象下下面这种情况:

    • 客户端1获取锁成功。
    • 客户端1访问共享资源。
    • 客户端1为了释放锁,先执行’GET’操作获取随机字符串的值。
    • 客户端1判断随机字符串的值,与预期的值相等。
    • 客户端1由于某个原因阻塞住了很长时间。
    • 过期时间到了,锁自动释放了。
    • 客户端2获取到了对应同一个资源的锁。
    • 客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。

    这种情况究其根本是 get 和 del 没有保持原子性,但是这次就没有那么幸运了,redis没有这样的原始指令给我们提供,但是我们可以通过lua脚本保持起原子性。

    1. if redis.call("get",KEYS[1]) == ARGV[1] then
    2. return redis.call("del",KEYS[1])
    3. else
    4. return 0
    5. end

    到此,redis单机版分布式锁的问题我们基本上就解决了。注意,我说的是单机版,对与集群情况下如果在master 加锁完成后 还没同步到slave节点,这个时候master突然宕机,导致slave变成了master节点,这个时候就会出现锁丢失的情况。这是个相当复杂的问题,这里就不做过多讨论了,有兴趣的同学可以参考 《基于Redis的分布式锁到底安全吗?》