分布式锁的实现方式有多种,我觉得redis实现起来应该是最方便的,后续我会更新下通过zookeeper来实现分布式锁。
首先看获取分布式锁的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 获取分布式锁
*
* @param lockKey 锁
* @param requestId 请求表示
* @param expireTime 超时时间,单位为毫秒
* @return
*/
public boolean tryGetLock(String lockKey, String requestId, int expireTime) {
//setIfAbsent对应的setnx
if (valueOperations.setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS)) {
return true;
}
return false;
}
对方法和参数一一解释下,
- setIfAbsent,当key不存在时,我们进行set操作;若key已经存在,则不做任何操作,也就是说当别的进程获取锁的时候key值是已存在的。
- requestId,在释放锁的时候根据requestId来进行释放,
- expireTime则是为了防止死锁而设置的超时时间自动释放锁,如果redis突然崩溃,而key没有设置超时时间,则key一直存在,应用一直无法获取到锁
释放锁的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 释放分布式锁
*/
public boolean unLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
return redisTemplate.execute(
(RedisConnection connection) -> connection.eval(
script.getBytes(),
ReturnType.INTEGER,
1,
lockKey.getBytes(),
requestId.getBytes())
).equals(1);
}
这里执行的是一段lua代码,逻辑很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。 eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令,就能保证操作的原子性, 如果不能保证原子性的话会存在以下场景: 客户端A加锁,一段时间之后客户端A解锁,在执行delete操作之前,锁过期了, 此时客户端B尝试加锁成功,然后客户端A再执行delete方法,则将客户端B的锁给解除了。
redis官方redlock的分布式锁的实现,但是争议很大,所以不建议用redis来做分布式锁。 以下是redlock算法流程:
- 获取当前时间戳,单位是毫秒
- 跟上面类似,轮流尝试在每个master节点上创建锁,过期时间较短,一般就几十毫秒
- 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
- 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
- 要是锁建立失败了,那么就依次删除这个锁
- 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁