介绍
HBase 基于 Google 的 BigTable 论文而来,是一个分布式海量列式非关系型数据库系统, 可以提供 超大规模数据集的实时随机读写 。
特点
海量存储: 底层基于 HDFS 存储海量数据列式存储: HBase 表的数据是基于列族进行存储的,一个列族包含若干列极易扩展: 底层依赖 HDFS ,当磁盘空间不足的时候,只需要动态增加 DataNode 服务节点就可以高并发: 支持高并发的读写请求(聚合查询不适合hbase,考虑clickhouse)稀疏: 稀疏主要是针对 HBase 列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的。数据的多版本: HBase 表中的数据可以有多个版本值,默认情况下是根据版本号去区分,版本号就是插入数据的时间戳数据类型单一: 所有的数据在 HBase 中是以字节数组进行存储
数据模型
架构
Zookeeper
- 实现了HMaster的高可用
- 保存了HBase的元数据信息,是所有HBase表的寻址入口
- 对HMaster和HRegionServer实现了监控
HMaster ( Master )
- 为HRegionServer分配Region
- 维护整个集群的负载均衡
- 维护集群的元数据信息
- 发现失效的Region,并将失效的Region分配到正常的HRegionServer上
HRegionServer ( RegionServer )
- 负责管理Region
- 接受客户端的读写数据请求
- 切分在运行过程中变大的Region
Region
- 每个HRegion由多个Store构成,
- 每个Store保存一个列族(Columns Family),表有几个列族,则有几个Store,
- 每个Store由一个MemStore和多个StoreFile组成,MemStore是Store在内存中的内容,写到文件后就是StoreFile。StoreFile底层是以HFile的格式保存。
安装配置
下载安装包 : http://archive.apache.org/dist/hbase/1.3.1/tar -zxvf hbase-1.3.1-bin.tar.gz -C /opt/lxq/servers需要把 hadoop 中的配置 core-site.xml 、 hdfs-site.xml 拷贝到 hbase 安装目录下的 conf 文件夹中ln -s /opt/lxq/servers/hadoop-2.9.2/etc/hadoop/core-site.xml/opt/lxq/servers/hbase-1.3.1/conf/core-site.xmlln -s /opt/lxq/servers/hadoop-2.9.2/etc/hadoop/hdfs-site.xml/opt/lxq/servers/hbase-1.3.1/conf/hdfs-site.xml修改 hbase-env.sh# 添加 java 环境变量export JAVA_HOME=/opt/module/jdk1.8.0_231# 指定使用外部的 zk 集群export HBASE_MANAGES_ZK=FALSE改 hbase-site.xml<configuration><!-- 指定 hbase 在 HDFS 上存储的路径 --><property><name>hbase.rootdir</name><value>hdfs://linux121:9000/hbase</value></property><!-- 指定 hbase 是分布式的 --><property><name>hbase.cluster.distributed</name><value>true</value></property><!-- 指定 zk 的地址,多个用 “,” 分割 --><property><name>hbase.zookeeper.quorum</name><value>linux121:2181,linux122:2181,linux123:2181</value></property></configuration>修改 regionservers 文件# 指定 regionserver 节点linux121linux122linux123hbase 的 conf 目录下创建文件 backup-masters (Standby Master),内容如下:linux122vim /etc/profileexport HBASE_HOME=/opt/lxq/servers/hbase-1.3.1export PATH=$PATH:$HBASE_HOME/binsource /etc/profile分发hbase到其它节点 不知道rsync-script的,去看我hadoop文章rsync-script hbase-1.3.1前提条件:先启动 hadoop 和 zk 集群启动 HBase : start-hbase.sh停止 HBase : stop-hbase.sh启动好 HBase 集群之后,可以访问地址: HMaster 的主机名 :16010
基操
进入 Hbase 客户端命令操作界面:hbase shellhbase ( main ) : 001 : 0 > help# 有哪些表hbase ( main ) : 006 : 0 > list# 创表,包含两个列族hbase ( main ) : 001 : 0 > create 'lxq' , 'base_info' , 'extra_info'或者 ( Hbase 建表必须指定列族信息 )create 'lxq' , { NAME => 'base_info' , VERSIONS => '3' },{ NAME => 'extra_info' , VERSIONS => '3' }VERSIONS 是指此单元格内的数据可以保留最近的 3 个版本put 'lxq' , 'rk1' , 'base_info:name' , '赖晓期'hbase ( main ) : 008 : 0 > get 'lxq' , 'rk1' , 'base_info:name' , 'base_info:age'或者get 'lxq' , 'rk1' , {COLUMN = > [ 'base_info:name' , 'extra_info:address' ]}hbase ( main ) : 001 : 0 > get 'lxq' , 'rk1' , { FILTER => "ValueFilter(=, 'binary:赖晓期')" }# 模糊查询 列标示符中含有 a 的信息hbase ( main ) : 001 : 0 > get 'lxq' , 'rk1' , { FILTER => "(QualifierFilter(=,'substring:a'))" }hbase ( main ) : 000 : 0 > scan 'lxq'hbase ( main ) : 002 : 0 > scan 'lxq' , { COLUMNS => 'base_info' , RAW => true , VERSIONS => 3 }## Scan 时可以设置是否开启 Raw 模式 , 开启 Raw 模式会返回包括已添加删除标记但是未实际删除的数据## VERSIONS 指定查询的最大版本数# 查询 lxq 表中列族为 base_info 和 extra_info 且列标示符中含有a字符的信息hbase ( main ) : 001 : 0 > scan 'lxq' , { COLUMNS => [ 'base_info' , 'extra_info' ], FILTER => "(QualifierFilter(=,'substring:a'))" }# 查询lxq 表中列族为 base_info , rk 范围是 [rk1, rk3) 的数据( rowkey 底层存储是字典序) 按rowkey 顺序存储。hbase ( main ) : 001 : 0 > scan 'lxq' , { COLUMNS => 'base_info' , STARTROW => 'rk1' , ENDROW => 'rk3' }# 查询 lxq 表中 row key 以 rk 字符开头的hbase ( main ) : 001 : 0 > scan 'lxq' ,{ FILTER => "PrefixFilter('rk')" }更新操作同插入操作一模一样,只不过有数据就更新,没数据就添加hbase ( main ) : 030 : 0 > put 'lxq' , 'rk1' , 'base_info:name' , 'liang'# 删除 lxq 表 row key 为 rk1 ,列标示符为 base_info:name 的数据hbase ( main ) : 002 : 0 > delete 'lxq' , 'rk1' , 'base_info:name' , 1600660619655hbase ( main ) : 035 : 0 > alter 'lxq' , 'delete' => 'base_info'# 清空表数据hbase ( main ) : 001 : 0 > truncate 'lxq'# 删除表# 先 disable 再 drophbase(main):036:0> disable 'lxq'hbase(main):037:0> drop 'lxq'# 如果不进行 disable ,直接 drop 会报错ERROR: Table user is enabled. Disable it first.
原理
读操作
1 )首先从 zk 找到 meta 表的 region 位置,然后读取 meta 表中的数据, meta 表中存储了用户表的region 信息2 )根据要查询的 namespace 、表名和 rowkey 信息。找到写入数据对应的 region 信息3 )找到这个 region 对应的 regionServer ,然后发送请求4 )查找对应的 region5 )先从 memstore 查找数据,如果没有,再从 BlockCache 上读取HBase上 Regionserver 的内存分为两个部分
- 一部分作为Memstore,主要用来写;
- 另外一部分作为BlockCache,主要用于读数据;
6 )如果 BlockCache 中也没有找到,再到 StoreFile 上进行读取从 storeFile 中读取到数据之后,不是直接把结果数据返回给客户端,而是把数据先写入到 BlockCache 中,目的是为了加快后续的查询;然后在返回结果给客户端。写操作
1 )首先从 zk 找到 meta 表的 region 位置,然后读取 meta 表中的数据, meta 表中存储了用户表的region信息2 )根据 namespace 、表名和 rowkey 信息。找到写入数据对应的 region 信息3 )找到这个 region 对应的 regionServer ,然后发送请求4 )把数据分别写到 HLog ( write ahead log )和 memstore 各一份5 ) memstore 达到阈值后把数据刷到磁盘,生成 storeFile 文件6 )删除 HLog 中的历史数据region拆分策略 HBase 的 Region Split 策略一共有以下几种:1 ) ConstantSizeRegionSplitPolicy0.94 版本前默认切分策略当 region 大小大于某个阈值 (hbase.hregion.max.filesize=10G) 之后就会触发切分,一个 region 等分为2 个 region 。但是在生产线上这种切分策略却有相当大的弊端:切分策略对于大表和小表没有明显的区分。阈值(hbase.hregion.max.filesize)设置较大对大表比较友好,但是小表就有可能不会触发分裂,极端情况下可能就1 个,这对业务来说并不是什么好事。如果设置较小则对小表友好,但一个大表就会在整个集群产生大量的region ,这对于集群的管理、资源使用、 failover 来说都不是一件好事。2 ) IncreasingToUpperBoundRegionSplitPolicy0.94 版本 ~2.0 版本默认切分策略切分策略稍微有点复杂,总体看和 ConstantSizeRegionSplitPolicy 思路相同,一个 region 大小大于设置阈值就会触发切分。但是这个阈值并不像ConstantSizeRegionSplitPolicy 是一个固定的值,而是会在一定条件下不断调整,调整规则和region 所属表在当前 regionserver 上的 region 个数有关系 .region split 的计算公式是:regioncount^3 * 128M * 2 ,当 region 达到该 size 的时候进行 split例如:第一次 split : 1^3 * 256 = 256MB第二次 split : 2^3 * 256 = 2048MB第三次 split : 3^3 * 256 = 6912MB第四次 split : 4^3 * 256 = 16384MB > 10GB ,因此取较小的值 10GB后面每次 split 的 size 都是 10GB 了3 ) SteppingSplitPolicy2.0 版本默认切分策略这种切分策略的切分阈值又发生了变化,相比 IncreasingToUpperBoundRegionSplitPolicy 简单了一些,依然和待分裂region 所属表在当前 regionserver 上的 region 个数有关系,如果 region 个数等于1,切分阈值为flush size * 2 ,否则为 MaxRegionFileSize 。这种切分策略对于大集群中的大表、小表会比 IncreasingToUpperBoundRegionSplitPolicy 更加友好,小表不会再产生大量的小 region ,而是适可而止。4 ) KeyPrefixRegionSplitPolicy根据 rowKey 的前缀对数据进行分组,这里是指定 rowKey 的前多少位作为前缀,比如 rowKey 都是 16 位的,指定前5 位是前缀,那么前 5 位相同的 rowKey 在进行 region split 的时候会分到相同的 region 中。5 ) DelimitedKeyPrefixRegionSplitPolicy保证相同前缀的数据在同一个 region 中,例如 rowKey 的格式为: userid_eventtype_eventid ,指定的delimiter为 _ ,则 split 的的时候会确保 userid 相同的数据在同一个 region 中。6 ) DisabledRegionSplitPolicy不启用自动拆分 , 需要指定手动拆分表预分区当一个 table 刚被创建的时候, Hbase 默认的分配一个 region 给 table 。也就是说这个时候,所有的读写请求都会访问到同一个regionServer 的同一个 region 中,这个时候就达不到负载均衡的效果了,集群中的其他regionServer 就可能会处于比较空闲的状态。解决这个问题可以用 pre-splitting, 在创建 table的时候就配置好,生成多个region 。
- 增加数据读写效率
- 负载均衡,防止数据倾斜
- 方便集群容灾调度region
每一个 region 维护着 startRow 与 endRowKey ,如果加入的数据符合某个 region 维护的 rowKey 范围,则该数据交给这个regioncreate 'person','info1','info2',SPLITS => ['1000','2000','3000']region合并Region 的合并不是为了性能,而是出于维护的目的。通过 Merge 类冷合并 Region这里通过 org.apache.hadoop.hbase.util.Merge 类来实现,不需要进入 hbase shell ,直接执行(需要先关闭hbase 集群)hbase org.apache.hadoop.hbase.util.Merge student \student,,1595256696737.fc3eff4765709e66a8524d3c3ab42d59. \student,aaa,1595256696737.1d53d6c1ce0c1bed269b16b6514131d0.通过 online_merge 热合并 Region与冷合并不同的是, online_merge 的传参是 Region 的 hash 值,而 Region 的 hash 值就是 Region 名称的最后那段在两个. 之间的字符串部分。需求:需要把 lxq_s 表中的 2 个 region 数据进行合并:student,,1587392159085.9ca8689901008946793b8d5fa5898e06. \student,aaa,1587392159085.601d5741608cedb677634f8f7257e000.需要进入 hbase shell :merge_region'c8bc666507d9e45523aebaffa88ffdd6','02a9dfdf6ff42ae9f0524a3d8f4c7777'
Java Api 应用
package ;
import com.cycc.proc.commons.core.hbase.config.HbaseConfig;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class HbaseUtils {
private static final Connection conn;
private static final Admin admin;
static {
try {
conn = HbaseConfig.getHbaseConnection();
admin = conn.getAdmin();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void close() {
if (admin != null) {
try {
admin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/*if (conn != null) {
try {
conn.close();
} catch (IOException e) {
e.printStackTrace();
}
}*/
}
/**
* 创建表
*
* @param tableName 表名
* @param columnFamily 列族名集合
*/
public static void creatTable(String tableName, Set<String> columnFamily) throws IOException {
if (StringUtils.isBlank(tableName) || CollectionUtils.isEmpty(columnFamily)) {
return;
}
//创建表描述器
HTableDescriptor table = new HTableDescriptor(TableName.valueOf(tableName));
for (String familyName : columnFamily) {
//设置列族描述器
table.addFamily(new HColumnDescriptor(familyName));
}
if (admin.tableExists(TableName.valueOf(tableName))) {
// 表已经存在了
return;
} else {
//执行创建操作
admin.createTable(table);
System.out.println(table + " create success!");
}
close();
}
/**
* 获取所有的表名
*/
public static List<String> getAllTableNames() throws IOException {
List<String> result = new ArrayList<>();
TableName[] tableNames = admin.listTableNames();
for (TableName tableName : tableNames) {
result.add(tableName.getNameAsString());
}
close();
return result;
}
/**
* 保存数据
*
* @param tableName 表名
* @param rowKey 行key
* @param familyName 列族名
* @param col 列名
* @param data 数据
* @throws IOException 异常
*/
public static void putData(String tableName, String rowKey, String familyName, String col, String data)
throws IOException {
if (StringUtils.isBlank(tableName) || StringUtils.isBlank(rowKey) || StringUtils.isBlank(familyName)
|| StringUtils.isBlank(col) || StringUtils.isBlank(data)) {
return;
}
if (!admin.tableExists(TableName.valueOf(tableName))) {
return;
}
// 获取一个表对象
Table t = conn.getTable(TableName.valueOf(tableName));
if (t == null) {
return;
}
// 设定 rowKey
Put put = new Put(Bytes.toBytes(rowKey));
//列族,列,value
put.addColumn(Bytes.toBytes(familyName), Bytes.toBytes(col), Bytes.toBytes(data));
//执行插入
t.put(put);
// t.put();//可以传入list批量插入数据
//关闭table对象
t.close();
close();
System.out.println("save " + data + " to table " + tableName + " success!");
}
/**
* 根据行Key删除
*
* @param tableName 表名
* @param rowKey 行Key
* @throws IOException 异常
*/
public static void deleteData(String tableName, String rowKey) throws IOException {
if (StringUtils.isBlank(tableName) || StringUtils.isBlank(rowKey)) {
return;
}
if (!admin.tableExists(TableName.valueOf(tableName))) {
return;
}
// 需要获取一个table对象
final Table t = conn.getTable(TableName.valueOf(tableName));
if (t == null) {
return;
}
// 准备delete对象
final Delete delete = new Delete(Bytes.toBytes(rowKey));
// 执行删除
t.delete(delete);
// 关闭table对象
t.close();
close();
System.out.println("delete " + rowKey + " from " + tableName + " success!");
}
/**
* 查看行Key下的列族信息
*
* @param tableName 表名
* @param rowKey 行Key
* @param familyName 列族名
*/
public static Cell[] getDataByCF(String tableName, String rowKey, String familyName) throws IOException {
if (StringUtils.isBlank(tableName) || StringUtils.isBlank(rowKey) || StringUtils.isBlank(familyName)) {
return null;
}
if (!admin.tableExists(TableName.valueOf(tableName))) {
return null;
}
// 获取表对象
HTable hTable = (HTable) conn.getTable(TableName.valueOf(tableName));
if (null == hTable) {
return null;
}
//创建查询的get对象
Get get = new Get(Bytes.toBytes(rowKey));
// 指定列族信息
// get.addColumn(Bytes.toBytes("info"), Bytes.toBytes("sex"));
get.addFamily(Bytes.toBytes(familyName));
// 执行查询
Result res = hTable.get(get);
// 获取改行的该列族的所有cell对象
Cell[] cells = res.rawCells();
for (Cell cell : cells) {
// 通过cell获取rowkey,cf,column,value
String cf = Bytes.toString(CellUtil.cloneFamily(cell));
String column = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
System.out.println(rowkey + "----" + cf + "---" + column + "---" + value);
}
// 关闭表对象资源
hTable.close();
close();
return cells;
}
/**
* 全表扫描 这里的返回结果没有 需要在封装一下
*
* @param tableName 表名
* @throws IOException 异常
*/
public static void scanAllData(String tableName) throws IOException {
if (StringUtils.isBlank(tableName)) {
return;
}
if (!admin.tableExists(TableName.valueOf(tableName))) {
return;
}
HTable hTable = (HTable) conn.getTable(TableName.valueOf(tableName));
if (null == hTable) {
return;
}
Scan scan = new Scan();
ResultScanner resultScanner = hTable.getScanner(scan);
for (Result result : resultScanner) {
// 获取改行的所有cell对象
Cell[] cells = result.rawCells();
for (Cell cell : cells) {
// 通过cell获取 rowkey,cf,column,value
String cf = Bytes.toString(CellUtil.cloneFamily(cell));
String column = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
System.out.println(rowkey + "----" + cf + "--" + column + "---" + value);
}
}
hTable.close();
close();
}
/**
* 指定查哪些行key的数据 结果还需要在封装一下
*
* @param tableName 表名
* @param startRow 开始行key
* @param stopRow 结束行key
* @throws IOException 异常
*/
public static void scanRowKey(String tableName, String startRow, String stopRow) throws IOException {
if (StringUtils.isBlank(tableName) || StringUtils.isBlank(startRow) || StringUtils.isBlank(stopRow)) {
return;
}
HTable hTable = (HTable) conn.getTable(TableName.valueOf(tableName));
if (null == hTable) {
return;
}
Scan scan = new Scan();
scan.setStartRow(startRow.getBytes());
scan.setStopRow(stopRow.getBytes());
ResultScanner resultScanner = hTable.getScanner(scan);
for (Result result : resultScanner) {
// 获取改行的所有cell对象
Cell[] cells = result.rawCells();
for (Cell cell : cells) {
// 通过cell获取rowkey,cf,column,value
String cf = Bytes.toString(CellUtil.cloneFamily(cell));
String column = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
System.out.println(rowkey + "----" + cf + "--" + column + "---" + value);
}
}
hTable.close();
}
}
协处理器
访问 HBase 的方式是使用 scan 或 get 获取数据,在获取到的数据上进行业务运算。但是在数据量非常大的时候,比如一个有上亿行及十万个列的数据集,再按常用的方式移动获取数据就会遇到性能问题。客户端也需要有强大的计算能力以及足够的内存来处理这么多的数据。此时就可以考虑使用 Coprocessor( 协处理器 ) 。将业务运算代码封装到 Coprocessor 中并在RegionServer 上运行,即在数据实际存储位置执行,最后将运算结果返回到客户端。利用协处理器,用户可以编写运行在 HBase Server 端的代码。通过Java代码编写自己的协处理器,示例:
public class MyProcessor extends BaseRegionObserver { @Override public void prePut(ObserverContext<RegionCoprocessorEnvironment> ce, Put put, WALEdit edit, Durability durability) throws IOException { //把自己需要执行的逻辑定义在此处,向t2表插入数据,数据具体是什么内容与Put一样 final HTableInterface t2 = e.getEnvironment().getTable(TableName.valueOf("t2")); //解析t1表的插入对象put final Cell cell = put.get(Bytes.toBytes("info"), Bytes.toBytes("name")).get(0); //table对象.put final Put put1 = new Put(put.getRow()); put1.add(cell); t2.put(put1); //执行向t2表插入数据 t2.close(); } }
<dependency><groupId>org.apache.hbase</groupId><artifactId>hbase-server</artifactId><version>1.3.1</version></dependency>打成 Jar 包,上传 HDFScd /opt/lxq/softwaresmv original-hbaseStudy-1.0-SNAPSHOT.jar processor.jarhdfs dfs -mkdir -p /processorhdfs dfs -put processor.jar /processor挂载协处理器hbase(main):056:0> describe 't1'hbase(main):055:0> alter 't1' ,METHOD = >'table_att' , 'Coprocessor' = > 'hdfs://linux121:9000/processor/processor.jar|com.lxq.hbase.processor.MyProcessor|1001|'# 再次查看 't1' 表,hbase(main):043:0> describe 't1'# 卸载协处理器disable 't1'alter 't1' ,METHOD = > 'table_att_unset' ,NAME = > 'coprocessor$1'enable 't2'Observer协处理器与触发器 (trigger) 类似:在一些特定事件发生时回调函数(也被称作钩子函数, hook )被执行。这些事件包括一些用户产生的事件,也包括服务器端内部自动产生的事件。协处理器框架提供的接口如下RegionObserver :用户可以用这种的处理器处理数据修改事件,它们与表的 region 联系紧密。MasterObserver :可以被用作管理或 DDL 类型的操作,这些是集群级事件。WALObserver :提供控制 WAL 的钩子函数Endpoint这类协处理器类似传统数据库中的存储过程,客户端可以调用这些 Endpoint 协处理器在 Regionserver中执行一段代码,并将 RegionServer 端执行结果返回给客户端进一步处理。Endpoint 常见用途聚合操作假设需要找出一张表中的最大数据,即 max 聚合操作,普通做法就是必须进行全表扫描,然后 Client代码内遍历扫描结果,并执行求最大值的操作。这种方式存在的弊端是无法利用底层集群的并发运算能力,把所有计算都集中到 Client 端执行 , 效率低下。使用 Endpoint Coprocessor ,用户可以将求最大值的代码部署到 HBase RegionServer 端, HBase会利用集群中多个节点的优势来并发执行求最大值的操作。也就是在每个 Region 范围内执行求最大值的代码,将每个 Region 的最大值在 Region Server 端计算出,仅仅将该 max 值返回给 Client 。在Client进一步将多个 Region 的最大值汇总进一步找到全局的最大值。Endpoint Coprocessor 的应用我们后续可以借助于 Phoenix 非常容易就能实现。针对 Hbase 数据集进行聚合运算直接使用SQL 语句就能搞定。
rowkey设计原则
RowKey长度原则 建议越短越好,不要超过 16 个字节RowKey散列原则 建议将 rowkey 的高位作为散列字段,这样将提高数据均衡分布在每个 RegionServer ,以实现负载均 衡的几率。RowKey 唯一原则访问hbase table 中的行:有 3 种方式:
- 单个rowkey
- rowkey 的range
- 全表扫描(一定要避免全表扫描)
RowKey排序原则 HBase 的 Rowkey 是按照 ASCII 有序设计的,我们在设计 Rowkey 时要充分利用这点
优化相关
检索 habse 的记录首先要通过 row key 来定位数据行。当大量的 client 访问 hbase 集群的一个或少数几个节点,造成少数region server 的读 / 写请求过多、负载过大,而其他 region server 负载却很小,就造成了“ 热点 ” 现象。解决方案有:预分区预分区的目的让表的数据可以均衡的分散在集群中,而不是默认只有一个 region 分布在集群的一个节点上。加盐这里所说的加盐不是密码学中的加盐,而是在 rowkey 的前面增加随机数,具体就是给 rowkey 分配一个随机前缀以使得它和之前的rowkey 的开头不同。哈希哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey ,可以使用 get 操作准确获取某一个行数据。反转反转固定长度或者数字格式的 rowkey 。这样可以使得 rowkey 中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey ,但是牺牲了 rowkey 的有序性。15X,13X,二级索引常见的二级索引我们一般可以借助各种其他的方式来实现,例如 Phoenix 或者 solr 或者 ES 等Hbase也需要像redis一样运用布隆过滤器hbase 的读操作需要访问大量的文件,大部分的实现可通过布隆过滤器来避免大量的读文件操作。
行储存和列存储比较
行存储(Row-based Storage)和列存储(Column-based Storage)是数据库系统中两种核心的数据物理组织方式,它们在性能、适用场景和底层实现上存在显著差异。以下是它们的详细比较:
1. 数据存储方式
行存储(Row-oriented)
- 按行存储:将同一行的所有数据(包括不同列的值)连续存储在磁盘上(如:
(UserID, Name, Age, City)
作为一个整体存储)。- 示例:
plaintextCopy Code
Block1: [1, "Alice", 28, "Paris"] Block2: [2, "Bob", 32, "London"]
列存储(Column-oriented)
- 按列存储:将同一列的所有数据连续存储(如:所有
UserID
存在一起,所有Name
存在一起)。- 示例:
plaintextCopy Code
Block1 (UserID): [1, 2, 3, ...] Block2 (Name): ["Alice", "Bob", "Charlie", ...] Block3 (Age): [28, 32, 25, ...]
2. 核心优势对比
特性 行存储 列存储 写入性能 ⭐⭐⭐⭐ 高
(单次插入整行数据)️ 低
(需写入多列位置)点查询(单行读取) ⭐⭐⭐⭐ 高
(一次I/O读取整行)⚠️ 低
(需从多列重组行)聚合查询(SUM/AVG) ️ 低
(需扫描整行数据)⭐⭐⭐⭐ 高
(仅读取目标列)数据压缩率 ⚠️ 低
(行内数据类型多样)⭐⭐⭐⭐ 高
(同列数据类型一致)索引效率 依赖B树等索引 列数据天然紧凑,适合位图索引 存储空间 通常更大 较小(高压缩率)
3. 适用场景
行存储适合:
- OLTP(在线事务处理):如银行交易、订单提交。
- 频繁的单行读写(如用户登录、订单查询)。
- 需要完整行数据的操作(如更新用户资料)。
- 常见数据库:MySQL、PostgreSQL、SQL Server。
列存储适合:
- OLAP(在线分析处理):如数据仓库、商业智能(BI)。
- 大规模聚合查询(统计销售额、计算平均值)。
- 仅需部分列的查询(如分析用户年龄分布)。
- 常见数据库:Amazon Redshift、Google BigQuery、ClickHouse。
4. 优化技术
行存储优化:
- 索引:通过B+树加速行定位。
- 行缓存:缓存热点行(如Redis)。
列存储优化:
- 向量化处理:批量处理列数据(利用CPU SIMD指令)。
- 列裁剪:仅读取查询涉及的列。
- 高级压缩:字典编码(Dictionary Encoding)、行程编码(RLE)。
5. 混合存储方案
现代数据库(如 Apache Cassandra、Bigtable)支持混合模式:
- 基于行的分区 + 列族(Column Families):在分区内按列存储数据。
- Delta Lake / Iceberg:在数据湖中同时支持行列混合格式(如Parquet/ORC列存 + 行式ACID事务)。
典型场景示例
OLTP查询(行存优势)
sqlCopy Code
SELECT * FROM users WHERE user_id = 100; -- 一次I/O读取整行
OLAP分析(列存优势)
sqlCopy Code
SELECT AVG(salary), department FROM employees GROUP BY department; -- 仅读取salary和department列,压缩数据加速扫描
总结
维度 行存储 列存储 设计目标 高并发事务处理 大规模数据分析 数据布局 行连续存储 列连续存储 强项操作 单行CRUD 聚合、扫描、列投影 弱点 全表扫描效率低 点查询/更新成本高 工业应用 MySQL, PostgreSQL Redshift, Snowflake, ClickHouse 💡 关键选择建议:
- 需要实时处理事务? → 选择行存储(或Hybrid)。
- 需要分析TB级数据? → 选择列存储。
现代分布式系统(如 Apache Druid)甚至支持时间序列数据按列分片,进一步优化时序分析场景。理解存储模型是数据库性能调优和架构设计的底层基石。
感谢阅读!!!