前言:基于redis实现企业级实际生产中分布式锁的应用
/**
* 基于redis的分布式锁
*
* @param key
* @param expireSec 秒
* @return
*/
public boolean getLock(String key, int expireSec) {
long expireTs = System.currentTimeMillis() + expireSec * 1000;
Boolean b = redisTemplate.opsForValue().setIfAbsent(key, "" + expireTs);
if (Boolean.TRUE.equals(b)) {
return true;
}
boolean flag = false;
try {
// 值不为空且还未到过期时间,则视为获取锁失败
Object v = get(key);
if (Objects.nonNull(v) && Long.parseLong(v.toString()) >= System.currentTimeMillis()) {
return false;
}
Object oldValue = redisTemplate.opsForValue().getAndSet(key, "" + expireTs);
if (Objects.isNull(oldValue)) {
return true;
}
flag = oldValue.toString().equals(v.toString());
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
代码解析:
redis实现分布式锁的核心逻辑setnx命令+对应key设置过期时间
setnx命令保证多客户端redis会保证当前操作原子性,即要么设置成功,要么设置失败,多个客户端操作相互独立。
例如,假设有两个客户端 A 和 B 几乎同时尝试使用 SETNX
命令设置同一个键 mylock
:
- 如果客户端 A 的
SETNX mylock valueA
命令先执行,并且键mylock
不存在,那么 A 的操作将成功,并返回1
。 - 在 A 的操作完成后,客户端 B 的
SETNX mylock valueB
命令执行时,由于mylock
已经存在,B 的操作将失败,并返回0
。
在这个过程中,Redis 保证:
- A 和 B 的操作不会互相干扰,不会出现 A 操作设置了一半的值然后被 B 操作覆盖的情况。
- 最终
mylock
键的状态是确定的,要么是 A 设置的valueA
,要么是原本就存在的值(如果 A 操作之前mylock
就已经存在的话)。
对key设置过期时间是为了防止过程出现异常,key一直存在导致死锁问题
代码逐行说明:
Boolean b = redisTemplate.opsForValue().setIfAbsent(key, "" + expireTs);
这个一步对应的redis中命令setnx,同时我们将key对应的value设置为过期时间,后面我们通过改时间判断key有没有过期。这样做好处是我们不需要额外进行设置过期时间这一步骤,避免设置过期时间这一步发生异常设置失败,这个key就没有过期时间了。
Object v = get(key);
if (Objects.nonNull(v) && Long.parseLong(v.toString()) >= System.currentTimeMillis()) {
return false;
}
这一步判断锁的过期时间,没过期说明锁还在被占用,获取失败。
Object oldValue = redisTemplate.opsForValue().getAndSet(key, "" + expireTs);
if (Objects.isNull(oldValue)) {
return true;
}
flag = oldValue.toString().equals(v.toString());
这一步核心是使用getset命令,这是一个原子性操作,获取值并设置新值。如果获取的值为null说明可以正常获取锁,否则我们将getset获取到的值与上面get获取的值比较,相等说明是第一次设置值正常获取锁,不相等说明存在并发情况,这次getset已经不是第一次设置的值了。