Redis分布式锁

基于单点Redis的分布式锁

获取锁

SET resource_name my_random_value NX PX 30000
  • my_random_value是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。
  • NX表示只有当resource_name对应的key值不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。
  • PX 30000表示这个锁有一个30秒的自动过期时间。当然,这里30秒只是一个例子,客户端可以选择合适的过期时间。

释放锁

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

这里的KEYS[1],ARGV[1]就是前面创建锁时的key和value

  • 锁必须设置一个过期时间  

如果没有设置过期时间当一个客户端获取锁成功之后,假如它崩溃了,或者由于发生了网络分割导致无法与redis通信了,那么它就会一直持有这个锁,而其他客户端就永远无法获取到锁了  

  • 获取锁必须是一个原子操作

如果将这个命令实现为两个redis命令

SETNX resource_name my_random_value
EXPIRE resource_name 30

如果执行完第一个命令后崩溃了,那么就没办法再执行第二个命令了,导致一直持有这个锁

  • 锁的值必须为一个随机字符串

这个随机字符串可以保证客户端释放的锁是属于自已的,假如锁的值是一个固定值,那么就有可能发生如下情况

  • 客户端1获取锁成功
  • 客户端2在某个操作上阻塞很长时间
  • 过期时间到,锁被释放
  • 客户端2获取到了对应的同一个资源的锁
  • 客户端1从阻塞中恢复过来,释放了客户端2的锁

  • 释放锁操作必须使用Lua脚本来实现  

锁释放包含三步操作:GET,判断,删除 ,这三步必须是原子操作,否则就会发生执行序列的问题    

  • 客户端1获取锁成功
  • 客户端1访问共享资源
  • 客户端1为了释放锁,先执行GET操作获取锁
  • 然后判断随机字符串的值,与预期相等
  • 客户端1阻塞
  • 过期时间到了,锁自动过期
  • 客户端2获取到了同一个资源的锁
  • 客户端1从阻塞中恢复过来,执行DEL操作,释放了客户端2持有的锁


基于单点redis节点的分布式锁就介绍完毕了,但是却有一个很致命的问题就是单点redis宕机,那么所有客户端就无法来获取锁了,虽然redis有主从复制,但是确实异步的,这可能在failover过程中丧失锁的安全性

  • 客户端1从Master获取到了锁
  • Master宕机,存储的锁还没有来得及同步到slave上
  • slave升级为Master
  • 客户端2从新的Master获取到了对应统一资源的锁

于是客户端1和2都持有了同一个资源的锁

分布式锁ReadLock

由于基于单点Redis节点分布式锁在failover的时候会产生解决补了的安全性问题,因为Redlock的产生就是为了解决这个问题,它基于N个完全独立的Redis节点

运行Redlock算法的客户端一次执行下面的各个步骤,来完成获取锁的操作

  • 获取当前的时间(毫秒)
  • 按顺序依次想N个redis节点执行获取锁的操作,这个操作和单点redis分布式锁获取锁过程相同。并且为了在某个redis节点不可用时可以继续运行,必须设置一个获取锁的操作的超时时间,这个超时时间远比锁的有效时间短。客户端再向某个Redis节点获取锁失败以后,应立即尝试下一个redis节点。
  • 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第一步的时间。如果客户端从大多数Redis节点(>=N/2+1)成功获取到了锁总共消耗的时间没有超过锁的有效时间,那么这是客户端才认为最终获取锁成功;否则认为最终获取锁失败
  • 如果最终锁获取成功了,那么这个锁有效器应该重新计算,它等于最初锁的有效期减去第3步计算出来的获取锁消耗的时间
  • 如果最终锁获取失败了,那么客户端应该应该立即想所有redis节点发起释放锁的操作,无论是否获取锁成功


redlock很好的解决了由于单点redis分布式锁的failover问题,看似已经完美但是也有些问题,当节点发生崩溃重启,还是会对锁可能会产生影响,具体和redis的数据持久化程度有关

假设有五个redis节点:A、B、C、D、E。获取锁时发生了如下的时间

  • 客户端去成功锁住了A、B、C,获取锁成功,但是DE没有锁住
  • 节点C崩溃重启,但客户端1在C上假的锁没有持久化下来丢失了
  • 节点C重启后,客户端锁住了C、D、E,获取锁成功

客户端1和客户端2同时获取到了锁,导致了访问同一资源