HBase源码分析7—行锁和MVCC

12. 四月 2018 hbase, 数据库 0

HBase中的Row Lock

HBase是一个列式数据库,不像传统的RDBMS一样会把一整行数据作为一个整体来看待。当有两个请求并发修改同一个rowkey的A、B两个列,如果没有锁的控制,可能出现两个列分别体现了两个请求的修改的情况,这种情况是不一致的。为了避免不一致情况出现,需要行锁的控制(当然也可以像pg一样纯无锁,但那不是HBase的实现方式,暂不讨论了)。

HBase的所有操作最终都是落到region server来操作的,所以锁的实现放在了HRegion 里。主要涉及两个类:RowLock 和RowLockContext ,其中RowLock 是抽象类,其具体实现是RowLockImpl 。

RowLockContext 的作用在我看来主要有两个:

  • 提供了可重入能力
  • 提供了具体的锁结构
class RowLockContext {
    private final HashedBytes row;
    final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
    final AtomicBoolean usable = new AtomicBoolean(true);            // cleanup 之后被设为false
    final AtomicInteger count = new AtomicInteger(0);                // 重入多少次
    final Object lock = new Object();                                // 一个用于自己synchronized的符号
    private String threadName;
...
}

RowLockContext 类主要提供了两个方法:newReadLock 和newWriteLock ,他们的实现类似这样

RowLockImpl newReadLock() {
    Lock l = readWriteLock.readLock();
    return getRowLock(l);
}

getRowLock 方法用自己的l 作为参数构造了一个RowLock 吐出来。

public static class RowLockImpl implements RowLock {
    private final RowLockContext context;
    private final Lock lock;

    public RowLockImpl(RowLockContext context, Lock lock) {
      this.context = context;
      this.lock = lock;
    }
...
}

而这个RowLockImpl 提供的最主要的两个方法是getLockrelease 。

感觉这俩类完全可以合成一个写……暂时可以这么理解

具体锁是怎么用的呢?获取锁的函数在下面(5000多行),用起来是这样的

RowLock rowLock = getRowLockInternal(get.getRow(), false, null);

其中第二个参数是“是否是读锁”,第三个参数是“上一个锁(prevRowLock)”。

注:我在这块看了很多遍,我觉得prevRowLock 这个东西完全不起作用,本意是判断如果这个行锁前面被获取过(所谓的prevRowLock),就直接返回他,但是我觉得这个玩意保存有问题,是不会触发的……所以后面涉及这个玩意的代码我都略过了。

(提了个issue上去,当然也有可能是我看错了,如有麻烦指正……)

getRowLockInternal 中一共做了两件事:

  1. 从全局变量lockedRows 中找row对应的RowLock ,如果没有就新建
  2. 设置超时,尝试获取锁

以上就是行锁的分析,比较简单。

HBase中的MVCC

MVCC是一种版本控制的手段,目的是减少锁的使用,使得读不加锁,读写不冲突。通常数据库实现MVCC都是通过快照来实现的,HBase的实现方式其实跟快照差不多。

HRegion 中定义了一个mvcc控制实例

private final MultiVersionConcurrencyControl mvcc = new MultiVersionConcurrencyControl();

MultiVersionConcurrencyControl 这个类就是实现mvcc的类,比较短,大概意思是:它内部维护了一个队列writeQueue ,然后提供了两个方法:

  1. begin 写事务开始时调用
  2. complete 写事务结束时调用

mvcc类内部有两个计数器,分别是writePoint ——写事务的点(可以看做递增的序列号)和readPoint 可读的点(当前已commit的最大序列号),读的时候只会读取mvcc值小于等于当前readPoint的数据。

每当写事务开启时,都会给这个WriteEntry 分配一个新的writePoint (通过原子的incrementAndGet 实现),结束时尝试将readPoint 提高到writePoint 的位置。

那么问题来了:如果我们依次有1、2、3、4四个写事务,4号事务先完成了,前三个迟迟没有完成,会怎样?

我们看complete 方法

public boolean complete(WriteEntry writeEntry) {
    synchronized (writeQueue) {
        writeEntry.markCompleted();
        long nextReadValue = NONE;
        boolean ranOnce = false;
        while (!writeQueue.isEmpty()) {
            ranOnce = true;
            WriteEntry queueFirst = writeQueue.getFirst();

            if (nextReadValue > 0) {
                if (nextReadValue + 1 != queueFirst.getWriteNumber()) {
                    throw new RuntimeException("Invariant in complete violated, nextReadValue="
                            + nextReadValue + ", writeNumber=" + queueFirst.getWriteNumber());
                }
            }

            if (queueFirst.isCompleted()) {
                nextReadValue = queueFirst.getWriteNumber();
                writeQueue.removeFirst();
            } else {
                break;
            }
        }

        if (!ranOnce) {
            throw new RuntimeException("There is no first!");
        }

        if (nextReadValue > 0) {
            synchronized (readWaiters) {
                readPoint.set(nextReadValue);
                readWaiters.notifyAll();
            }
        }
        return readPoint.get() >= writeEntry.getWriteNumber();
    }
}

他会在队列首的第一个未完成的writeEntry 那里break ,然后设置readPoint。也就是说,这个函数执行完之后,虽然4号事务已经提交了,但是仍然不可见。所以mvcc还提供了另外一个同步的方法。在写入memstore的时候实际上是这样的

// STEP 5. Write back to memStore
// NOTE: writeEntry can be null here
writeEntry = batchOp.writeMiniBatchOperationsToMemStore(miniBatchOp, writeEntry);

// STEP 6. Complete MiniBatchOperations: If required calls postBatchMutate() CP hook and
// complete mvcc for last writeEntry
batchOp.completeMiniBatchOperations(miniBatchOp, writeEntry);

封装了一层mvcc,里面调用的是同步方法。

至于异步的complete 是给谁用的,是用来写WAL的。但是WAL不是更重要吗?怎么就能异步了?

WAL使用的是disrupter框架(ring buffer),多生产者单消费者,可以保证写入的顺序。在doWALAppend 的最后会sync 一下保证WAL确实是写入了的,但是WAL占用的这个writePoint 之前可能还有未提交的事务。这个时候,readPoint 是否跟上来了是不重要的,因为:

  • 写入WAL并不代表事务完成,所以此时不可见是正确的。
  • memstore写入成功才是事务的终点,那个时候会使用同步方法来保证可见性。

也就是,他知道readPoint 早晚会跟上来的。


发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据