Redis 分布式锁
分布式锁的定义:
保证同一时间只能有一个客户端对共享资源进行操作。
另外有几点要求也是必须要满足的:
1、不会发生死锁。 即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
2、具有容错性。 只要大部分的Redis节点正常运行,客户端就可以加锁和解锁
3、解铃还须系铃人。 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
分布式锁演进史
第一版 setnx
setnx,是set if not exists 的缩写,也就是只有不存在的时候才设置, 设置成功时返回 1 , 设置失败时返回 0 。可以利用它来实现锁的效果,但是很多人在使用的过程中都有一些问题没有考虑到。
setIfAbsent(key, val) -> setnx(key, val)
PS: 当执行流程异常,线程被销毁, 锁无法释放, 就会产生死锁.
第二版 expire
基于 setnx 命令实现分布式锁的缺陷也是很明显的, 那就是一定情况下可能发生死锁.
对 Redis 的锁标志位加上过期时间就能很好的防止死锁问题
PS: 对分布式锁添加了过期时间, 但依然无法避免极端情况下的死锁问题。那就是如果在客户端加锁成功后, 还没有设置过期时间时宕机, 所以加锁标志位与添加过期时间命令保证一个原子性, 要么一起成功, 要么一起失败.
第三版 set
添加锁原子命令就要登场了, 从 Redis 2.6.12 版本起, 提供了可选的 字符串 set 复合命令。
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
cache.set(lockKey, "lock", "nx", "ex", 5)
PS: 加锁以及设置过期时间确实保证了原子性, 但存在业务执行时长比过期时间要长的情况, 可能存在线程一将线程二的锁释放掉之后, 线程三获取到锁, 然后线程二执行完将线程三的锁释放.
第四版 verify value
创建辨别客户端身份的唯一值了, 将加锁及解锁归一化
String lockKey = "lockKey";
String lockValue= UUID.randomUUID.toString();
cache.set(lockKey, lockValue, "nx", "ex", 5); //加锁
if(Object.equals(cache.get(lockKey),lockValue)) cache.del(lockKey);
同时为每个客户端设置了 uuid 作为锁标志位的 val, 解锁时需要判断锁的 val 是否和自己客户端的相同, 辨别成功才会释放锁.
PS: 解锁时, 由于判断锁和删除标志位并不是原子性的, 所以可能还是会存在误删
第五版 lua
很不友好的是, del 删除操作并没有提供原子命令, 所以我们需要想点办法
Redis在 2.6 推出了脚本功能, 允许开发者使用 Lua 语言编写脚本传到 Redis 中执行
使用 Lua 脚本有什么好处呢?
1、减少网络开销: 原本我们需要向 Redis 服务请求多次命令, 可以将命令写在 Lua 脚本中, 这样执行只会发起一次网络请求
2、原子操作: Redis 会将 Lua 脚本中的命令当作一个整体执行, 中间不会插入其它命令
3、复用: 客户端发送的脚步会存储 Redis 中, 其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑
# 获取 KEYS[1] 对应的 Val
local cliVal = redis.call('get', KEYS[1])
# 判断 KEYS[1] 与 ARGV[1] 是否保持一致
if(cliVal == ARGV[1]) then
# 删除 KEYS[1]
redis.call('del', KEYS[1])
return 'OK'
else
return nil
end
String script = "local cliVal = redis.call('get', KEYS[1]) " +
"if(cliVal == ARGV[1]) then " +
"redis.call('del', KEYS[1]) " +
"return 'OK' " +
"else " +
"return nil " +
"end ";
cache.eval(script, Lists.newArrayList(lockKey),Lists.newArrayList(lockValue));
//KEYS[1]: lockKey
//ARGV[1]: lockValue
锁过期了怎么办?
例如 Redisson引入了一个Watch Dog机制,这个机制是针对分布式锁来实现锁的自动续约, 如果当前获得锁的线程没有执行完,那么Redisson会自动给Redis中目标key延长超时时间
watch dog 的自动延期机制 , 如果释放锁操作本身异常了,watch dog 还会不停的续期 造成死锁吗?
不会,因为无论释放锁操作是否成功,EXPIRATION_RENEWAL_MAP中的目标 ExpirationEntry 对象已经被移除了,watch dog 通过判断后就不会继续给锁续期了。
释放锁的源码:
// 锁释放
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
}
// 进入 unlockAsync(Thread.currentThread().getId()) 方法 入参是当前线程的id
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
//执行lua脚本 删除key
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.onComplete((opStatus, e) -> {
// 无论执行lua脚本是否成功 执行cancelExpirationRenewal(threadId) 方法来删除EXPIRATION_RENEWAL_MAP中的缓存
cancelExpirationRenewal(threadId);
if (e != null) {
result.tryFailure(e);
return;
}
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
result.trySuccess(null);
});
return result;
}
// 此方法会停止 watch dog 机制
void cancelExpirationRenewal(Long threadId) {
ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (task == null) {
return;
}
if (threadId != null) {
task.removeThreadId(threadId);
}
if (threadId == null || task.hasNoThreads()) {
Timeout timeout = task.getTimeout();
if (timeout != null) {
timeout.cancel();
}
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
}
原链接: https://www.cnblogs.com/MrLiuZF/p/15110559.html
原文有Redisson实现Redis分布式锁的底层原理及加锁机制讲解