HBase 是列族式数据库。列族是也就是说建表的基本单位是列族,是表的 schema 的一部分(而列不是)。一个列族由多个列构成,列名都以列族名作为前缀。例如java:spring
,java:netty
都属于java
这个列族
1.HBase Table 概念视图
Table = RowKey + Family + Column + Timestamp + Value
表 = 行键 + 列族 + 列 + 时间戳 + 具体值。
其中,时间戳,对于一行数据来说,它如果有多个版本,就对应有 TimeStamp 不同的多行
下面,我们在 HBase 中实际创建一个表,并添加几条数据,看看表实际是什样子的:
create_namespace 'test'
create 'test:user', {NAME => 'b', VERSIONS => '3', TTL => '2147483647', 'BLOOMFILTER' => 'ROW'},
{NAME => 'o', VERSIONS => '3', TTL => '2147483647', 'BLOOMFILTER' => 'ROW'}
put 'test:user','1', 'b:name', 'zhangsan'
put 'test:user','1', 'o:phone', '12345678919'
put 'test:user','2', 'b:name', 'lisi'
表扫描结果如下:
可以看到,对于每一行数都是 colunm=Family:Column,然后是时间戳和 value,和我们上面说的 HBase 表视图一致。另外,在多条数据时,是通过 RowKey 不同来区分。
如何定位一个数据?
(Table,RowKey,Family,Column,Timestamp)=> value
构建 RowKey 的一点思路
RowKey 基本要求:
- 根据 HBase 的表视图,可以看到,RowKey 在表中必须是唯一的
- 根据定位一个数据的流程,可以看到,我们在后面查询时还要用到 RowKey,所以 RowKey 要是可复原的
那我们在开发中应该如何构建 RowKey 呢?RowKey = 数据标识(比如 UserId)+ 随机序列(比如时间戳)
- 随机序列保证了唯一性;
- 数据标识保证了可复原,我们在查询时通过前缀匹配就行了
return new StringBuilder(
String.valueOf(user.getUserId())).reverse().toString() // 数据标识
+ (Long.MAX_VALUE - System.currentTimeMillis()); // 随机序列
PS:HBase 提供了许多 RowKey 扫描的匹配算法,除了列前缀匹配还有列范围匹配等等…所以,我们还可以在 RowKey 中保存一些别的信息,起到 MySQL 中 where 的作用。
2.列族属性
HBase Table 的每个列族都可以设置 VERSION,TTL、BLOOMFLTER 等很多属性;如果我们不设置,HBase 会填充默认值:
NAME => 'b', # 列族名
BLOOMFILTER => 'ROW', # 布隆过滤器
VERSIONS => '3', # 版本数量
IN_MEMORY => 'false', # 激进缓存
KEEP_DELETED_CELLS => 'FALSE', # 保留删除的单元格
DATA_BLOCK_ENCODING => 'NONE', # 数据块编码
TTL => 'FOREVER', # 存活时间
COMPRESSION => 'GZ', # 压缩
MIN_VERSIONS => '0', # 最小版本数
BLOCKCACHE => 'true', # 块缓存
BLOCKSIZE => '65536', # 数据块大小
REPLICATION_SCOPE => '0' # 复制范围
我们下面把 VERSIONS,TTL,BLOOMFILTER 这几个我们经常设置的列族属性着重说一下:
1)VERSIONS:版本数量
HBase 一切操作均为更新,Hbase Put 操作不会去覆盖一个值,只会在后面追加写,用时间戳(版本号)来区分,HBase 版本维度按递减顺序存储,以便在从存储文件读取时,首先找到最近的值;
PS:0.96版本默认是3个, 0.98版本之后是1, 要根据业务来划分,版本是历史记录,版本增多意味空间消耗。
下面我们来看个 VERSIONS 的例子。我们还是用上面创建的 test:user 表(VERSIONS=3)
1.在原来的基础上再增加两条数据:
put 'test:user','1', 'b:name', 'wangwu'
put 'test:user','1', 'b:name', 'zhaoliu'
2.获取这三个版本的数据(注:这里要用 get,因为 scan 是获取多行,所以只会拿到最新版本的数据)
get 'test:user', '1', {COLUMN => 'b:name', VERSIONS => 3} # 注:建表时设置的 3,即使你这写个 4,也只能返回三行数据
可以看到,这 b:name 的这三个数据都出来了。
3.我们再新增/更新一条数据
put 'test:user', '1', 'b:name', 'Jack' # 这里既可以理解成新增,也可以理解成更新
4.删除掉 b:name=zhaoliu(时间戳是 1611678810121 )
delete 'test:user', '1', 'b:name', 1611678810121 # 注:如果不指定时间戳,删除的就是 Jack
可以看到,zhangsan 又回来了。
同样的,Hbase Delete 操作也不是真正删除了记录,而是放置了一个墓碑标记,过早的版本会在执行 Major Compaction 时真正删除。
2)TTL:存活时间
TTL 全称是 Time To Live,ColumnFamilies 可以设置 TTL(单位是s),HBase 会自动检查 TTL 值是否达到上限,如果 TTL 达到上限后自动删除行。当然真正删除是在 Major Compaction 过程中执行的。
PS:数据过期时间,一般都设置成 2147483647(s),表示永久
3)BLOOMFILTER:布隆过滤器
布隆过滤器用自己的算法,实现了快速的检索一个元素是否在一个较大的元素列表之中。
它的基本思想是:当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点,把它们置为1;检索时,只要看看这些点是不是都是1 就(大约)知道集合中有没有它了——如果这些点有任何一个0,则被检元素一定不在,如果都是1,则被检元素很可能在。
它的优点是空间效率和查询时间都远远超过一般的算法;缺点是有一定的误识别率和删除困难使用了Hash算法,必然会存在极限巧合下的 hash 碰撞,会将不存在的数据认为是存在的。但是存在的数据一定是可以正确判断的。
PS:关于如何实现一个布隆过滤器可以参考我的这篇文章…
HBase 中的 BloomFilter 主要用来过滤不存在待检索 RowKey 或者 Row-Col 的 HFile 文件,避免无用的 IO 操作。它可以判断 HFile 文件中是否可能存在待检索的KV,如果不存在,就可以不用消耗 IO 打开文件进行 seek。通过设置 BloomFilter 可以提升随机读写的性能。
BloomFilter 是一个列族级别的配置属性,如果在表中设置了 BloomFilter,那么 HBase 会在生成 StoreFile 时包含一份 BloomFilter 结构的数据,称其为 MetaBlock ,和 DataBlock (真实KeyValue数据)一起由 LRUBlockCache 维护。所以开启 BloomFilter 会有一定的存储即内存 Cache 的开销。
BloomFilter 取值有两个,ROW 和 ROWCOL,需要根据业务来确定具体使用哪种。
- 如果业务大多数随机查询时仅仅使用 row 作为查询条件,BloomFilter 设置为 row;
- 如果大多数随机查询使用 row+cf 作为查询条件,BloomFilter 需要设置为 rowcol;
- 如果不确定查询类型,建议设置为 row。
3.数据存储原型
HBase 采用的时列式存储。先来看张图对比一下行式存储和列式存储(注:图片出自这篇博客):
MySQL 是行式存储,HBase 是列式存储
- 行式存储:
- 维护大量索引,
- 存储成本高,不能做到线性扩展
- 随机读效率高
- 对支持事务处理能力支持的比较好
- => 情景:数据量不大
- 维护大量索引,
- 列式存储
- 同一列数据相似,利于对数据压缩,存储成本可以降低
- 列数据分开存储,利于并行查询
- => 情景:大数据处理
HBase 底层实现是两级 SortedMap:
- 先按照 RowKey 进行排序
- 再按照 Column(列名称)排序