分布式锁系列--03关于分布式锁的选型分析01

2022-05-07 16:43:04 浏览数 (1)

本文分析,在分布式系统中,使用redis实现分布式锁,会遇到什么问题。关于分布式锁概念redis分布式锁的具体实现,可参考前面的2篇文章。本文重点在于,对分布式锁技术选型的分析。

1.redis锁单节点实现

常规的,使用redis做分布式锁,主要实现如下:

1.1加锁
代码语言:javascript复制
  /**
   * 加锁
   *
   * @param lockName 锁名,对应被争用的共享资源
   * @param randomValue 随机值,需要保持全局唯一,便于释放时校验锁的持有者
   * @param expireTime 过期时间,到期后自动释放,防止出现问题时死锁,资源无法释放
   * @return
   */
  public static boolean acquireLock(String lockName,String randomValue,int expireTime){
    Jedis jedis = jedisPool.getResource();
    try {
      while (true){
        String result = jedis
            .set(lockName, randomValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if(LOCK_SUCCESS.equals(result)){
          logger.info("【Redis lock】success to acquire lock for [ " lockName " ],expire time:" expireTime "ms");
          return true;
        }
      }
    }catch (Exception ex){
      ex.printStackTrace();
    }finally {
      if(null != jedis){
        jedis.close();
      }
    }
    logger.info("【Redis lock】failed to acquire lock for [ " lockName " ]");
    return false;
  }
1.2解锁
代码语言:javascript复制
  /**
   * redis释放锁
   * watch和muti命令保证释放时的对等性,防止误解锁
   *
   * @param lockName 锁名,对应被争用的共享资源
   * @param randomValue 随机值,需要保持全局唯一,以检验锁的持有者
   * @return 是否释放成功
   */
  public static boolean releaseLock(String lockName,String randomValue){
    Jedis jedis = jedisPool.getResource();
    try{
      jedis.watch(lockName);//watch监控
      if(randomValue.equals(jedis.get(lockName))){
        Transaction multi = jedis.multi();//开启事务
        multi.del(lockName);//添加操作到事务
        List<Object> exec = multi.exec();//执行事务
        if(RELEASE_SUCCESS.equals(exec.size())){
          logger.info("【Redis lock】success to release lock for [ " lockName " ]");
          return true;
        }
      }
    }catch (Exception ex){
      logger.info("【Redis lock】failed to release lock for [ " lockName " ]");
      ex.printStackTrace();
    }finally {
      if(null != jedis){
        jedis.unwatch();
        jedis.close();
      }
    }
    return false;
  }

2.redis锁单节点分析

基于单点redis的锁实现,上述这种实现,基本达到了单节点的安全限度,解决了如下几个问题:

1.防止死锁

设置过期时间后,即使客户端挂了,加锁后未解锁,这个锁也是会到期释放的,不存在死锁的可能。

典型的死锁场景:

一个客户端获取锁成功,但是在释放锁之前崩溃了,此时该客户端实际上已经失去了对公共资源的操作权,但却没有办法请求解锁(删除 key-value 键值对),那么,它就会一直持有这个锁,而其它客户端永远无法获得锁。

2.对称性

加锁的时候,我们给锁设置了个随机值,保证了即使在如下情况,解锁也是只会释放自己加的锁,而不会误删。

典型的误删场景:

假如获取锁时SET的不是一个随机字符串,而是一个固定值,那么可能会发生下面的执行序列:

  1. 客户端1获取锁成功。
  2. 客户端1在某个操作上阻塞了很长时间。
  3. 过期时间到了,锁自动释放了。
  4. 客户端2获取到了对应同一个资源的锁。
  5. 客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。

此时,如果客户端2访问共享资源,就没有锁来提供资源保护了。

3.互斥性

一个时刻只能有一个客户端可以得到锁,这个由redis自身命令setnx即可得到保证。

4.解锁

释放锁,包含3个操作,“get”、“判断”、“del”,我们必须保证释放锁时,这三个操作是原子性的。有2种方式来保证这一批操作的原子性。(这里本质是想说,这三个操作期间,这个key不能被更改,基于watch机制的乐观锁)

  • 1.我们可以执行lua脚本,来保证删除操作的原子性。
  • 2.使用redis提供的watch机制,如前文的实现,来保证。

如果三个操作的原子性得不到保证,下面的场景,就会出问题:

  1. 客户端1获取锁成功。
  2. 客户端1访问共享资源。
  3. 客户端1为了释放锁,先执行’GET’操作获取随机字符串的值。
  4. 客户端1判断随机字符串的值,与预期的值相等。
  5. 客户端1由于某个原因阻塞住了很长时间。
  6. 过期时间到了,锁自动释放了。
  7. 客户端2获取到了对应同一个资源的锁。
  8. 客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。
前面的4个问题,如果我们自己单节点实现时,可以考虑到,代码层面都是可以正确处理的。那么,还有一个问题,如何做到高可用呢?

5.高可用

单节点,存在挂机的风险,为了达到高可用,我们可以做redis集群。一个redis节点作为master,master挂一个slave,当master挂掉后,自动切到slave节点。

看上去,前面5个问题都得到了解决。

但是,在集群模式下,考虑一个场景:

  1. 客户端1从Master获取了锁。
  2. Master宕机了,存储锁的key还没有来得及同步到Slave上。
  3. Slave升级为Master。
  4. 客户端2从新的Master获取到了对应同一个资源的锁。

由于redis的主从复制是异步的,这可能导致,在failover的过程中,资源丧失了锁带来的安全性。

这是我们使用redis实现锁遇到的第一个问题。

6.参数的选择

在前文的算法中,我们给锁设置了有效期,这个值,究竟多少合适呢?

如果太短,锁可能在客户端还未完成对资源的操作之前就过期,从而失去了保护;

如果太长,一个客户端如果主动释放锁失败了,那么,需要等到过期时间才会被动释放,那么,在漫长的有效期内,其他客户端,都无法获得这个资源的锁。

这是我们使用redis实现锁遇到的第二个问题。

0 人点赞