数据库事务系列-HBase行级事务模型

HBase是BigTable的开源实现,事务模型也与BigTable一脉相承 – 仅支持行级别的事务。虽然Jeff Dean大神在接受采访时公开承认目前在技术领域最后悔的事情就是没有在BigTable中加入跨行事务模型,以至于之后很多团队都在BigTable之上重复造各种各样的分布式事务轮子。这点笔者是认同的,现在确实有很多团队在HBase之上造了很多轮子(Tephra | Trafodian | Omid),试想如果这个工作做在了BigTable里面,这些团队的人是不是可以做更多其他有意义的事情了~ 所幸的是之后Google又发布了一篇介绍分布式事务模型的的paper – Percolator,现在很多团队都参考该论文实现分布式事务,包括TiDB、Omid等,也算是一种弥补吧。

虽说HBase目前只支持行级事务,但行级事务也是事务!麻雀虽小,肝脏具全。这篇文章一起看看HBase行级事务所满足的ACID特性。下文中隔离性部分基本翻译文章Apache HBase Internals: Locking and Multiversion Concurrency Control,大家可以阅读原文。

HBase事务原子性保证

HBase数据会首先写入WAL,再写入Memstore。写入Memstore异常很容易可以回滚,因此保证写入/更新原子性只需要保证写入WAL的原子性即可。HBase 0.98之前版本需要保证WAL写入的原子性并不容易,这由WAL的结构决定。假设一个行级事务更新R行中的3列(c1, c2, c3),来看看之前版本和当前版本的WAL结构:

1. 之前版本WAL结构:

<logseq1-for-edit1>:<KeyValue-for-edit-c1>

<logseq2-for-edit2>:<KeyValue-for-edit-c2>

<logseq3-for-edit3>:<KeyValue-for-edit-c3>

每个KV都会形成一个WAL单元,这样一行事务更新多少列就会产生多少个WAL单元。在将这些WAL单元append到日志文件的时候,一旦出现宕机或其他异常,就会出现部分写入成功的情况,原子性更新就无法保证。

2. 当前版本WAL结构:

<logseq#-for-entire-txn>:<WALEdit-for-entire-txn>

<logseq#-for-entire-txn>:<-1, 3, <Keyvalue-for-edit-c1>, <KeyValue-for-edit-c2>, <KeyValue-for-edit-c3>>

通过这种结构,每个事务只会产生一个WAL单元。这样就可以保证WAL写入时候的原子性。

HBase事务一致性保证:强一致性保证

HBase事务隔离性

写写并发控制

1. 为什么需要写写并发控制?

现在假设有两个并发写入请求同时进来,分别对同一行数据进行写入。下图所示RowKey为Greg,现在分别更新列族info下的Company列和Role列:

0007

0001

如果没有任何并发控制策略的话,写入数据(先写WAL,再写memstore)可能会出现不同KV写入”交叉”现象,如下图所示:

0002

这样的话,用户最终读取到的数据就会产生不一致,如下:

0003

2. 如何实现写写并发控制?

实现写写并发其实很简单,只需要在写入(或更新)之前先获取行锁,如果获取不到,说明已经有其他线程拿了该锁,就需要不断重试等待或者自旋等待,直至其他线程释放该锁。拿到锁之后开始写入数据,写入完成之后释放行锁即可。这种行锁机制是实现写写并发控制最常用的手段,后面可以看到MySQL也是使用行锁来实现写写并发的。

3. 如何实现批量写入多行的写写并发?

HBase支持批量写入(或批量更新),即一个线程同时更新同一个Region中的多行记录。那如何保证当前事务中的批量写入与其他事务中的批量写入的并发控制呢?思路还是一样的,使用行锁。但这里需要注意的是必须使用两阶段锁协议,即:

(1) 获取所有待写入(更新)行记录的行锁

(2) 开始执行写入(更新)操作

(3) 写入完成之后再统一释放所有行记录的行锁

不能更新一行锁定(释放)一行,多个事务之间容易形成死锁。两阶段锁协议就是为了避免死锁,MySQL事务写写并发控制同样使用两阶段锁协议。

4. 笔者疑惑

其实笔者一直在这里有个疑惑,还是使用上面例子说明吧。大家都知道,HBase是支持多版本的,那么在数据写入的时候加锁给待写入数据都打上版本信息,此时释放锁。即使之后出现交叉的现象,对于读请求来说,最终读到的数据都是根据最大版本号来的。即使该表只设置了一个版本号,那也可以根据版本号将之前版本的数据清理掉,和交叉不交叉并没有太大关系。不知道大家怎么理解这个问题?有兴趣的可以留言评论!

读写并发控制

1. 为什么需要读写并发控制?

现在我们通过在写入更新之前加锁、写入更新之后释放锁实现写写并发控制,那读写之间是不是也需要一定的并发控制呢?如果不加并发控制,会出现什么现象呢?接着看下图:

0004

上图分别是两个事务更新同一行数据,现在假设第一个事务已经更新完成,在第二个事务更新到一半的时候进来一个读请求,如果没有任何并发控制的话,读请求就会读到不一致的数据,Company列为Restaurant,Role列为Engineer,如下图所示:

0005

可见,读写之间也需要一种并发控制来保证读取的数据总能够保持一致性,不会出现各种诡异的不一致现象。

2. 如何实现读写并发控制?

实现读写并发最简单的方法就是仿照写写并发控制 – 加锁。但几乎所有数据库都不会这么做,性能太差,对于读多写少的应用来说必然不可接受。那还有其他方法吗?

当然,这就是今天要重点提到的MVCC机制 – Mutil Version Concurrent Control。HBase中MVCC机制实现主要分为两步:

(1) 为每一个写(更新)事务分配一个Region级别自增的序列号

(2) 为每一个读请求分配一个已完成的最大写事务序列号

示意图如下所示:

0006

上图中两个写事务分别分配了序列号1和序列号2,读请求进来的时候事务1已经完成,事务2还未完成,因此分配事务1对应的序列号1给读请求。此时序列号1对本次读可见,序列号2对本次读不可见,读到的数据是:

0007

具体实现中,所有的事务都会生成一个Region级别的自增序列,并添加到队列中,如下图最左侧队列,其中最底端为已经提交的事务,队列中的事务为未提交事务。现假设当前事务编号为15,并且写入完成(中间队列红色框框),但之前的写入事务还未完成(序列号为12、13、14的事务还未完成),此时当前事务必须等待,而且对读并不可见,直至之前所有事务完成之后才会对读可见(即读请求才能读取到该事务写入的数据)。如最右侧图,15号事务之前的所有事务都成功完成,此时Read Point就会移动到15号事务处,表示15号事务之前的所有改动都可见。

0008

 Note:上文所讲的自增序列就是上一篇文章的SequenceId!!!可能有朋友有疑问:如果这两个自增序列是同一个序列,那是不是这个队列的顺序必须与事务写入WAL的顺序一致?如果不一致有什么问题?如果要求一致的话怎么才能实现?

千万不要查看1.1.2的代码,会把你彻底搞混的!建议阅读更高版本的相关代码!

所以,MVCC的精髓是写入的时候分配递增版本信息(SequenceId),读取的时候分配唯一的版本用于读取可见,比之大的版本不可见。这里需要注意版本必须递增,而且版本递增的范围一定程度上决定了事务是什么事务,比如HBase是Region级别的递增版本,那么事务就是region级别事务。MySQL中版本是单机递增版本,那么MySQL事务就支持单机跨行事务。Percolator中版本是集群递增版本,那么Percolator事务就是分布式事务。

HBase事务持久性保证

HBase事务持久化可以理解为WAL持久化,目前实现了多种持久化策略:SKIP_WAL,ASYNC_WAL,SYNC_WAL,FSYNC_WAL。SKIP_WAL表示不写WAL,这样写入更新性能最好,但在RegionServer宕机的时候有可能会丢失部分数据;ASYNC_WAL表示异步将WAL持久化到硬盘,因为是异步操作所以在异常的情况下也有可能丢失少量数据;SYNC_WAL表示同步将WAL持久化到操作系统缓存,再由操作系统将数据异步持久化到磁盘,这种场景下RS宕掉并不会丢失数据,当操作系统宕掉会导致部分数据丢失;FSYNC_WAL表示WAL写入之后立马落盘,性能相对最差。目前实现中FSYNC_WAL并没有实现!用户可以根据业务对数据丢失的敏感性在客户端配置相应的持久化策略。

范欣欣

网易杭州研究院技术专家。负责网易内部Hadoop&HBase等组件内核开发运维工作,擅长大数据领域架构设计,性能优化以及问题诊断。 著有《HBase原理与实践》一书。 微信公众号:大数据基建。 邮箱:libisthanks@gmail.com。

在 “数据库事务系列-HBase行级事务模型” 上有 30 条评论

  1. 你好,我看hbase1.2.3版本server端出来put操作的源码,发现它对所有需要put的行都加了读锁,不知道为什么,具体代码在 HRegion的doMiniBatchMutation里面

      1. 你好,是的,我理解的是行锁也分给这行加写锁还是读锁吧,我看put都是加的读锁,也就是它对所有需要更新的row都加了读锁,不应该是加写锁嘛

        1. 行锁不分读写锁的,你说的是读锁是region级别的那个锁吧 那个锁是在region分裂、flush的时候会加写锁 写的时候加读锁

  2. 您好!最近公司项目需要使用HBase存储,在研究HBase性能问题,也看过很多您的博客了,非常感兴趣,也有些问题想请教您,可以把我加入到HBase的讨论群吗?谢谢

  3. 其实笔者一直在这里有个疑惑,还是使用上面例子说明吧。大家都知道,HBase是支持多版本的,那么在数据写入的时候加锁给待写入数据都打上版本信息,此时释放锁。即使之后出现交叉的现象,对于读请求来说,最终读到的数据都是根据最大版本号来的。即使该表只设置了一个版本号,那也可以根据版本号将之前版本的数据清理掉,和交叉不交叉并没有太大关系。不知道大家怎么理解这个问题?有兴趣的可以留言评论!
    —————
    对于你这个例子(两个事务分别写不同的column)来讲是没问题的,但是,如果是两个事务同时往一个column插入,多版本也是有问题的。

    1. 能不能具体讲讲两个事务同时写入同一个column的场景下,多版本会有什么问题?举个例子更好,哈哈哈

      1. 两个写,如果两个版本号 比如 16 17 两个版本, 如果多版本写入的话。 17的版本写成功了 16的失败,要怎么办?

  4. 通过这种结构,每个事务只会产生一个WAL单元。这样就可以保证WAL写入时候的原子性。
    —————
    WAL写入的时候能保证一行数据的原子性了,为什么还需要加写锁?是因为memstore会有并发写的问题吗?

  5. HBase不是只支持行级事务吗,看博主文章中的<>这一小节,似乎多行写事务也支持?
    不是很明白,望赐教

      1. 博主文章中的《如何实现批量写入多行的写写并发》 这一小节不是说到了,对于批量写入/更新的操作,服务端会先获取涉及到的rowkey的行锁吗?这不就是多行事务了吗 ?
        博主说的不支持多行事务,是不是这种Case:当要修改的行不在一个Region上时,HBase就没办法保证多行事务了。

  6. 但这里需要注意的是必须使用两阶段锁协议,即:

    (1) 获取所有待写入(更新)行记录的行锁

    (2) 开始执行写入(更新)操作

    (3) 写入完成之后再统一释放所有行记录的行锁
    ——————
    我理解的这里使用的应该是一次性锁协议,两阶段锁协议并不要求解锁必须放在末尾并且可能有死锁问题。

  7. 范神,有没有比较好的 多线程 并发 锁 的书籍,最好是带有例子说明的,有没有这方面的书推荐,我看了很多书,大部分都是讲解原理,但是很少有用实际例子来说明,所以懂大概原理,却不懂如何实际运用

  8. 哈喽,我看了这篇文章后有些疑惑,是不是分配了序号后,序号较低的写请求还没提交事务的时候,序号较大的就提交不了事务了?不然读请求分配已经完成的最大序号,就可以看到比它低的序号内容了,

  9. 笔者疑惑

    其实笔者一直在这里有个疑惑,还是使用上面例子说明吧。大家都知道,HBase是支持多版本的,那么在数据写入的时候加锁给待写入数据都打上版本信息,此时释放锁。即使之后出现交叉的现象,对于读请求来说,最终读到的数据都是根据最大版本号来的。即使该表只设置了一个版本号,那也可以根据版本号将之前版本的数据清理掉,和交叉不交叉并没有太大关系。不知道大家怎么理解这个问题?有兴趣的可以留言评论!

    会不会是因为默认version是系统时间戳,如果两个写请求时间戳相同,但内容不同,这个时候两个数据的版本信息相同,写入的时候还是可能出现交叉写的问题吧 个人理解

  10. 笔者疑惑

    其实笔者一直在这里有个疑惑,还是使用上面例子说明吧。大家都知道,HBase是支持多版本的,那么在数据写入的时候加锁给待写入数据都打上版本信息,此时释放锁。即使之后出现交叉的现象,对于读请求来说,最终读到的数据都是根据最大版本号来的。即使该表只设置了一个版本号,那也可以根据版本号将之前版本的数据清理掉,和交叉不交叉并没有太大关系。不知道大家怎么理解这个问题?有兴趣的可以留言评论!
    —————————————–
    1、这里是用来协调原子写和非原子写的,在HRegion.getRowLockInternal这个方法中可以看到,原子写获取的是写锁,而非原子写获取的是读锁,即原子写执行时其它写都要等待;
    2、原子写是啥意思,可以看下HTable.mutateRow这个方法,其参数为RowMutations,该类的描述信息:Performs multiple mutations atomically on a single row.Currently {@link Put} and {@link Delete} are supported.

  11. 范神,你好,请教两个问题
    1. 并发写同一个region 数据时, 如果sequenceId 大的先完成,小的还没完成, 这个时候这两个写在hlog 上表现什么形式呢, 另外如果正好出现server挂掉,sequenceId大的数据还会存在吗
    2. 另外事务提交这个动作到底是执行了什么呀

发表评论

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