概述
BigTableHbase简介Hbase与传统关系型数据库的对比分析 Hbase数据模型
数据模型相关概念数据坐标概念视图物理视图面向列的存储 Hbase的实现原理
Hbase的功能组件表和regionRegion的定位 Hbase运行机制
Hbase系统架构Region服务器工作原理Store工作原理HLog工作原理 Hbase应用方案Hbase编程实践
基本操作(增删改查)Java API
Hbase是针对谷歌BigTable的开源实现,是一个高可靠、高性能、面向列、可伸缩的分布式数据库,主要用来存储非结构化和半结构化的松散数据。Hbase可以支持超大规模数据存储,它可以通过水平扩展的方式,利用廉价计算机集群处理由超过10亿行数据和数百万列元素组成的数据表。 概述 BigTable
BigTable是一个分布式存储系统,利用谷歌提出的MapReduce分布式并行计算模型来处理海量数据,使用谷歌分布式文件系统GFS作为底层数据存储,并采用Chubby提供协同服务管理,可以扩展到PB级别的数据和上千台机器,具备广泛应用性、可扩展性、高性能和高可用性等特点。从2005年4月开始,BigTable已经在谷歌公司的实际生产系统中使用,谷歌的许多项目都存储在BigTable中,包括搜索、地图、财经、打印、社交网站Orkut、视频共享网站YouTube和博客网站Blogger等。这些应用无论在数据量方面(从URL到网页到卫星图像),还是在延迟需求方面(从后端批量处理到实时数据服务),都对BigTable 提出了截然不同的需求。尽管这些应用的需求大不相同,但是BigTable依然能够为所有谷歌产品提供一个灵活的、高性能的解决方案。当用户的资源需求随着时间变化时,只需要简单地往系统中添加机器,就可以实现服务器集群的扩展。
总的来说,BigTable 具备以下特性:支持大规模海量数据、分布式并发数据处理效率极高、易于扩展且支持动态伸缩、适用于廉价设备、适合于读操作不适合写操作。
Hbase简介Hbase是一个高可靠、高性能、面向列、可伸缩的分布式数据库,是谷歌BigTable的开源实现,主要用来存储非结构化和半结构化的松散数据。 Hbase的目标是处理非常庞大的表,可以通过水平扩展的方式,利用廉价计算机集群处理由超过10亿行数据和数百万列元素组成的数据表
Hbase 利用Hadoop MapReduce来处理Hbase中的海量数据,实现高性能计算;利用Zookeeper作为协同服务,实现稳定服务和失败恢复;使用HDFS作为高可靠的底层存储,利用廉价集群提供海量数据存储能力。当然,Hbase也可以直接使用本地文件系统而不用HDFS作为底层数据存储方式,不过,为了提高数据可靠性和系统的健壮性,发挥Hbase处理大数据量等功能,一般都使用HDFS作为Hbase的底层数据存储方式。此外,为了方便在Hbase上进行数据处理,Sqoop为Hbase提供了高效、便捷的RDBMS数据导人功能,Pig和Hive为Hbase提供了高层语言支持。
关系数据库从20世纪70年代发展到今天,已经是一种非常成熟稳定的数据库管理系统,通常具备的功能包括面向磁盘的存储和索引结构、多线程访问、基于锁的同步访问机制、基于日志记录的恢复机制和事务机制等。
但是,随着Web 2.0应用的不断发展,传统的关系数据库已经无法满足Web 2.0 的需求,无论在数据高并发方面,还是在高可扩展性和高可用性方面,传统的关系数据库都显得力不从心,关系数据库的关键特性干完 善的事务机制和高效的查询机制,在Web 2.0时代也成为“鸡肋"。包括Hbase在内的非关系型数据库的出现,有效弥补了传统关系数据库的缺陷,在Web 2.0应用中得到了大量使用。
Hbase与传统的关系数据库的区别主要体现在以下几个方面:
• (1)数据类型:关系数据库采用关系模型,具有丰富的数据类型和存储方式, Hbase则采用了更加简单的数据模型,它把数据存储为未经解释的字符串
• (2)数据操作:关系数据库中包含了丰富的操作,其中会涉及复杂的多表连接。 Hbase操作则不存在复杂的表与表之间的关系,只有简单的插入、查询、删除、清空等,因为Hbase在设计上就避免了复杂的表和表之间的关系
• (3)存储模式:关系数据库是基于行模式存储的。 Hbase是基于列存储的,每个列族都由几个文件保存,不同列族的文件是分离的
• (4)数据索引:关系数据库通常可以针对不同列构建复杂的多个索引,以提高数据访问性能。 Hbase只有一个索引——行键,通过巧妙的设计, Hbase中的所有访问方法,或者通过行键访问,或者通过行键扫描,从而使得整个系统不会慢下来
• (5)数据维护:在关系数据库中,更新操作会用最新的当前值去替换记录中原来的旧值,旧值被覆盖后就不会存在。而在Hbase中执行更新操作时,并不会删除数据旧的版本,而是生成一个新的版本,旧有的版本仍然保留
• (6)可伸缩性:关系数据库很难实现横向扩展,纵向扩展的空间也比较有限。相反, Hbase和BigTable这些分布式数据库就是为了实现灵活的水平扩展而开发的,能够轻易地通过在集群中增加或者减少硬件数量来实现性能的伸缩(横向扩展:用更多的节点支撑更大量的请求。 如成千上万的蚂蚁完成一项搬运工作。 纵向扩展:扩展一个点的能力支撑更大的请求。如利用1个人的能力,如蜘蛛侠逼停火车)
Hbase是一个稀疏、多维度、排序的映射表,这张表的索引是行键、列族、列限定符和时间戳。每个值是一个未经解释的字符串,没有数据类型。用户在表中存储数据,每一行都有一个可排序的行键和任意多的列。表在水平方向由一个或者多个列族组成,一个列族中可以包含任意多个列,同一个列族里面的数据存储在一起。列族支持动态扩展,可以很轻松地添加一个列族或列,无需预先定义列的数量以及类型,所有列均以字符串形式存储,用户需要自行进行数据类型转换。由于同一张表里面的每一行数据都可以有截然不同的列,因此对于整个映射表的每行数据而言开有些列的值就是空的,所以说Hbase是稀疏的。在Hbase中执行更新操作时,并不会删除数据旧的版本,而是生成一个新的版本,旧有的版本仍然保留,Hbase可以对允许保留的版本的数量进行设置。客户端可以选择获取距离某个时间最近的版本,或者一次获取所有版本。如果在查询的时候不提供时间戳,那么会返回距离现在最近的那一个版本的数据,因为在存储的时候,数据会按照时间戳排序。Hbase提供了两种数据版本回收方式:一是保存数据的最后n个版本;二是保存最近一段时间内的版本(如最近7天)。
表:Hbase采用表来组织数据,表由行和列组成,列划分为若干个列族。
行:每个Hbase表都由若干行组成,每个行由行键( Row Key)来标识。访问表中的行只有3种方式:通过单个行键访问;通过一个行键的区间来访问;全表扫描。行键可以是任意字符串(最大长度是64 KB,实际应用中长度一般为10~100字节),在Hbase内部,行键保存为字节数组。存储时,数据按照行键的字典序排序存储。在设计行键时,要充分考虑这个特性,将经常一起读取的行存储在一起。
列族:一个Hbase表被分组成许多“列族"的集合,它是基本的访问控制单元。列族需要在表创建时就定义好,数量不能太多( Hbase的一些缺陷使得列族数量只限于几十个),而且不要频繁修改。存储在一个列族当中的所有数据,通常都属于同一种数据类型,这通常意味着具有更高的压缩率。表中的每个列都归属于某个列族,数据可以被存放到列族的某个列下面,但是在把数据存放到这个列族的某个列下面之前,必须首先创建这个列族。在创建完成一个列族以后,就可以使用同一个列族当中的列。列名都以列族作为前缀。例如,courses:history 和courses:math这两个列都属于courses这个列族。在Hbase中,访问控制、磁盘和内存的使用统计都是在列族层面进行的。实际应用中,我们可以借助列族上的控制权限帮助实现特定的目的。比如,我们可以允许一些应用能够向表中添加新的数据,而另一些应用则只允许浏览数据Hbase 列族还可以被配置成支持不同类型的访问模式。比如,一个列族也可以被设置成放入内存当中,以消耗内存为代价,从而换取更好的响应性能。
列限定符:列族里的数据通过列限定符(或列)来定位。列限定符不用事先定义,也不需要在不同行之间保持一致。 列限定符没有数据类型,总被视为字节数组byte[]。
单元格:在Hbase表中,通过行、列族和列限定符确定一个“单元格”(Cell )。单元格中存储的数据没有数据类型,总被视为字节数组bytel[]。 每个单元格中可以保存一个数据的多个版本, 每个版本对应一个不同的时间戳。
时间戳:每个单元格都保存着同一份数据的多个版本,这些版本采用时间戳进行索引。每次对一个单格执行操作(新建、修改、删除)时,Hbase 都会隐式地自动生成并存储一个时间戳。时间戳一般是64位整型,可以由用户自己赋值(自己生成唯一时间戳可以避免应用程序中出现数据版本冲突),也可以由Hbase 在数据写入时自动赋值。一个单元格的不同版本是根据时间戳降序的顺序进行存储的,这样,最新的版本可以被最先读取。
数据坐标Hbase使用坐标来定位表中的数据,也就是说,每个值都是通过坐标来访问的。对于我们熟悉的关系数据库而言,数据定位可以理解为采用“二维坐标”,即根据行和列就可以确定表中一个具体的值。
Hbase中需要根据行键、列族、列限定符和时间戳来确定一个单元格,因此,可以视为一个“四维坐标”,即[行键, 列族, 列限定符, 时间戳]
在Hbase的概念视图中,一个表可以视为一个稀疏、多维的映射关系。在一个Hbase表的概念视图中,每个行都包含相同的列族,尽管这个行不需要在每个列族里存储数据。
从概念视图层面,Hbase中的每个表是由许多行组成的,但是在物理存储层面,它是采用了基于列的存储方式,而不是像传统关系数据库那样采用基于行的存储方式,这也是Hbase和传统关系数据库的重要区别。上述的概念视图在物理存储的时候,会存成两个小片段,也就是说,这个Hbase表会按照contents和anchor这两个列族分别存放,属于同一个列族的数据保存在一起,同时,和每个列族起存放的还包括行键和时间戳。
简单地说,行式数据库使用NSM ( N-ary Storage Model )存储模型,一个元组(或行)会被连续地存储在磁盘页中,也就是说,数据是一行行被存储的,第一行写入磁盘页后,再继续写人第二行,依此类推。在从磁盘中读取数据时,需要从磁盘中顺序扫描每个元组的完整内容,然后从每个元组中筛选出查询所需要的属性。如果每个元组只有少量属性的值对于查询是有用的,那么NSM就会浪费许多磁盘空间和内存带宽。
列式数据库采用DSM ( Decomposition Storage Model )存储模型,它是在1985年提出来的,目的是最小化无用的IO。DSM采用了不同于NSM的思路,对于采用DSM存储模型的关系数据库而言,DSM会对关系进行垂直分解,并为每个属性分配一个子关系。因此,一个具有n个属性的关系会被分解成n个子关系,每个子关系单独存储,每个子关系只有当其相应的属性被请求时才会被访问。也就是说,DSM是以关系数据库中的属性或列为单位进行存储,关系中多个元组的同一属性值(或同一列值)会被存储在一起,而一个元组中不同属性值则通常会被分别存放于不同的磁盘页中。
行式数据库主要适合于小批量的数据处理,如联机事务型数据处理,我们平时熟悉的Oracle和MySQL等关系数据库都属于行式数据库。列式数据库主要适合于批量数据处理和即时查询( Ad-Hoc Query )。它的优点是:可以降低IO开销,支持大量并发用户查询,其数据处理速度比传统方法快100倍,因为仅需要处理可以回答这些查询的列,而不是分类整理与特定查询无关的数据行;具有较高的数据压缩比,较传统的行式数据库更加有效,甚至能达到5倍的效果。列式数据库主要用于数据挖掘、决策支持和地理信息系统等查询密集型系统中,因为一次查询就可以得出结果,而不必每次都要遍历所有的数据库。所以,列式数据库大多都是应用在人口统计调查、医疗分析等行业中,这种行业需要处理大量的数据统计分析,假如采用行式数据库,势必导致消耗的时间会无限放大。
DSM存储模型的缺陷是:执行连接操作时需要昂贵的元组重构代价,因为一个元组的不同属性被分散到不同磁盘页中存储,当需要一个完整的元组时,就要从多个磁盘页中读取相应字段的值来重新组合得到原来的一个元组。对于联机事务型数据处理而言,需要频繁对一些元组进行修改(如百货商场售出一件衣服后要立即修改库存数据),如果采用DSM存储模型,就会带来高昂的开销。在过去的很多年里,数据库主要应用于联机事务型数据处理。因此,在很长一段时间里,主流商业数据库大都采用了NSM存储模型而不是DSM存储模型。但是,随着市场需求的变化,分析型应用开始发挥着越来越重要的作用,企业需要分析各种经营数据帮助企业制定决策,而对于分析型应用而言,一般数据被存储后不会发生修改(如数据仓库);因此不会涉及昂贵的元组重构代价。所以,从近些年开始,DSM模型开始受到青睐,并且出现了一些采用DSM模型的商业产品和学术研究原型系统,如Sybase IQ、ParAccel 、Sand/DNA Analytics 、Vertica、 InfiniDB 、INFOBright、MonetDB 和LucidDB。类似Sybase IQ和Vertica这些商业化的列式数据库,已经可以很好地满足数据仓库等分析型应用的需求,并且可以获得较高的性能。鉴于DSM存储模型的许多优良特性,Hbase 等非关系型数据库(或称为NoSQL数据库)也吸收借鉴了这种面向列的存储格式。
可以看出,如果严格从关系数据库的角度来看,Hbase并不是一个列式存储的数据库,毕竟Hbase是以列族为单位进行分解(列族当中可以包含多个列),而不是每个列都单独存储,但是Hbase借鉴和利用了磁盘上的这种列存储格式,所以,从这个角度来说,Hbase可以被视为列式数据库。
Hbase的实现原理 Hbase的功能组件Hbase的实现包括3个主要的功能组件:库函数,链接到每个客户端;一个Master主服务器;许多个Region服务器。Region服务器负责存储和维护分配给自己的Region,处理来自客户端的读写请求。主服务器Master负责管理和维护Hbase表的分区信息,比如,一个表被分成了哪些Region,每个Region被存放在哪台Region 服务器上,同时也负责维护Region服务器列表。因此,如果Master主服务器死机,那么整个系统都会无效。Master 会实时监测集群中的Region服务器,把特定的Region分配到可用的Region服务器上,并确保整个集群内部不同Region服务器之间的负载均衡,当某个Region服务器因出现故障而失效时, Master会把该故障服务器上存储的Region重新分配给其他可用的Region服务器。除此以外,Master还处理模式变化,如表和列族的创建。
客户端并不是直接从Master主服务器上读取数据,而是在获得Region的存储位置信息后,直接从Region服务器上读取数据。尤其需要指出的是,Hbase客户端并不依赖于Master而是借助于Zookeeper来获Region的位置信息的,所以大多数客户端从来不和主服务器Master通信,这种设计方式使Master的负载很小。
表和region在一个Hbase中,存储了许多表。对于每个Hbase表而言,表中的行是根据行键的值的字典序进行维护的,表中包含的行的数量可能非常庞大,无法存储在一台机器上,需要分布存储到多台机器上。因此,需要根据行键的值对表中的行进行分区,每个行区间构成一个分区,被称为"Region”,包含了位于某个值域区间内的所有数据,它是负载均衡和数据分发的基本单位,这些Region会被分发到不同的Region服务器上。
初始时,每个表只包含一个Region,随着数据的不断插入,Region会持续增大,当一个Region中包含的行数量达到一个阈值时,就会被自动等分成两个新的Region。随着表中行的数量继续增加,就会分裂出越来越多的Region。
每个Region的默认大小是1GB到2GB,是Hbase中负载均衡和数据分发的基本单位。Master主服务器会把不同的Region分配到不同的Region服务器上,但是同一个Region是不会被拆分到多个Region服务器上的。每个Region服务器负责管理一个Region集合,通常在每个Region服务器上会放置10~1000个Region。
一个Hbase的表可能非常庞大,会被分裂成很多个Region,这些Region 被分发到不同的Region服务器上。因此,必须设计相应的Region定位机制,保证客户端知道到哪里可以找到自己所需要的数据。每个Region都有一个RegionID来标识它的唯一性, 这样,一个Region标识符就可以表示成“表名+开始主键+RegionID"。
有了Region 标识符,就可以唯一标识每个Region。 为了定位每个Region所在的位置,就可以构建一张映射表,映射表的每个条目(或每行)包含两项内容,一个是Region标识符,另一个是Region服务器标识,这个条目就表示Region和Region服务器之间的对应关系,从而就可以知道某个Region被保存在哪个Region服务器中。这个映射表包含了关于Region的元数据(即Region和Region服务器之间的对应关系),因此也被称为“元数据表”,又名“.meta.表”。
当一个Hbase表中的Region数量非常庞大的时候,.meta.表的条目就会非常多,一个服务器保存不下,也需要分区存储到不同的服务器上,因此.meta.表也会被分裂成多个Region,这时,为了定位这些Region,就需要再构建一个新的映射表,记录所有元数据的具体位置,这个新的映射表就是“根数据表”,又名“-ROOT-表"。; -ROOT-表是不能被分割的,永远只存在一个Region用于存放-ROOT-表,因此这个用来存放-ROOT-表的唯一个Region,它的名字是在程序中被写死的,Master 主服务器永远知道它的位置。
综上所述,Hbase使用类似B+树的三层结构来保存Region位置信息
为了加快访问速度, .meta.表的全部Region都会被保存在内存中。假设.meta.表的每行(一个映射条目)在内存中大约占用1KB,并且每个Region限制为128MB,那么,上面的三层结构可以保存的用户数据表的Region数目的计算方法是:
(-ROOT-表能够寻址的.meta.表的Region个数)×(每个.meta.表的Region可以寻址的用户数据表的Region个数)
一个-ROOT-表最多只能有一个Region,也就是最多只能有128MB,按照每行(一个映射条目)占用1KB内存计算, 128MB空间可以容纳128MB/1KB=217行,也就是说,一个-ROOT-表可以寻址217个.meta.表的Region。
同理,每个.meta.表的 Region可以寻址的用户数据表的Region个数是128MB/1KB=217。
最终,三层结构可以保存的Region数目是(128MB/1KB) × (128MB/1KB)= 234个Region
客户端访问用户数据之前,需要首先访问Zookeeper,获取-ROOT表的位置信息,然后访问-ROOT-表,获得.meta.表的信息,接着访问.meta.表,找到所需的Region具体位于哪个Region服务器,最后才会到该Region服务器读取数据。该过程需要多次网络操作,为了加速寻址过程,一般会在客户端做缓存,把查询过的位置信息缓存起来,这样以后访问相同的数据时,就可以直接从客户端缓存中获取Region的位置信息,而不需要每次都经历一个“三级寻址”过程。
需要注意的是,随着Hbase中表的不断更新,Region 的位置信息可能会发生变化,但是客户端缓存并不
会自己检测Region位置信息是否失效,而是在需要访问数据时,从缓存中获取Region位置信息却发现不存在的时候,才会判断出缓存失效,这时,就需要再次经历上述的“三级寻址”过程,重新获取最新的Region位置信息去访问数据,并用最新的Region 位置信息替换缓存中失效的信息。
客户端:客户端包含访问Hbase的接口,同时在缓存中维护着已经访问过的Region位置信息,用来加快后续数据访问过程。Hbase客户端使用Hbase的RPC机制与Master和Region服务器进行通信。其中,对于管理类操作,客户端与Master进行RPC;而对于数据读写类操作,客户端则会与Region服务器进行RPC。
Zookeeper服务器:Zookeeper服务器并非一台单一的机器, 可能是由多台机器构成的集群来提供稳定可靠的协同服务。Zookeeper能够很容易地实现集群管理的功能,如果有多台服务器组成一个服务器集群,那么必须有一个“总管”知道当前集群中每台机器的服务状态,一旦某台机器不能提供服务,集群中其他机器必须知道,从而做出调整重新分配服务策略。同样,当增加集群的服务能力时,就会增加一台或多台服务器,同样也必须让“总管”知道。在Hbase服务器集群中,包含了一个Master和多个Region服务器,Master就是这个Hbase集群的“总管”,它必须知道Region服务器的状态。Zookeeper 就可以轻松做到这一点,每个Region服务器都需要到Zookeeper中进行注册,Zookeeper会实时监控每个Region服务器的状态并通知给Master,这样,Master就可以通过Zookeeper随时感知到各个Region服务器的工作状态。Zookeeper不仅能够帮助维护当前的集群中机器的服务状态,而且能够帮助选出一个“总管",让这个总管来管理集群。Hbase 中可以启动多个Master,但是Zookeeper可以帮助选举出一个Master作为集群的总管,并保证在任何时刻总有唯一个Master 在运行,这就避免了Master的“单点失效”问题。Zookeeper中保存了-ROOT表的地址和Master的地址,客户端可以通过访问Zookeeper 获得-ROOT-表的地址,并最终通过“三级寻址”找到所需的数据。Zookeeper中还存储了Hbase 的模式,包括有哪些表,每个表有哪些列族。
Master:主服务器Master主要负责表和Region的管理工作:
– 管理用户对表的增加、删除、修改、查询等操作
– 实现不同Region服务器之间的负载均衡
– 在Region分裂或合并后,负责重新调整Region的分布
– 对发生故障失效的Region服务器上的Region进行迁移
Region服务器:Region服务器是Hbase中最核心的模块,负责维护分配给自己的Region,并响应用户的读写请求
Region服务器工作原理 Region 服务器是Hbase中最核心的模块,下图描述了Region服务器向HDFS文件系统中读写数据的基本原理,从图中可以看出,Region服务器内部管理了一系列Region对象和一个HLog文件,其中HLog是磁盘上面的记录文件,它记录着所有的更新操作。每个Region对象又是由多个Store 组成的,每个Store对应了表中的一个列族的存储。每个Store又包含了一个MemStore和若干个StoreFile,其中,MemStore是在内存中的缓存,保存最近更新的数据; StoreFile 是磁盘中的文件,这些文件都是B树结构的,方便快速读取。StoreFile 在底层的实现方式是HDFS文件系统的HFile, HFile的数据块通常采用压缩方式存储压缩之后可以大大减少网络I/O和磁盘IO。
用户读写数据过程:
•用户写入数据时,被分配到相应Region服务器去执行
•用户数据首先被写入到MemStore和Hlog中
•只有当操作写入Hlog之后, commit()调用才会将其返回给客户端
•当用户读取数据时, Region服务器会首先访问MemStore缓存,如果找不到,再去磁盘上面的StoreFile中寻找
缓存的刷新:
•系统会周期性地把MemStore缓存里的内容刷写到磁盘的StoreFile文件中,清空缓存,并在Hlog里面写入一个标记
•每次刷写都生成一个新的StoreFile文件,因此,每个Store包含多个StoreFile文件
•每个Region服务器都有一个自己的HLog 文件,每次启动都检查该文件,确认最近一次执行缓存刷新操作之后是否发生新的写入操作;如果发现更新,则先写入MemStore,再刷写到StoreFile,最后删除旧的Hlog文件,开始为用户提供服务
StoreFile的合并
•每次刷写都生成一个新的StoreFile,数量太多,影响查找速度
•调用Store.compact()把多个合并成一个
•合并操作比较耗费资源,只有数量达到一个阈值才启动合并
Store是Region服务器的核心
多个StoreFile合并成一个
单个StoreFile过大时,又触发分裂操作, 1个父Region被分裂成两个子Region
在分布式环境下,必须要考虑到系统出错的情形,比如当Region服务器发生故障时, MemStore缓存中的数据(还没有被写入文件)会全部丢失。因此,Hbase采用HLog来保证系统发生故障时能够恢复到正确的状态。
Hbase系统为每个Region服务器配置了一个HLog文件,它是一种预写式日志( Write AheadLog),也就是说,用户更新数据必须首先被记入日志后才能写人MemStore缓存,并且直到MemStore缓存内容对应的日志已经被写入磁盘之后,该缓存内容才会被刷新写入磁盘。
Zookeeper会实时监测每个Region服务器的状态,当某个Region服务器发生故障时,Zookeeper会通知Master。Master首先会处理该故障Region服务器上面遗留的HLog文件,由于一个Region服务器上面可能会维护着多个Region对象,这些Region对象共用一个HLog文件,因此这个遗留的HLog 文件中包含了来自多个Region对象的日志记录。系统会根据每条日志记录所属的Region对象对HLog数据进行拆分,分别放到相应Region对象的目录下,然后再将失效的Region重新分配到可用的Region服务器中,并把与该Region对象相关的HLog日志记录也发送给相应的Region 服务器。Region 服务器领取到分配给自己的Region对象以及与之相关的HLog日志记录以后,会重新做一遍日志记录中的各种操作,把日志记录中的数据写人MemStore缓存,然后刷新到磁盘的StoreFile 文件中,完成数据恢复。
需要特别指出的是,Hbase系统中,每个Region 服务器只需要维护一个HLog文件,所有Region对象共用一个HLog,而不是每个Region 使用一个HLog。在这种Region对象共用一个HLog的方式中,多个Region对象的更新操作所发生的日志修改,只需要不断把日志记录追加到单个日志文件中,而不需要同时打开、写入到多个日志文件中,因此可以减少磁盘寻址次数,提高对表的写操作性能。这种方式的缺点是,如果一个 Region服务器发生故障,为了恢复其上的Region对象,需要将Region服务器上的HLog按照其所属的Region对象进行拆分,然后分发到其他Region服务器上执行恢复操作。
Hbase应用方案Hbase实际应用中的性能优化方法:
行键是按照字典序存储,因此,设计行键时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。举个例子:如果最近写入Hbase表中的数据是最可能被访问的,可以考虑将时间戳作为行键的一部分,由于是字典序排序,所以可以使用Long.MAX_VALUE - timestamp作为行键,这样能保证新写入的数据在读取时可以被快速命中。
创建表的时候,可以通过HColumnDescriptor.setInMemory(true)将表放到Region服务器的缓存中,保证在读取的时候被cache命中。
创建表的时候,可以通过HColumnDescriptor.setMaxVersions(int maxVersions)设置表中数据的最大版本,如果只需要保存最新版本的数据,那么可以设置setMaxVersions(1)。
创建表的时候,可以通过HColumnDescriptor.setTimeToLive(int timeToLive)设置表中数据的存储生命期,过期数据将自动被删除,例如如果只需要存储最近两天的数据,那么可以设置setTimeToLive(2 * 24 * 60 * 60)。
Hbase编程实践 基本操作(增删改查)Hbase中用create命令创建表,具体如下:
create 'student','Sname','Ssex','Sage','Sdept','course'
此时,即创建了一个“student”表,属性有:Sname,Ssex,Sage,Sdept,course。因为Hbase的表中会有一个系统默认的属性作为行键,无需自行创建,默认为put命令操作中表名后第一个数据。创建完“student”表后,可通过describe命令查看“student”表的基本信息。命令执行截图如下:
Hbase中用put命令添加数据,注意:一次只能为一个表的一行数据的一个列,也就是一个单元格添加一个数据,所以直接用shell命令插入数据效率很低,在实际应用中,一般都是利用编程操作数据。
put 'student','95001','Sname','LiYing'
命令执行截图如下,即为student表添加了学号为95001,名字为LiYing的一行数据,其行键为95001。
put 'student','95001','course:math','80'
命令执行截图如下,即为95001行下的course列族的math列添加了一个数据。
在Hbase中用delete以及deleteall命令进行删除数据操作,它们的区别是:1、delete用于删除一个数据,是put的反向操作;2、deleteall操作用于删除一行数据。
delete 'student','95001','Ssex'
命令执行截图如下, 即删除了student表中95001行下的Ssex列的所有数据。
deleteall 'student','95001'
命令执行截图如下,即删除了student表中的95001行的全部数据。
Hbase中有两个用于查看数据的命令:1、get命令,用于查看表的某一行数据;2、scan命令用于查看某个表的全部数据
get 'student','95001'
scan 'student'
删除表有两步,第一步先让该表不可用,第二步删除表。
disable 'student' drop 'student'
查询表的历史版本,需要两步。
1、在创建表的时候,指定保存的版本数(假设指定为5)
create 'teacher',{NAME=>'username',VERSIONS=>5}
2、插入数据然后更新数据,使其产生历史版本数据,注意:这里插入数据和更新数据都是用put命令
put 'teacher','91001','username','Mary'put 'teacher','91001','username','Mary1'put 'teacher','91001','username','Mary2'put 'teacher','91001','username','Mary3'put 'teacher','91001','username','Mary4' put 'teacher','91001','username','Mary5'
3、查询时,指定查询的历史版本数。默认会查询出最新的数据。(有效取值为1到5)
get 'teacher','91001',{COLUMN=>'username',VERSIONS=>5}
Java APIimport org.apache.hadoop.conf.Configuration;import org.apache.hadoop.hbase.*;import org.apache.hadoop.hbase.client.*;import org.apache.hadoop.hbase.util.Bytes; import java.io.IOException;public class ExampleForHbase { public static Configuration configuration; public static Connection connection; public static Admin admin; public static void main(String[] args)throws IOException{ init(); createTable("student",new String[]{"score"}); insertData("student","zhangsan","score","English","69"); insertData("student","zhangsan","score","Math","86"); insertData("student","zhangsan","score","Computer","77"); getData("student", "zhangsan", "score","English"); close(); } public static void init(){ configuration = HbaseConfiguration.create(); configuration.set("hbase.rootdir","hdfs://localhost:9000/hbase"); try{ connection = ConnectionFactory.createConnection(configuration); admin = connection.getAdmin(); }catch (IOException e){ e.printStackTrace(); } } public static void close(){ try{ if(admin != null){ admin.close(); } if(null != connection){ connection.close(); } }catch (IOException e){ e.printStackTrace(); } } public static void createTable(String myTableName,String[] colFamily) throws IOException { TableName tableName = TableName.valueOf(myTableName); if(admin.tableExists(tableName)){ System.out.println("talbe is exists!"); }else { TableDescriptorBuilder tableDescriptor = TableDescriptorBuilder.newBuilder(tableName); for(String str:colFamily){ ColumnFamilyDescriptor family = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(str)).build(); tableDescriptor.setColumnFamily(family); } admin.createTable(tableDescriptor.build()); } } public static void insertData(String tableName,String rowKey,String colFamily,String col,String val) throws IOException { Table table = connection.getTable(TableName.valueOf(tableName)); Put put = new Put(rowKey.getBytes()); put.addColumn(colFamily.getBytes(),col.getBytes(), val.getBytes()); table.put(put); table.close(); } public static void getData(String tableName,String rowKey,String colFamily, String col)throws IOException{ Table table = connection.getTable(TableName.valueOf(tableName)); Get get = new Get(rowKey.getBytes()); get.addColumn(colFamily.getBytes(),col.getBytes()); Result result = table.get(get); System.out.println(new String(result.getValue(colFamily.getBytes(),col==null?null:col.getBytes()))); table.close(); }}