Redis实现分布式锁
2020-06-17 本文已影响0人
cbhe
前言
一般有三种方式来实现分布式锁:
- 使用数据库实现
- 使用 redis 实现
- 使用 zookeeper 实现
本文就是来分析一下如何用 redis 实现分布式锁。
实现分布式锁要遵循的原则
为了保证分布式锁的可用性和可靠性,至少要满足以下四条原则:
- 互斥性:任意时刻,必须保证只能有一个客户端可以获取到锁。
- 解铃还须系铃人:分布式锁只能由加锁的客户端来解锁。
- 不会发生死锁:即使持有锁的客户端崩溃,也要保证分布式锁在可容忍的时间内解锁。
- 具有容错性:只要大部分 redis 客户端正常运行,分布式锁就可以正常加锁和解锁。
加锁方式
首先我们看一下Redis的这个方法:
SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
它的参数解释如下:
- EX seconds:以秒为单位设置过期时间
- PX milliseconds:以毫秒为单位设置过期时间
- NX:仅当 key 不存在时添加 key:value
- XX:仅当 key 已经存在时执行,并将原值改为命令中指定的 value
- KEEPTTL:保留先前给这个 key 设置的过期时间(这个怎么用,求大神留言区解答一下)
它的返回值如下:
- 设置成功返回 “OK”
- 如果失败则返回 Null (失败一般是因为不满足 NX|XX 条件)
而分布式锁加锁过程就利用了上面我们提到的 SET 函数,java 代码如下:
String lockResult = jedis.set(lockName, clientId, "NX", "EX", expireTime);
其中变量lockName
就是分布式锁的名字,clientId
是当前请求锁的客户端的id。后面三个参数的意思就是只有当指定键不存在时才执行添加操作,且过期时间是expireTime
指定的秒数。如果该条语句返回"OK"则表示加锁成功,返回 null 表示加锁失败。
我们再分析一下这个加锁命令是否满足了可靠性和可用性:
- 互斥性:对于指定的一个分布式锁 lockName,由于“NX”下只能在 key 不存在时才能设置成功,因此多台客户端时显然只能有一个客户端才能设置成功。
- 只能由加锁的客户端执行解锁:我们在加锁时制定了 value 为客户端id,因此解锁时可以根据该id来判断是否是本客户端加的锁,所以可以保证只能是加锁的客户端才能执行解锁操作。
- 不会发生死锁:我们指定了过期时间,因此即便客户端不执行解锁操作,在指定时间后也会删除该 key,确保了该分布式锁能在容忍的时间内自动解锁。
解锁操作
解锁操作相对简单,我们可以先查询一下lockName的值,如果能查到,证明已经加锁。再比较一下查到的clientId是否为当前客户端id,如果是,则执行删除操作,删除这个 key,就可以解锁了。如果查不到则无需做任何操作,或者报错(这时候程序员认为有加锁但实际上没有,肯定是哪里出了问题)。
解锁代码如下:
public void unLock(String lockName, int clientId)
Stirng lockedClientId = jedis.get(lockName);
if (lockedClientId != null && lockedClientId.equals(clientId)){
jedis.del(lockName);
}
}
编程模板
上面说的解锁方法在某种情况下可以简化一些,一般我们在编程时候通用加锁解锁模型如下:
public void lockAndHandle(){
String lockResult = jedis.set(lockName, clientId, "NX", "EX", expireTime);
if(lockResult == null || !lockResult.equals("OK")){
return;
}
try{
doSomethings();
} catch(Exception e){
handleException(e);
} finally {
jedis.del(lockName);
}
}