细粒度锁的实现之分级锁的设计实现

前言


在分布式系统中,想必我们经常会看到锁的应用来保证操作的原子性,使用较简单的例如对象锁,单一锁等等,再高级一点的例如读写锁等等。但是不论是单一锁或者读写锁,在使用上都具有一定的互斥性。这里的互斥性指的是当某个锁持有者持有当前的锁之后,其它线程必须进行阻塞等待操作。这种行为在具有很高workload的系统中,代价还是比较高的。从更深层次来看待这种锁,它是一种单一粒度,较为粗粒度的锁设计模式。那么在实际的应用中,我们是否能够将这种单一锁进行优化呢,使之用起来能够更为的高效。本文笔者将要讲述的将是粗粒度锁的细粒度化改造,改造的实现方式为分级锁的实现。

锁细粒度化改造的好处


为什么我们这么强调锁的细粒度化改造呢?相比于粗粒度锁,它在使用上能够带给系统怎样的帮助呢?

说到这里我们不得不谈到粗粒度锁在使用上的一些弊端,典型的一点比如它会阻塞一些毫无关联的请求操作的处理。比如某个存储系统在根目录下有A,B两个目录,为了保证系统处理请求操作的原子性,我们用锁来做其中的控制。如果我用1个锁来做,则会有一下两种情况发生:

  • 系统在执行A目录下的请求操作,持有锁状态,B目录的所有操作被block住。
  • 系统在执行B目录下的请求操作,持有锁状态,A目录的所有操作被block住。

但其实上面的场景系统在持有锁的情况去保护A目录的并发修改却同样block住了B目录的操作,这其实是可以避免的,我们完全可以让这2个目录的相关操作并发地执行,然后再用2个对应锁去保证这2个目录空间下的请求操作。这样的话,系统的请求吞吐量将会上升很多。

在上面的例子中从一个全局单一锁到2个命名空间目录单独锁的拆分,就是锁细粒化改造的一个简单例子。下面本文将要讲述的分级锁的设计部分也是采用了上述的思路,但是额外多了部分的优化改造,使之更适用于实际系统的使用。

分级锁的设计和实现


本节将要介绍的分级锁的主要特点在于它包含有多个层级的锁,在这里我们以两级锁为例,在此锁内,包含有2个级别锁:

  • Top锁
  • Child锁

在分布锁中,遵守以下规则:

在操作开始前,必须先申请得到Top锁来准备获取Child锁,在获取得到Child锁之后,可以再释放Top锁。这里的Child锁,可以理解为就是每个分区锁。这里Top锁的目的是为了保证获取各个分区锁的原子性。

分级锁原型定义如下:

 /**
 * LatchLock controls two hierarchical Read/Write locks:
 * the topLock and the childLock.
 * Typically an operation starts with the topLock already acquired.
 * To acquire child lock LatchLock will
 * first acquire the childLock, and then release the topLock.
 */
public abstract class LatchLock<C> {
  // Interfaces methods to be defined for subclasses
  /** @return true topLock is locked for read by any thread */
  protected abstract boolean isReadTopLocked();
  /** @return true topLock is locked for write by any thread */
  protected abstract boolean isWriteTopLocked();
  protected abstract void readTopdUnlock();
  protected abstract void writeTopUnlock();

  protected abstract boolean hasReadChildLock();
  protected abstract void readChildLock();
  protected abstract void readChildUnlock();

  protected abstract boolean hasWriteChildLock();
  protected abstract void writeChildLock();
  protected abstract void writeChildUnlock();

  protected abstract LatchLock<C> clone();

  // Public APIs to use with the class
  public void readLock() {
    // 在获取child锁后,可以释放top锁
    readChildLock();
    readTopdUnlock();
  }

  public void readUnlock() {
    readChildUnlock();
  }

  public void writeLock() {
    // 在获取child锁后,可以释放top锁
    writeChildLock();
    writeTopUnlock();
  }

  public void writeUnlock() {
    writeChildUnlock();
  }
}

在分级锁中,尽管Top锁会是同一个,但是假设我们获取的不同的Child锁,其实不会收到Top锁其它线程持有的情况。因为其它Child锁被lock之后,Top锁就释放了,这样的话其它分级锁的Child锁的获取就不会受到影响了。
在这里Top锁扮演的还是之前全局同一锁的角色,但是所锁住的对象是每个分区的实例而不是每一个具体的操作了。

这里我们以典型的HDFS FSN全局单一锁为例作为Top锁的分级锁实现:

 public class INodeMapLock extends LatchLock<ReentrantReadWriteLock> {
    Logger LOG = LoggerFactory.getLogger(INodeMapLock.class);

    private ReentrantReadWriteLock childLock;

    INodeMapLock() {
      this(null);
    }

    private INodeMapLock(ReentrantReadWriteLock childLock) {
      assert namesystem != null : "namesystem is null";
      this.childLock = childLock;
    }

    @Override
    protected boolean isReadTopLocked() {
      return namesystem.getFSLock().isReadLocked();
    }

    @Override
    protected boolean isWriteTopLocked() {
      return namesystem.getFSLock().isWriteLocked();
    }

    @Override
    protected void readTopdUnlock() {
      namesystem.getFSLock().readUnlock("INodeMap", false);
    }

    @Override
    protected void writeTopUnlock() {
      namesystem.getFSLock().writeUnlock("INodeMap", false, false);
    }

    @Override
    protected boolean hasReadChildLock() {
      return this.childLock.getReadHoldCount() > 0 || hasWriteChildLock();
    }

    @Override
    protected void readChildLock() {
      // LOG.info("readChildLock: thread = {}, {}", Thread.currentThread().getId(), Thread.currentThread().getName());
      this.childLock.readLock().lock();
      namesystem.getFSLock().addChildLock(this);
      // LOG.info("readChildLock: done");
    }

    @Override
    protected void readChildUnlock() {
      // LOG.info("readChildUnlock: thread = {}, {}", Thread.currentThread().getId(), Thread.currentThread().getName());
      this.childLock.readLock().unlock();
      // LOG.info("readChildUnlock: done");
    }

    @Override
    protected boolean hasWriteChildLock() {
      return this.childLock.isWriteLockedByCurrentThread();
    }

    @Override
    protected void writeChildLock() {
      // LOG.info("writeChildLock: thread = {}, {}", Thread.currentThread().getId(), Thread.currentThread().getName());
      this.childLock.writeLock().lock();
      namesystem.getFSLock().addChildLock(this);
      // LOG.info("writeChildLock: done");
    }

    @Override
    protected void writeChildUnlock() {
      // LOG.info("writeChildUnlock: thread = {}, {}", Thread.currentThread().getId(), Thread.currentThread().getName());
      this.childLock.writeLock().unlock();
      // LOG.info("writeChildUnlock: done");
    }

    @Override
    protected LatchLock<ReentrantReadWriteLock> clone() {
      return new INodeMapLock(new ReentrantReadWriteLock(false)); // not fair
    }
  }

在使用分级锁时,如果遇到可能需要获取多分区(Child)锁时,则要进行多个分区Child锁的获取,之后再释放Top锁,操作方法如下:

  /**
   * 获取多Child锁,keys为write操作涉及到的相关分区实例
   */ 
  public void latchWriteLock(K[] keys) {
    LatchLock<?> pLock = null;
    for(K key : keys) {
      pLock = getPartition(key).partLock;
      pLock.writeChildLock();
    }
    assert pLock != null : "pLock is null";
    pLock.writeTopUnlock();
  }

在上面的例子中,遵循的规则如下:

每个partition对应一个partition锁(就是本文提到的分级锁),每个partition锁包含Child锁和Top锁,Top锁是所有partition锁共用的一个锁,Child锁则是每个Partition独有的。所以我们可看到,分级锁在多partition情况下可以很好地得到运用。

本文阐述的分级锁的设计以及实现参考了目前Hadoop社区基于metadata partition的NN改造相关设计,感兴趣的同学可前往引用链接处继续学习了解。

引用


[1].https://issues.apache.org/jira/browse/HDFS-14703 . NameNode Fine-Grained Locking via Metadata Partitioning

发布了373 篇原创文章 · 获赞 403 · 访问量 203万+

猜你喜欢

转载自blog.csdn.net/Androidlushangderen/article/details/100047602
今日推荐