redis分布式锁

什么是分布式锁

  一个重要资源被多个jvm进程竞争,会发生数据安全问题,对于分布式系统来说,多个微服务同时竞争一个资源时,就会产生如上问题。对于该问题,我们的方案是用分布式锁来锁住该数据。

使用redis来分布式锁。

  分布式锁的本质是在redis里面占一个坑,当别的进程也要占时,却占不了了,只能等待,或者放弃。
  命令如下setnx(set if not exists)只允许一个客户端占坑,然后del 则是删除这个锁。

问题

  1.由于种种原因,比如程序执行到中间出了bug,导致这个del指令没有被调用,这样就会陷入死锁。

  2.于是我们队这个setnx加了过期时间限制,比如setnx lock true; expire lock 5,使得过5秒自动过期。然后再删除。但是存在问题,当setnx和expire之间服务器突然挂掉了,会导致expire得不到执行,从而继续死锁。如果用redis事务来处理也不行,因为当setnx没有抢到锁时,expire是不该被执行的。而这个redis事务里面没有ifelse判断语句。后续redis2.8版本该作者加入了set指令的扩展参数,是的setnx和expire可以一起执行形成一个原子操作。彻底解决了这个问题。

  3.存在超时问题:当一个进程获取到锁后,由于逻辑执行部分太长,以至于超出了锁的超时限制。
  
  那就出现了问题,因为第二个进程获取到了这个锁,接着第一个线程执行了业务逻辑,于是就释放了这个锁,那么第三个进程就会获取到这个锁。
   
  为了避免这个问题,redis分布式锁,一般不用于较长时间的任务,如果真的出现了,那会很麻烦。为了避免第一个进程删除第二个进程锁的问题。我们可以在加锁前,设置一个随机数,释放锁的时候就进行判断是否需要删除这个锁。这样就保证了自己删自己的锁。但是匹配value和删除可以不是一个原子操作,这就需要lua脚本处理了,因为lua脚本可以保证多个命令是原子操作的。可以将匹配和删除放在一起。

  4.主从集群问题,当主节点挂掉了,从节点变成了主节点,但是从节点没有锁,其他进程就会请求加锁成功。为了解决这个问题,有些开源的library对其做了良好的封装。用户可以拿来就用。比如redlock-py,加锁时,它会向过半节点发送加锁命令,释放锁的时候,则是删除所有节点信息

代码如下

  获取锁。要让其加锁和释放锁的是同一个线程,因此给这个key加了valu.第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作.

  /**
 * 尝试获取分布式锁
 * @param jedis Redis客户端
 * @param lockKey 锁
 * @param requestId 请求标识
 * @param expireTime 超期时间
 * @return 是否获取成功
 */
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

    String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

    if (LOCK_SUCCESS.equals(result)) {
        return true;
    }
    return false;

}

  释放锁。先判断value是否一致,看看自己是不是自己加的锁,让自己释放自己的锁。判断和删除要保证原子性,因此要使用lua脚本保证其原子性。

 /**
 * 释放分布式锁
 * @param jedis Redis客户端
 * @param lockKey 锁
 * @param requestId 请求标识
 * @return 是否释放成功
 */
public static boolean releaseDistributedLock(Jedis jedis, 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";
    Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

    if (RELEASE_SUCCESS.equals(result)) {
        return true;
    }
    return false;

}