Redis分布式锁

分布式锁需要具备的三个基础属性

  1. 安全属性:互斥.在任何时间,最多只有一个客户端可以持有锁
  2. 活跃属性A:不能够产生死锁.最终总是可能获取到锁,即使持有锁的其它客户端宕机了
  3. 活跃属性B:错误容忍.只要大多数的Redis节点处于正常运行状态,客户端可以获取和释放锁

单实例上的实现

1
SET resource_name my_random_value NX PX 30000

获取锁:使用一个key,和一个唯一标识来设置键,并且只有当key不存在的时候(NX)才能执行成功,这个唯一标识my_random_value必须在所有的客户端上都是唯一的值,以保证释放锁的客户端是获取锁的那个客户端.

释放锁:当释放锁时,使用Lua脚本加上这个唯一标识.告诉Redis只有当key存在并且值是这个唯一标识时,再释放锁.一个Lua脚本的例子如下:

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

如果没有使用唯一标识,那么有可能当一个客户端获取到锁,执行一段业务逻辑,但还没执行完锁已经过期了,此时锁被其他客户端获取到,如果原客户端没有使用唯一标识去删除锁,那么会释放了别的客户端获取到的锁.

红锁算法

由于单实例可能存在单点故障,为了保证高可用性,需要使用多个独立的Redis实例来实现分布式锁.每个实例上获取分布式锁的模式和单实例的模式是一样的,只不过在这个基础上加了其他一些步骤.

假如有N个独立的Redis实例,获取锁的步骤如下:

  1. 获取当前时间
  2. 尝试使用同样的key和唯一标识在所有的N个实例上顺序的获取锁.当在每一个实例上获取锁时,使用一个相对于总体的锁释放时间来得很小的超时时间,例如锁的过期时间是10秒,那么超时时间可以是5-50毫秒,一旦获取锁超时,那么就赶紧去下一个实例获取锁.这么做是防止长时间在某个Redis实例上发生长时间的阻塞,如果一个实例不可用,我们应该尽可能的去下一个实例上获取锁.
  3. 通过当前时间减去步骤1锁获取到的时间,客户端可以计算为了获取锁使用了多少时间,如果能在半数以上的Redis实例上获取到锁,并且获取锁所使用的时间小于锁的有效时间,那么就认为获取到了锁
  4. 如果锁获取到了,那么锁的真实有效时间被认为是锁的过期时间减去获取锁所使用的时间
  5. 如果客户端未能获取到锁,那么应该尝试对所有的实例进行锁的释放,即使在某个实例上并没有获取到锁

开启持久化配置

每个实例需要使用持久性配置fsync=always,否则某个实例宕机后恢复,那么有可能导致一个分布式锁被两个客户端获取到.

延长锁时间

如果在锁过期之前,客户端还未能处理完逻辑,那么可能需要考虑去在获取到Redis锁的实例上去延长锁的时间,具体方式可以通过Lua脚本来完成,如果key和value都是之前获取锁的值,那么就延长时间.如果没有在半数以上的实例上延长成功,那么就视为不成功,更具体的步骤类似于红锁算法中获取锁的步骤.

参考资料


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!