redis分布式锁

背景

  单机情况下,对于多线程情况下的竞态资源,我们可以在代码中使用synchronized或者ReentrantLock来加锁,防止发生并发问题。但是当我们的服务进行集群部署时,对于这两种加锁方式,就会失效,它们只能在单机加锁,所以就需要与之对应的分布式锁。


要求

  1. 互斥:任何时刻只能有一个客户端获取锁。
  2. 释放死锁:不会发生死锁,即获取锁的服务崩溃,也能释放锁。
  3. 容错性:(redlock中用)只要多数redis节点(一半以上)在使用,client就可以获取和释放锁。

分布式锁

  一般采用redis的setnx原子操作来实现分布式锁。
学习一下
学习二下
学习三下
学习四下

setnx(获得锁)

  setnx 是SET if Not eXists(如果不存在,则 SET)的简写。
  完整语法:

1
2
3
4
5
# set命令模式
set key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]

# setnx命令模式
setnx key value
  1. value的值尽可能使用随机数或者线程独有的,能够识别的,为了安全的释放锁。
  2. 使用不同的redis客户端(jedis,redisTemplate)时写法会有所不同,这里是redis黑窗口命令。

  参数说明:

  1. EX:设置过期时间,时间精确到秒
  2. PX:设置过期时间,时间精确到毫秒
  3. NX:表示key不存在时才设置,否则返回null
  4. XX:表示key存在时才设置,否则返回null

使用过程:

  1. 执行setnx命令进行加锁,返回ok,返回nil则为加锁失败。
  2. 执行expire命令设置超时时间
  3. 执行业务逻辑
  4. delete命令解锁

问题

  1. 加锁与设置超时时间分步执行,若超时时间设置失败则有可能产生死锁。
  2. delete命令存在误删非当前线程持有锁的可能。
  3. 不支持阻塞等待,不可重入。
  4. 单机redis锁,存在加锁后,主从切换时锁还未同步到问题,锁会丢失。

lua脚本(释放锁)

  我们在手动解锁时,极限情况下会有删除其他线程锁的情况,因为我们的随机数比较和删除过程并不是原子操作。存在判断通过后,锁自动失效,其他线程加锁成功的情况,这是解锁会出问题。通过lua脚本原子操作,可以安全的解锁。

1
2
3
4
5
6
7
8
9
10
11
-- lua删除锁:
-- KEYS和ARGV分别是以集合方式传入的参数,对应上文的Test和uuid。
-- 如果对应的value等于传入的uuid。
if redis.call('get', KEYS[1]) == ARGV[1]
then
-- 执行删除操作
return redis.call('del', KEYS[1])
else
-- 不成功,返回0
return 0
end

setex&psetex

  setex等同于set命令在可选参数使用EX的情况,都是在NX模式下,添加了过期时间,避免死锁。psetex相对于setex采用毫秒作为超时单位。