在不同进程需要互斥地访问共享资源时,在业务中体现为多个实例需要同时访问同一个 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 不同,就不能删除这个键,也就是不能解锁。