HBase源码分析7—行锁和MVCC
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 提供的最主要的两个方法是getLock和release 。
感觉这俩类完全可以合成一个写……暂时可以这么理解
具体锁是怎么用的呢?获取锁的函数在下面(5000多行),用起来是这样的
RowLock rowLock = getRowLockInternal(get.getRow(), false, null);
其中第二个参数是“是否是读锁”,第三个参数是“上一个锁(prevRowLock)”。
注:我在这块看了很多遍,我觉得prevRowLock 这个东西完全不起作用,本意是判断如果这个行锁前面被获取过(所谓的prevRowLock),就直接返回他,但是我觉得这个玩意保存有问题,是不会触发的……所以后面涉及这个玩意的代码我都略过了。
(提了个issue上去,当然也有可能是我看错了,如有麻烦指正……)
在getRowLockInternal 中一共做了两件事:
- 从全局变量lockedRows 中找row对应的RowLock ,如果没有就新建
- 设置超时,尝试获取锁
以上就是行锁的分析,比较简单。
HBase中的MVCC
MVCC是一种版本控制的手段,目的是减少锁的使用,使得读不加锁,读写不冲突。通常数据库实现MVCC都是通过快照来实现的,HBase的实现方式其实跟快照差不多。
在HRegion 中定义了一个mvcc控制实例
private final MultiVersionConcurrencyControl mvcc = new MultiVersionConcurrencyControl();
MultiVersionConcurrencyControl 这个类就是实现mvcc的类,比较短,大概意思是:它内部维护了一个队列writeQueue ,然后提供了两个方法:
- begin 写事务开始时调用
- 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 早晚会跟上来的。