Redis 分布式锁的正确编写方式

Redis 分布式锁的正确编写方式

在不同进程需要互斥地访问共享资源时,在业务中体现为多个实例需要同时访问同一个 redis 的共享资源,分布式锁是一种非常有用的技术手段,我们可以使用 redis 实现它。 (1) 为了确保这个 redis(业务里 redis 为单机版) 锁是可用的,需要满足一些条件: a.互斥性。在任意时刻,只有一个 jedis 客户端能持有锁。 b.不会发生死锁。即使有一个 jedis 客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。 c.加锁和解锁必须是同一个 jedis 客户端,客户端自己不能把别人加的锁给解了。 用 redis 来实现分布式锁最简单的方式就是在 redis 里创建一个键值,创建出来的键值一般都是有一个超时时间的(这个是 redis 自带的超时特性),所以每个锁最终都会释放(参见前文要求 b)。而当一个客户端想要释放锁时,它只需要删除这个键值即可。 锁的实现主要基于 redis 的 SETNX 命令 SETNX key value 将 key 的值设为 value ,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作。 SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。 (2)上锁使用的代码: jedis.set(String subject, String requestId, String SET\_IF\_NOT\_EXIST, String SET\_WITH\_EXPIRE\_TIME, int expireTime); 这个set()方法一共有五个形参: a.第一个为上锁使用的 key,我们使用和业务相关的参数来当锁,因为key是唯一的。 b.第二个为上锁使用的 value ,我们传的是UUID,很多可能同学不明白,有key作为锁不就够了吗,为什么还要用到 value?原因就是我们在上面讲到可靠性时,锁要满足c条件,解铃还须系铃人,通过给 value 赋值为 UUID,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。UUID 可以使用 UUID.randomUUID().toString() 方法生成。 c.第三个参数 SET\_IF\_NOT\_EXIST 是能否能上锁的判断条件,这个参数我们传的值是 NX,意思是当第一个参数 key 在 redis 中不存在时,我们对该 key 进行 set 操作,与之对应的 jedis 操作返回值为 OK;若 key 已经存在,则不做任何操作,与之对应的 jedis 操作返回值为 Null。利用这两个返回值,我们就能对从不同地方发过来的 jedis 上锁请求做出判断,从而决定是否让该 jedis 连接获得锁。 d.第四个为 SET\_WITH\_EXPIRE\_TIME,这个参数我们传的值是 PX,意思是我们要给这个 key 加一个过期的设置,关于过期通俗易懂的解释是:上锁使用的键值对(第一和第二个参数)在出生时被赋予了生命,感性的讲,生命是有限的,它不是例外当然也会死去,我们站在上帝的视角给它赋予生命的上限,生命的上限由第五个参数决定。 e.第五个为 expireTime,与第四个参数相呼应,代表 key 的过期时间,单位为 ms。锁的有效时间(lock validity time),设置成多少合适呢?如果设置太短的话,锁就有可能在客户端完成对于共享资源的访问之前过期,从而失去保护;如果设置太长的话,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作。看来真是个两难的问题。 (3)解锁使用的代码: /\*lua脚本\*/ String script = "if redis.call('get', KEYS\[1\]) == ARGV\[1\] then return redis.call('del', KEYS\[1\]) else return 0 end"; /\*通过释放锁的返回值来判断解锁是否成功\*/ Object result = jedis.eval(script, Collections.singletonList(param), Collections.singletonList(UUID)); 使用两行代码来完成我们的解锁: a.第一行代码创建了一行 lua 脚本字符串,这个字符串在第二行代码中用到了。 if redis.call('get', KEYS\[1\]) == ARGV\[1\] then return redis.call('del', KEYS\[1\]) else return 0 end 这行代码的逻辑是:如果拿到的 KEYS\[1\] 和 ARGV\[1\] 相等,则把这个键删除,如果不相等则不做操作,利用这个 lua 脚本,我们可以保证加锁和解锁的客户端是同一个,具体实现可可以查看关于第二行代码的解释。 释放锁的操作必须使用 lua 脚本来实现。释放锁其实包含三步操作:'GET'、判断和'DEL',用 lua 脚本来实现能保证这三步的原子性。 b.第二行代码使用 jedis 的 eval 方法,使参数 KEYS\[1\] 赋值为 业务相关参数,ARGV\[1\] 赋值为 UUID。只有当初给这个键设置值的 jedis 客户端才能给它解锁,因为只有它的 UUID和 redis 中 业务相关参数键的值相等。其它客户端由于 UUID 不同,就不能删除这个键,也就是不能解锁。