HBase是一个面向海量数据场景,分布式的、多版本、面向列的开源KV数据库。运行在HDFS的基础上,支持PB级别、百万列的数据存储。
只负责各种例如建表、删表、移动Region、合并等操作。他们的共性就是需要跨RegionServer,这些操作由哪个RegionServer都不合适,所有HBase将这些操作放到了Master。
完全由用户指定的一串不重复的字符串。根据字典排序,如果插入HBase的时候rowKey出现碰撞,就会把之前的row更新掉。之前的数据会被放到拉链表里面,需要根据版本号才能查询该被覆盖调的数据。
一个列上可以存储多个版本的单元格,单元格就是数据存储的最小单元,多个版本的值存储在多个单元格里面,多个版本之间用版本号version来区分,所以唯一确定一条数据的表达式rowKey && column family && column && version。如果不写版本号,默认返回是最后一个版本的数据,每一个列或者单元格的值都被赋予了一个时间戳,这个时间戳可以自动生成,也可以用户自定义。
在HBase中,若干列可以组成列族。
HBase会把想用的列族信息尽量放到同一台机器上,如果想让某几列被放到一起,就给他们使用相同的列族。一个表要设置的列族越少越好,因为HBase不希望用户指定太多的列族,虽然HBase是分布式数据库,但是数据分布在同一台机器上数据查询会更快。
HBase中每一行数据都是离散的,因为列族,一行里不同的列会被分配到不同的服务器,行的概念被削弱到一个抽象的存在。如果多个列标记为同一个rowKey,则说明他们是同一行数据。
在HBase中,每一个存储语句都必须精确的写出数据要被存储到哪些单元格,而单元格是由表是由表:列族:行:列来定义,也就是说一行10列的数据要精确的写出数据被存储哪个表哪个列族的哪一行的哪一列,而传统数据库存储语句可以把整行数据一次性写在行语句里。例如(HBase版本:hbase:0.94-adh3u11.7.9):
HTablePool hTablePool;
Configuration conf = HBaseConfiguration.create();
conf.setBoolean(DiamondAddressHelper.DIMAOND_HBASE_UNITIZED, true);
conf.set(DiamondAddressHelper.DIAMOND_HBASE_KEY_NEW, "");
conf.set(DiamondAddressHelper.DIAMOND_HBASE_GROUP, "");
this.hTablePool = new HTablePool(conf, Integer.valueOf(htablePoolMaxSize));
HTableInterface table = this.hTablePool.getTable("表名");
// 指定行rowKey
Put put = new Put(Bytes.toBytes(rowKey));
put.add(Bytes.toBytes("列族"), Bytes.toBytes("列名1"), Bytes.toBytes("列值1"));
put.add(Bytes.toBytes("列族"), Bytes.toBytes("列名2"), Bytes.toBytes("列值2"));
put.add(Bytes.toBytes("列族"), Bytes.toBytes("列名3"), Bytes.toBytes("列值3"));
put.setDurability(Durability.SYNC_WAL);
table.put(put);
table.close();
如下图所示:
默认情况下,每个table起始只有一个Region,随着数据不断写入,Region会自动进行拆分。刚拆分时,两个子Region都位于当前Region Server,但出于负载均衡的考虑,HMaster有可能会将某个Region转移到其他Region Server。
split时机
0.94之前
0.94之后
HBase数据库是否高效与rowKey设计关系密切,概括起来宗旨只有一个,那就是尽可能选择一个可以使你数据均匀分布在集群中的rowKey。
对于频繁写的场景,随机rowKey性能更好;
对于频繁读的场景,有序的rowKey性能更好;
对于时间连续的数据,有序的rowKey方便使用Scan操作。
在对一条记录应用多个修改的时候,最后的改动会覆盖之前的操作,返回给客户端的记录都以最后一次改动的为准。在实际操作中,基于LWW的策略并一定正常,比如多个节点对同一个记录进行修改,如果节点服务的始终不是严格对等的,不一定完全遵循LWW,其实我们在实际业务场景操作幂等性的数据时比较适合使用双主操作。
vector-clock是由Lamport演化而来,Lomport只有每个进程本的本地时间,没有其他进程时间。向量时钟算法利用了向量这种数据结构将全局的各个进程的逻辑时间戳广播给各个进程,每个进程发送事件时都会将当前一直的所有进程时间写入一个向量中。需要说明一下向量时钟是用来检测发现分布式系统中多副本的数据冲突问题,但是他并不解决问题,解决还是通过LWW。
v0-->v1-->v2-->[v1,v2]
v0-->v1-->v1-->[v0,v2]
二级索引的思想:简单理解就是,根据列族的列的值,查出rowkey,再按照rowkey就能很快从hbase查询出数据,我们需要构建出根据列族的列的值,很快查出rowkey的方案
常见的二级索引方案
原表:
row rowkey1 f:nam a
row rowkey2 f:name b
row rowkey3 f:name c
如果我们想根据Id查询,需要再建一张hbase表
row a f:id rowkey
row b f:id rowkey2
row c f:id rowkey3
先通过name找到rowkey,然后通过rowKey找到原表数据,和MySQL的回表操作类似。
1、Coprocessor提供了一种机制可以让开发者直接在RegionServer上运行自定义代码来管理数据。通常我们使用get或者scan来从Hbase中获取数据,使用Filter过滤掉不需要的部分,最后在获得的数据上执行业务逻辑。但是当数据量非常大的时候,这样的方式就会在网络层面上遇到瓶颈。客户端也需要强大的计算能力和足够大的内存来处理这么多的数据,客户端的压力就会大大增加。但是如果使用Coprocessor,就可以将业务代码封装,并在RegionServer上运行,也就是数据在哪里,我们就在哪里跑代码,这样就节省了很大的数据传输的网络开销。
2、Coprocessor有两种:Observer和Endpoint EndPoint主要是做一些计算用的,比如计算一些平均值或者求和等等。而Observer的作用类似于传统关系型数据库的触发器,在一些特定的操作之前或者之后触发。学习过Spring的朋友肯定对AOP不陌生,想象一下AOP是怎么回事,就会很好的理解Observer了。Observer Coprocessor在一个特定的事件发生前或发生后触发。在事件发生前触发的Coprocessor需要重写以pre作为前缀的方法,比如prePut。在事件发生后触发的Coprocessor使用方法以post作为前缀,比如postPut。Observer Coprocessor的使用场景如下:
2.1. 安全性:在执行Get或Put操作前,通过preGet或prePut方法检查是否允许该操作;
2.2. 引用完整性约束:HBase并不直接支持关系型数据库中的引用完整性约束概念,即通常所说的外键。但是我们可以使用Coprocessor增强这种约束。比如根据业务需要,我们每次写入user表的同时也要向user_daily_attendance表中插入一条相应的记录,此时我们可以实现一个Coprocessor,在prePut方法中添加相应的代码实现这种业务需求。
2.3.二级索引:可以使用Coprocessor来维持一个二级索引。正是我们需要的
索引设计思想
我们的需求是找出满足cf1:col2=c22这条记录的cf1:col1的值,实现方法如图,首先根据cf1:col2=c22查找到该记录的行键,然后再通过行健找到对应的cf1:col1的值。其中第二步是很容易实现的,因为Hbase的行键是有索引的,那关键就是第一步,如何通过cf1:col2的值找到它对应的行键。很容易想到建立cf1:col2的映射关系,即将它们提取出来单独放在一张索引表中,原表的值作为索引表的行键,原表的行键作为索引表的值,这就是Hbase的倒排索引的思想。
比如说你现在有一行数据
id name age ….30 个字段
但是你现在搜索,只需要根据 id name age 三个字段来搜索
如果你傻乎乎的往 es 里写入一行数据所有的字段,就会导致说70%的数据是不用来搜索的,结果硬是占据了es机器上的filesystem cache的空间,单挑数据的数据量越大,就会导致 filesystem cahce 能缓存的数据就越少仅仅只是写入 es 中要用来检索的少数几个字段就可以了,比如说,就写入 es id name age 三个字段就可以了,然后你可以把其他的字段数据存在 mysql 里面,我们一般是建议用 es + hbase 的这么一个架构。
hbase 的特点是适用于海量数据的在线存储,就是对 hbase 可以写入海量数据,不要做复杂的搜索,就是做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。
从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id,然后根据 doc id 到 hbase 里去查询每个 doc id 对应的完整的数据,给查出来,再返回给前端。
很多数据库或文件系统都使用 B+ 树作为存储数据的数据结构,但是HBase 却使用的是 LSM(Log-Structured Merge Tree)树。
B+ 树虽然适合在磁盘中存储,并且从原理上来看它的读速度很快。但是它并非总是顺序读写磁盘,例如它的节点进行分裂操作时在内存中会拆成两个新的页表,存储到磁盘上很可能就是不连续的;或者其他更新插入删除等操作,需要循环利用磁盘快,也会造成不连续问题。这也是 HBase 不使用 B+ 树的原因,不进行优化的话随机 I/O 太多,范围查询和大量随机写时尤其明显。
LSM 树在读写之间作出取舍,通过牺牲部分读性能,使用顺序写来大幅提高写性能,因此适合写多读少,以及大规模数据读取的场景。
LSM 树首先在内存中构建一颗有序的小树,随着小树的逐渐增大,达到一定阈值时会 flush 到磁盘上。所以 LSM 树不像 B+ 树一样是一棵完整的大树,一棵 LSM 树就是一个个 B+ 树合起来。多次flush之后会形成多个数据存储文件,后台线程会按照配置自动将多个文件合并成一个,此时多颗小树就会被合并成一棵大树。但是读取时,由于不知道数据在哪棵小树上,因此必须遍历所有小树(所以才说 LSM 牺牲了部分读的性能),每棵小树内部数据是有序的。查询是先查内存中的部分,再去查磁盘上的部分。