Redis 分布式锁
本篇介绍了 redis的分布式锁。 简单介绍了redis的分布式锁的一些基本,了解redis分布式锁。
分布式锁的奥义1.1
分布式所的本质就是要实现一个萝卜一个坑,当别的进程也要来占坑时发现那里一已经有一个“大萝卜”了,只能放弃或者稍后做尝试。 占坑一般使用setnx指令,他的意思就是“set if not exists”。只允许被一个客户端占用,当这个客户端占用完之后用del指令释放“坑”,其他客户端才能来调用。
> setx zikerHello true
OK
...做一些操作...
> del zikerHello
OK
但是在实际生产系统中,这可能满足不了我们,比如我们逻辑执行到中间出现异常了,可能会导致del指令没有被调用,这样就会陷入死锁,锁永远得不到释放。那我们该如何进行避免呢, redis给我们提供了可以给锁加上一个过期时间,比如5s,这样即使中间出现异常也可以保证5s之后锁会自动释放。> setnx zikerHello true
Ok
>expire zikerHello 5
...这的的意识给这个锁设置了5秒的过期时间,如果这个锁在5秒内没有释放,就会自动释放
> del zikerHello
(integer)1
但是从逻辑上看,上面的情况并不是万无一失的,因为setnx和expire之间服务器挂掉了,或者程序异常了,就会导致expire得不到执行,也会造成死锁。这个问题主要是因为setnx和expire 这两个命令他不是不是原子指令,如果这两条指令一起执行就不会出现问题了。 为了治理这个问题,我从书上学习到redis2.8版本中,作者加入了set指令的扩展参数,使得setnx和expire指令可以一起执行,彻底解决了这个问题。>set zikerHello okok ex 5 nx
ok
做操作
>del zikerHello
ok
上面这个指令就是setnx和expire组合在一起的原子指令,它就是分布式锁的奥义所在。
超时问题1.2
redis的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行得太长,以至于超出了锁得超时限制,就会出现问题。因为这时候第一个线程持有得锁过期了,临界区的逻辑 还有执行完,而同时第二个线程就提前重新新持有了这把锁,导致临界区代码不能得到严格串行执行。 为了避免这个问题,redis分布式锁不要用于较长时间的任务。
tag = random.nextint() #随机数
if redis.set(key, tag, nx=True, ex=5)
do_something()
redis.delifequals(key,tag)# 假想的 dekufequals 指令
`
有一个稍微安全一点的方案就是将set指令的value参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后在删除key,这是为了确保当前线程占有的锁不会被其他线程 释放,除非这个锁是因为过期了而被服务器自动释放的。 但是匹配value和删除key不是一个原子操作,redis也没有提供类似于delifequals这样的指令,这就需要使用Lua脚本处理了,因为Lua脚本保证连续多个指令的原子性执行。
if redis.call("get",KEYS[1]) == ARGV[1] then
return
redis.call("del",KEYS[1])
else
return 0
end
但是这也不是一个完美的方案,它只是相对安全一点,因为如果真的超时了,当前线程的逻辑没有执行完,其他线程也会乘虚而入。
可重入性1.3
可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。比如Java语言里有个ReentrantLock 就是可重入锁。Redis分布式锁如果要支持可重入,需要对客户端的set方法进行包装,使用线程的Threadlocal变量存储当前持有锁的计数。
public class SuoMain {
private ThreadLocal<Map<String, Integer>> lockers = new =ThreadLocal<>();
private Jedis jedis;
public SuoMain(Jedis jedis){
this.jedis = jedis;
}
private boolean _lock(String key) {
return jedis.set(key, "", "nx", "ex", 5L) != null;
}
private void _unlock(String key){
jedis.del(key);
}
private Map<String, Integer> currentLockers() {
Map<String, Integer> refss = lockers.get();
if (refs != null){
return refs;
}
lockers.set(new HashMap<>());
return lockers.get();
}
public boolean lock(String key) {
Map<String, Integer> refs = currentLockers();
Integer refCnt = refs.get(key);
if(refCnt != null){
refs.put(key, refCnt + 1);
return true;
}
boolean ok = this._lock(key);
if(!ok){
return false;
}
refs.put(key, 1);
return true;
}
public boolean unlock(String key){
Map<String, Integer> refs = currentLockers();
Integer refCnt = refs.get(key);
if(refCnt == null){
return false;
}
refCnt -= 1;
if(refCnt > 0){
refs.put(key, refCnt);
}else{
refs.remove(key);
this._unlock(key);
}
return true;
}
public static void main(String[] args){
Jedis jedis = new Jedis();
SuoMain redis = new SuoMain(jedis);
System.out.println(redis.lock("codehole"));
System.out.println(redis.lock("codehole"));
System.out.println(redis.unlock("codehole"));
System.out.println(redis.unlock("codehole"));
}
这是基于JAVA的锁的可重入性的原理,主要就是给予ThreadLocal和引用计数。
这是我对redis分布式锁以及其原理的简单介绍。
枯燥无味的学习往往让你变得更强大。