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 的作用在我看来主要有两个:

  • 提供了可重入能力
  • 提供了具体的锁结构

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

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

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

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

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

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

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

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

getRowLockInternal 中一共做了两件事:

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

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

HBase中的MVCC

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

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

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

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

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

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

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

我们看 complete 方法

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

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

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

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

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

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


发表评论

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

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