Redis 分布式锁

本篇介绍了 redis的分布式锁。 简单介绍了redis的分布式锁的一些基本,了解redis分布式锁。

分布式锁的奥义1.1

分布式所的本质就是要实现一个萝卜一个坑,当别的进程也要来占坑时发现那里一已经有一个“大萝卜”了,只能放弃或者稍后做尝试。 占坑一般使用setnx指令,他的意思就是“set if not exists”。只允许被一个客户端占用,当这个客户端占用完之后用del指令释放“坑”,其他客户端才能来调用。

> setx zikerHello trueOK ...做一些操作...> del zikerHelloOK

但是在实际生产系统中,这可能满足不了我们,比如我们逻辑执行到中间出现异常了,可能会导致del指令没有被调用,这样就会陷入死锁,锁永远得不到释放。那我们该如何进行避免呢, redis给我们提供了可以给锁加上一个过期时间,比如5s,这样即使中间出现异常也可以保证5s之后锁会自动释放。
> setnx zikerHello trueOk>expire zikerHello 5...这的的意识给这个锁设置了5秒的过期时间,如果这个锁在5秒内没有释放,就会自动释放> del zikerHello(integer)1 但是从逻辑上看,上面的情况并不是万无一失的,因为setnx和expire之间服务器挂掉了,或者程序异常了,就会导致expire得不到执行,也会造成死锁。这个问题主要是因为setnx和expire 这两个命令他不是不是原子指令,如果这两条指令一起执行就不会出现问题了。 为了治理这个问题,我从书上学习到redis2.8版本中,作者加入了set指令的扩展参数,使得setnx和expire指令可以一起执行,彻底解决了这个问题。
>set zikerHello okok ex 5 nxok做操作>del zikerHellook 上面这个指令就是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])
elsereturn 0end 但是这也不是一个完美的方案,它只是相对安全一点,因为如果真的超时了,当前线程的逻辑没有执行完,其他线程也会乘虚而入。

可重入性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分布式锁以及其原理的简单介绍。

枯燥无味的学习往往让你变得更强大。