HBase
一、HBase介绍
1.1 HBase - - 分布式数据库
HBase是Hadoop项目的子项目,其用于存储海量数据的结构化、半结构化和非结构化数据。
sql是单节点式查询数据,有着丰富的索引能力,但对于TB级别的数据却难以应对,而HBase正是对大型表数据的存储和处理。
HBase解决了两大问题:
1、海量数据的存储:单表千万级别的数据,甚至百亿级别。
2、高并发问题
处理方式:
1、海量数据:底层使用hdfs集群存储
2、高并发:多台机器进行处理请求
1.2 HBase结构原理 - - 简述
1、主从架构:Hmaster 和Hregionserver
Hmaster管理regionserver,负责regionserver的负载均衡等;regionserver管理元数据信息等。
2、底层用hdfs存储数据:regionserver将数据存到hdfs集群中
3、整个集群有zooKeeper管理。解决了主节点宕机的问题
-----基于以上,所以使用HBase必须ZooKeeper和Hdfs开启
1.3 数据存储原理
1.4 HBase的安装启动
前提环境,需开启hdfs和zookeeper
上传 解压 更改配置 分发
设置一键启动
启动命令:
start-hbase.sh
stop-hbase.sh
进入客户端命令:
hbase shell
页面端口:16010
1.5 hbase的应用场景
hbase 应用于:1、大表-千万级别
2、高并发
不适用于:1、多维度查询数据
2、做多方面的分析 比如不能做报表
一般用于:做标签数据/用户画像
二、shell命令
通过 hbase shell 命令 进到客户端
1、通用命令
status 查看当前hbase状态
version 查看当前hbase版本
help 查看所有命令--help 'commend' 查看命令的使用方式
whoami 查看用户信息
create_namespace 'myspace' -----创建名称空间
create 'myspace:table1','cf1' ---指定名称空间创建表
2、DDL 表相关命令
create 建表
create 'tb_1','cf1','cf2'
create '表名','列族','列族'...
list ------------------------查看当前nameSpase下的所有表列表
desc 'tb_1' ------------------查看表的结构
scan 'tb_1' ------------------查看表信息
put --向表中插入数据
put 'tb_1','1','cf:id','001'
put '表名','行数','列族:列名','值'
当 '表名','行数','列族:列名', 这个称为key 当key相同是,再插入数据便会覆盖
alter 修改命令
alter 'tb_1' ,NAME=>'cf10' ----增加列族
alter 'tb_1',NAME=>'cf10',VERSION=>'3' ---修改列族的属性,没有修改列族名字的
alter 'tb_1','delete'=>'cf10' -----删除列族
alter 'tb_1',MAX_FILESIZE=>'1233333' -------设置表的最大容量
alter 'tb_1',METHOD=>'table-att-unset',NAME=>'MAX_FILESIZE'--对表的信息进行设置
alter_status ---查看信息同步状态信息
clone_table_schema 'old_table','new_table' ----根据旧表建新表,结构相同
create_namespace 'myspace' -----创建名称空间
create 'myspace:table1','cf1' ---指定名称空间创建表
create 'table_regions','cf',SPLITS=>['k','p','z'] --创建多个region的表
disable 禁用表--禁用后不能执行表的操作,但可以对列族进行操作
disable 'table1'
disable_all 禁用所有表
disable_all 'ta.*' 禁用所有以ta开头的表
enable 开启禁用
enable 'table1'
enable_all
enable 'ta.*'
is_disabled 判断是否被禁用
is_enabled 判断是否不被禁用
drop 删除表,将表彻底删除
drop 'table1'
drop_all
drop 'ta.*'
delete 只能删除单元格
delete 'table1','1','cf1:age'
删除的是当前版本的值,表中仍保留之前版本的值
deleteall 删除整行
deleteall 'table1','1' ---删除行
deleteall 'table1','1','cf1:age'
alter 'table1' 'delete'=>'cf1' 删除列族
exists
exosts 'table1'
get_table 给表名起别名
t=get_table 'table1'
t.scan
list 获得表的列表
list_regions 获得region
list_regions 'table1'
locate_region 获得指定行的region
locate_region 'table1','3'
show_filters 获得所有的过滤命令
DML:
append 追加--在单元格之后继续追加内容
append 'table1','1','cf1:age','0'
1 column=cf1:age, timestamp=1625196340807, value=180---之前
1 column=cf1:age, timestamp=1625210903924, value=1800---之后
count 计算表有多少行
count 'table1'
incr 设置自增
incr 'table1','5','cf1:age',18
get_counter 获得自增的当前值
get_counter 'table1','5','cf1:age'
get
get 'table1','1' ---获取一行数据
get 'table1','1','cf1:age','cf2:job'---获取多个单元格的值
get_splits 获得切点
get_splits 'table1'
put 插入数据
一次写入一个单元格---会造成大量的交互,效率低
解决:缓存一批写一次
将文件从hdfs中 通过MR程序读取出来,然后输出到hbase中,以hfile的形式
scan 查看表的内容,全表检索,数据量大 ,一般不用
truncate 删除表,然后键一个与所删除表结构相同的表
Group name:
snapshots 快照 给表拍摄快照 可以再回到这个快照所对应的状态
clone_snapshot 克隆一个快照 然后创建一个新表
delete_all_snapshot
delete_snapshot
delete_table_snapshots
list_snapshots
list_table_snapshots
restore_snapshot恢复快照
snapshot
手动拆分region----移动region---手动合并region
手动拆分: split 'table','rkoo5'
移动region: move 'regionName','regionServerName'
平衡region分配:balance
手动合并region: merge_region 'regionName','regionName'.....,true
三、数据(表)实际的存储位置
通过访问hdfs网页可以看到数据的真实存储位置:
Hdfs: /hbase/data/名称空间/表/region/列族/数据内容
四、java端操作HBase
创建maven项目 ,添加上各种hdfs、zookeeper和hbase等的依赖
每创建一个maven项目,之前的pmo配置文件都要重新写
4.1 获得连接对象
public class Demo1 {
//获得连接对象
public static void main(String[] args) throws Exception {
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum","linux01:2181,linux02:2181,linux03:2181");
Connection conn = ConnectionFactory.createConnection(conf);
Admin admin = conn.getAdmin();//除了表的一些操作,其他操作基本都可以通过admin来实现
TableName tableName=TableName.valueOf("table1");
Table table = conn.getTable(tableName);//表的一些操作都可以在这里进行执行 追加 添加 删除等操作
}
}
4.2 操作
public static void main(String[] args) throws Exception {
Connection conn = HbaseUtils.getConnection();
Admin admin = conn.getAdmin();
//获取所有的表名
TableName[] tableNames = admin.listTableNames();
for (TableName tableName : tableNames) {
byte[] name = tableName.getName();
System.out.println(new String(name));
}
System.out.println(";;;;;;;;;;;;;;;;;;;;;;;;;;;;;");
//获得所有的名称空间
NamespaceDescriptor[] spaces = admin.listNamespaceDescriptors();
for (NamespaceDescriptor space : spaces) {
String name = space.getName();
System.out.println(name);
}
conn.close();
}
//c创建表--创建多个region的表
public static void main(String[] args) throws Exception {
Connection conn = HbaseUtils.getConnection();
Admin admin = conn.getAdmin();
//列族构造器
ColumnFamilyDescriptorBuilder cbuilder = ColumnFamilyDescriptorBuilder.newBuilder("cf1".getBytes());
//cbuilder.setTimeToLive(100); 设置列族存活时间--时间一到,列族保留,但列族里的行数据全被删除
//cbuilder.setMaxVersions(5); 设置列族的版本
//用列族构造器构建列族描述器
ColumnFamilyDescriptor column = cbuilder.build();
//获得表的构造器
TableDescriptorBuilder tableBuilder = TableDescriptorBuilder.newBuilder(TableName.valueOf("table_new_regions"));
//将列族描述器加到表的构建器中
tableBuilder.setColumnFamily(column);
//获得描述器
TableDescriptor descriptor = tableBuilder.build();
//创建表--预设region的分隔点
byte[][] regions=new byte[][]{"d".getBytes(),"k".getBytes(),"u".getBytes()};
admin.createTable(descriptor,regions);
conn.close();
}
4.3 put数据-三种方式
一:put多行数据
public static void main(String[] args) throws Exception {
Connection conn = HbaseUtils.getConnection();
Table table = conn.getTable(TableName.valueOf("new_1"));
//分别是三行数据
Put put1 = new Put("rk001".getBytes());
Put put2 = new Put("rk002".getBytes());
Put put3 = new Put("rk003".getBytes());
//每一行添加的数据都一样---非字符串类型,要使用Bytes工具进行包装
put1.addColumn("cf1".getBytes(),"name".getBytes(), "zss".getBytes());
put1.addColumn("cf2".getBytes(),"sal".getBytes(), Bytes.toBytes(10000.2000));
put2.addColumn("cf1".getBytes(),"name".getBytes(), "zss".getBytes());
put2.addColumn("cf2".getBytes(),"sal".getBytes(), Bytes.toBytes(10000.2000));
put3.addColumn("cf1".getBytes(),"name".getBytes(), "zss".getBytes());
put3.addColumn("cf2".getBytes(),"sal".getBytes(), Bytes.toBytes(10000.2000));
List<Put> list=new ArrayList<>();
list.add(put1);
list.add(put2);
list.add(put3);
table.put(list);
conn.close();
}
二:缓存式插入数据
public static void main(String[] args) throws Exception {
Connection conn = HbaseUtils.getConnection();
BufferedMutator bm = conn.getBufferedMutator(TableName.valueOf("new_1"));
bm.setWriteBufferPeriodicFlush(2000);//设置2秒刷新一次
/*
* 1、可以设置数据刷新时间
* 2、手动flush
* 3、数据量达到一定量的时候也会自动刷新
* 4、当前节点整体的内存达到阀值也会刷新
*
* */
//分别是三行数据
Put put1 = new Put("rk003".getBytes());
Put put2 = new Put("rk004".getBytes());
Put put3 = new Put("rk005".getBytes());
//每一行添加的数据都一样---非字符串类型,要使用Bytes工具进行包装
put1.addColumn("cf1".getBytes(),"name".getBytes(), "lss".getBytes());
put1.addColumn("cf2".getBytes(),"sal".getBytes(), Bytes.toBytes(10000.2000));
put2.addColumn("cf1".getBytes(),"name".getBytes(), "lss".getBytes());
put2.addColumn("cf2".getBytes(),"sal".getBytes(), Bytes.toBytes(10000.2000));
put3.addColumn("cf1".getBytes(),"name".getBytes(), "lss".getBytes());
put3.addColumn("cf2".getBytes(),"sal".getBytes(), Bytes.toBytes(10000.2000));
List<Put> list=new ArrayList<>();
list.add(put1);
list.add(put2);
list.add(put3);
bm.mutate(list);
//bm.flush();
bm.close();
conn.close();
}
三:导入数据
public static void main(String[] args) {
/*
* 此方法是比较好的方法,前提是在hdfs中有着静态的数据
* 然后将静态数据在hdfs中转为hfile的格式
* 最后将hfile文件的内容直接导入hbase表中
*
* put1:弊端是每次put都要进行一次写数据的流程,比较低效
* put2:缓存一批,再put,虽提升了效率,但当表的数据很大的时候,代码无疑会非常多, 并不适合
* put3:使用直接将数据导入到表中的方法,很是高效,但前提是在hdfs中有着静态数据
* */
/*
* 使用命令将数据插入到表中
* rz将数据上传到linux中
* --linux将数据上传到hdfs中(hdfs dfs -put)
* ---通过命令,将文件转成hfile文件保存在hdfs中
* --再执行命令将hfile文件插入到hbase指定的·表·中
*3 操作的数据在HDFS上
* hbase org.apache.hadoop.hbase.mapreduce.ImportTsv -Dimporttsv.separator=, -Dimporttsv.columns='HBASE_ROW_KEY,cf:name,cf:id,cf:gender,cf:city' -Dimporttsv.bulk.output=/student/output tb_student /student/student.txt
*4 将生成的hfile文件导入到hbase 的指定表中
* hbase org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles /student/output tb_student
* */
}
4.4 读取数据–scan
获取每行数据--获取单元格--从单元格获取行键、列族、属性、值
public static void main(String[] args) throws Exception {
Connection conn = HbaseUtils.getConnection();
Table table = conn.getTable(TableName.valueOf("tb_student"));
Scan scan = new Scan();
//得到所有的行数据
ResultScanner results = table.getScanner(scan);
//遍历每一行数据
for (Result result : results) {
while(result.advance()){
//获得每个单元格
Cell cell = result.current();
//从单元格中再获取每个单元格锁对应的行键、列族、属性、值
byte[] cloneRow = CellUtil.cloneRow(cell);
byte[] cloneFamily = CellUtil.cloneFamily(cell);
byte[] cloneQualifier = CellUtil.cloneQualifier(cell);
byte[] cloneValue = CellUtil.cloneValue(cell);
System.out.println(
new String(cloneRow)+"--"+
new String(cloneFamily)+"--"+
new String(cloneQualifier)+"--"+
new String(cloneValue)
);
}
}
}
4.5 MR式插入数据
数据--MR--导入到hbase的表中 这个就很流批
案例:将movie数据 导入到hbase的表中
//MR阶段
static class MR_Hbase_Mapper extends Mapper<LongWritable, Text,Text,MovieWritable>{
//使用Gson来解析JSON数据
Gson gson=new Gson();
Text k=new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
try{
String line = value.toString();
//使用gson进行解析json数据,将line转为movieBean
MovieWritable movieWritable = gson.fromJson(line, MovieWritable.class);
//设置key
String mid = movieWritable.getMovie();
String timeStamp = movieWritable.getTimeStamp();
//给key定长
String padMid = StringUtils.leftPad(mid, 5, "0");
String padTime = StringUtils.leftPad(timeStamp, 10, "0");
//得到设定好的行键
String rk=padMid+"-"+padTime;
k.set(rk);
//此时输出的key是设置好的行键,value是movieBean
context.write(k,movieWritable);
}catch(Exception e){
//一定要tey--catch 且要抛最大的异常,因为数据会有错误
//防止程序中断
}
}
}
//Reducer阶段 继承的类有所区别 继承TableReducer
static class MR_Hbase_Reducer extends TableReducer<Text,MovieWritable, ImmutableBytesWritable>{
@Override
protected void reduce(Text key, Iterable<MovieWritable> values, Context context) throws IOException, InterruptedException {
String rk = key.toString();
MovieWritable movieBean = values.iterator().next();
//put 给行键 然后把movieBean中的数据都放到put中去
Put put=new Put(rk.getBytes());
put.addColumn("cf".getBytes(),"movie".getBytes(), Bytes.toBytes(movieBean.getMovie()));
put.addColumn("cf".getBytes(),"rate".getBytes(), Bytes.toBytes(movieBean.getRate()));
put.addColumn("cf".getBytes(),"timeStamp".getBytes(), Bytes.toBytes(movieBean.getTimeStamp()));
put.addColumn("cf".getBytes(),"uid".getBytes(), Bytes.toBytes(movieBean.getUid()));
//输出 key为null ;value为put
context.write(null,put);
}
}
public static void main(String[] args) throws Exception {
//得到的conf是hbase和hadoop相联系的一个conf
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum","linux01:2181,linux02:2181,linux03:2181");
Job job = Job.getInstance(conf, "Hbase_Movie");
//设置mapper端
job.setMapperClass(MR_Hbase_Mapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(MovieWritable.class);
//数据的读取位置
FileInputFormat.setInputPaths(job,new Path("E:\\work2\\mrdata\\movie\\input"));
//设置输出到hbase中
TableMapReduceUtil.initTableReducerJob("tb_MR_movie",MR_Hbase_Reducer.class,job);
boolean b = job.waitForCompletion(true);
System.out.println(b);
}
五、HBase原理
5.1 读数据原理
1、客户端想zookeeper请求读取数据
2、zookeeper返回元数据所在的机器RegionServer
3、客户端请求RegionServer获取元数据
4、解析元数据,定位到对应的region
5、每个region都在hdfs中对应一个文件夹,从该文件夹中获取数据
5.1.2 读取数据-布隆过滤器
1、客户端想zookeeper请求读取数据
2、zookeeper返回元数据所在的机器RegionServer
3、客户端请求RegionServer获取元数据
4、解析元数据,定位到对应的region
----
5、三种读取数据的方式:
一:若内存中有,则直接从内存中获取
二:从region的缓存中读取,当从hdfs中获取后,会在region中再缓存一份,供下次更快的读取
三:从hdfs中获取
总结:若内存中没有,则会同时从region的缓存中和hdfs中获取,然后比较,拿到最新更新的数据
当写数据的时候,每次内存的刷新都会在hdfs中的region中生成hfile文件,不断的刷新,便会生成很多的hfile文件,那么读取数据的时候,怎么高效的定位到数据所在的hfile文件呢?
1、布隆过滤器:
在每个hfile文件中都有一小块区域,用于做hfile文件的标签,存放的是字节数组,key是 0-..而value初始全是0。
在put数据的时候,行键取哈希值,对应的map数组中这个hash值的位置的值就由0变为1.
在get数据的时候,根据行键取hash值,然后扫描hfile文件中有这个hash值处为1的hfile 文件。
当数据不在这个hfile文件中的时候,便一定不在这个文件中;也有可能出现哈希碰撞,所以 当取到多个hfile文件的时候,再扫描内容,拿到确定的数据。
在每个hfile文件内存都会流出一小块的存储空间【字节数组】,来记录key是否存在存储的标记信息,存在标记为1,不存在标记为0
2、数据块索引
就算找到了这个hfile文件,这个文件中也有很多行,怎么取到想要的那一行呢?
datdablock中存的是数据kv,在hfile中还有一块区域是数据块索引,每个索引都管着几行数据,这样通过数据块索引可以快速的定位到想要的那一行数据。
3、概念
稀疏索引:一个索引管理者好几个
稠密索引:一 一对应,一个索引对应一个
二级索引,a->A->001 通过a找到A,然后通过A找到对应的001
5.2 写数据原理
1、客户端请求写入数据,
2、zookeeper返回元数据所在的机器RegionServer
3、客户端找到Regionserver获得元数据
4、解析元数据,定位到写的位置region
5、一方面:在region中写到内存中,在内存中进行排序,然后刷写到hdfs中的·对应文件中,最终落入磁盘
另一方面:生成写数据的日志,并将日志传到hdfs中,这样即使传输中断,未来得及刷新,有了日 志也不会造成数据丢失。
*在RegionServer中有名称空间-->表-->region-->cf-->内容
*每个region中会有多个列族,每个列族都对应一个cf-store;每个cf-store中有一个memoryStore
写数据的时候·定位到region·定位到cf·定位到cf-store·定位到内存store(memoryStore)·写到 内存中
*当达到刷新的机制,内存开始溢出·刷新·每刷新一次就会生成一个StoreFile对象
*然后就会在对应的目录生成hfile文件,将内容写到hfile文件中
刷新机制:
1、当数据大小达到阀值(128M)
2、手动刷新
3、达到刷新的预设时间
4、当内存中所有的memoryStore整体达到一定值的时候,为了防止内存溢出,也会刷新
hdfs中不支持随机写和修改数据:
*所有的更新和删除都是写操作
*在每次的操作中都添加一个标签,比如put标签,delete标签
*在取值的时候,会比较标签来执行最新的操作
比如做更新操作,在取值的时候,就回比较两个标签,发现是更新操作,就回返回最新的时 间戳所对应的数据。若最新的操作有delete标签,则认为是删除操作,则取不出值。
后续会将这些文件进行合并,按照最新的时间戳进行合并,合并的时候才是数据真正进行更 新和删除的时候。
比如put一次 又更新一次 又删除一次,则会分别生成三个文件,后续会将这三个文件进行合并,发现最新的时间戳有删除标记,于是便将这三个冗余文件都删除掉。
5.3 合并
1、hfile的合并:小合并
所有的更新和删除数据都是写操作,每次操作都会有更新和删除的标记,应该保留最新的更新,而之前的旧数据就会进行合并、删除---指定列族下的文件的合并。(会保留设定的历史版本数)
2、region的合并:大合并
当执行大量的删除操作时,region所管理的行范围会大量减小,当region所管理的行范围很少的时候,则没必要再维持多个region,于是会进行region的合并,生成一个新的region,将数据复制过来。之后再将旧的region删除。
region级别的合并,要进行大量的IO,网络通信,占用的资源比较多。region合并最好在业务的低峰期。手动合并。
---手动更换region1所在的regionServer:
move '04980472336896c55bd96bae4271a909' , 'linux02,16020,1625467987938'
move 'regionName','regionServerName'
---手动合并:
merge_region '28eea66d36680849e4abdbfb36d25f7a','04980472336896c55bd96bae4271a909'
merger_region 'regionName','regionName','regionName'...,true
5.4 region拆分
拆分region
1、预拆分
shell客户端:create 'table','cf1',SPLITS=>['d','h','p']
java客户端:createTable(,byte二维数组)
2、自动拆分
当插入的数据越来越多,region所管理的行数据越来越多,于是便会进行自动拆分
拆分的大小:256M--2G--6.75G--10G..10G..10G....
3、拆分策略
默认大小
Rk前缀
分隔符
强制拆分:split 'table','rkoo9'
.......
5.5 rowKey的设计
rowKey有很多重要的作用,所以rowKey的设计极为重要:
两个原则:
能够解决热点问题,能够满足查询维度(没法满足多维度查询)
*rowKey要具有唯一性
*要定长:方便排序
*长度不宜过长
5.6 二级索引的设计
针对movie这个数据,想要两个查询维度:movieID和Uid
设置二级索引
mid_time 对应着cell
uid_time 对应着mid_time
根据uid查询的时候,先根据uid找到movieID,然后根据movieID再找到cell数据
具体实现:
实现二级索引:可以是建立两张表,一张表rowKey为mid_time;另一张表为uid_time。但这样无疑浪费了很多的资源,所以可以设置二级索引,也是建立两张表,
不过第二张表的内容为,rowKey--uid_time value---mid_time
这样便极大的节省了资源。
在put数据之前进行拦截,获取单元格的属性,然后如果属性是uid则将其取出作为第二张表的rowKey2,然后再取出mid和time作为roeKey2的值。第一张表正常put数据,rowkey为mid_time值为电影的各种属性。
做拦截功能的类时prePut,即在put之前的操作。