HBase的基础知识
HBase 是大数据领域解决海量数据实时读写挑战的核心组件之一,它的特性包括:
分布式、可扩展性、高可用:HBase节点分为HMaster和RegionServer
HMaster负责元数据管理和Region分配,其可以通过主备架构搭建,基于ZK进行节点选举
RegionServer负责用户数据存储,数据自动分Region,并分布在多个服务器RegionServer上,具备横向扩展能力,且可以在HDFS进行3副本备份存储,保证可靠性
列存储:数据按列族存储而非按行,每行有一个唯一标志符row key。HBase在读取数据时,只需读取对应的列族文件,避免读取整行数据
强一致性:对单行数据的读写操作提供强一致性,写入成功后对后续操作一定可见
高容量存储:基于HDFS,天然支持PB、EB级别的数据
高性能随机读写:采用LSM-Tree,将随机写入转换为顺序写入,(写入 MemStore 和 WAL,再顺序刷写到 HFile)
稀疏存储;允许存储大量的空单元格NULL,这种特性非常适合存储属性很多但每个记录只填充少量属性的场景
有序性:行间数据按row key的字典序进行排序,行内数据按列族、列限定符排序。scan性能高
版本化:每个单元格Cell可以保存多个历史版本数据(由列族配置),可以通过时间戳timestamp访问特定版本数据
Hadoop生态:深度集成HDFS作为底层存储,与MapReduce、Spark、Hive、Pig、Flink等框架无缝集成,使用Zookeeper进行集群协调、元数据管理和Master选举
选择HBase作为存储介质,一般我们的目的是其PB、EB级别的高容量、极高的吞吐量(尤其是随机访问和写入方面)、水平扩展能力、强一致性、数据版本特性
HBase的列存储特性
HBase的行键
row key是HBase中一行数据的唯一标志符,是HBase数据分布的基石
row key有如下特性:
数据唯一性:每行数据必须有唯一的row key,如果存入row key相同的行,可能会覆盖,或增加新版本,或增加新单元格
数据排序性:HBase中的数据严格按照row key的字典序升序存储,例如row key为002,插入个一个001,会直接排到002前面,插入010则直接排到二者后面。数据排序特性使得HBase做Scan非常高效
数据分布:HBase表数据被水平分割为多个region,region在HBase中是负载均衡和分布式存储的基本单元,由regionServer负责服务,一个Region包含一段连续的row key(例如001~010),当某个region变得太大,会split成两个新region(与mysql innodb的b+树页分裂差不多)
高性能数据访问:Get(随机访问)时,HBase先定位到对应的Region(通过hbase:meta表),然后由负责该region的regionServer提供服务,这一点与B+树的索引原理更像了;Scan(顺序扫描)时是按顺序读取各个Region,因此速度本身就很快,物理上存储都是连续的;Put(写入)时则与Get类似,要到要插入的位置。因此Row Key设计的好不好,直接影响到HBase表的读写性能
好的Row Key设计需要遵循如下原则:
避免热点 (Hotspotting):如果row key本身就有数据倾斜的特性(例如001~005特别多,005~009比较少),或者使用单纯的递增时间戳或递增常数做row key,会导致某一个RegionServer压力特别大。对于这种分布问题,解决方案包括:
加盐(salting):在原始 Row Key 前添加一个随机前缀(如
0_user123
,1_user456
,2_user789
)。这能将写入分散到不同的 Region。缺点是牺牲了 Row Key 的原始顺序性,扫描时需要处理所有前缀哈希 (Hashing): 对原始 Row Key 进行哈希(如 MD5, SHA1)或取模运算,生成一个更均匀分布的前缀或后缀。同样能分散写入,但也牺牲了顺序性
反转 (Reversing): 反转固定长度或具有自然递增趋势的键(如反转手机号
13800138000
->000083100831
,反转时间戳20230724120000
->00002143072023
)。这样新数据(如最新时间戳)在反转后就不会都堆积在最后。保留了部分前缀顺序,但牺牲了原始可读性组合键 (Composite Key): 将多个字段组合成 Row Key,将查询频率高的、能有效分散数据的字段放在前面。例如
[RegionCode]_[Timestamp]_[UserID]
或[UserID]_[ReverseTimestamp]
。这是最常用且灵活的方式。
长度:Row Key的长度在保证唯一性的前提下要尽量短,过长的Row Key会显著增加存储开销(因为每个 Key 在每个 HFile、MemStore、BlockCache 中都会重复存储)并降低比较效率
可读性:在满足性能和分布要求的前提下,设计具有一定可读性的Row Key有助于调试和理解数据
利用排序:设计 Row Key 时考虑你最常用的查询模式。如果你的查询主要是范围扫描,应让 Row Key 的结构支持这种扫描(例如,将范围扫描的维度放在 Row Key 的前缀部分,这是因为字典序是最左前缀匹配的,这和innodb的B+树索引失效原理也类似的)
HBase列存储
HBase 常被称为“列式数据库”或“面向列的存储”,但其本质并不像Parquet、ORC、Vertical这些真正的列存储,因为HBase并不是按每一列独立存储和压缩所有行的该列数据,而实际上是面对列族的存储
在创建表时需要定义一个或多个列族(例如 cf1
, cf2
, info
, metrics
)。列族是表的模式定义 (Schema) 的一部分,通常很少改变。而数据在物理存储层面是按列族组织的! 这是理解 HBase 存储的关键。包括如下特性:
在同一个Region内,属于同一个列族的所有列的数据(称为一个列族的qualifier),物理上存储在一起
每个列族的数据存储在自己的Store中
每个Store包含一个MemStore(内存写缓存)和零个或多个HFile(持久化在HDFS上的文件)
不同列族的数据物理上是分开存储的,存储在不同的HFile集合中
看如下案例,建一个四列,两个列族的HBase表,存入两条数据
Row Key | Column Family: `info` | Column Family: `metrics`
| qualifier: `name` | `email` | qualifier: `cpu` | `mem`
--------------------------------------------------------------------------
server-node1 | "Web Server 1" | "web1@ex.com" | 85.5 | 2048
server-node2 | "DB Server" | "db@ex.com" | 23.1 | 4096
这个表在一个Region中有两个Store:Store_info、Store_metrics
Store_info存储server-node1数据中的name、email列和server-node2中的name、email列,即"Web Server 1"、"web1@ex.com"、"DB Server"、"db@ex.com",生成自己的MemStore和HFile集合
Store_metrics存储server-node1数据中的cpu、mem,server-node2中的cpu、mem,即85.5、2048、23.1、4096,同样生成自己的MemStore和HFile集合
HBase面向列族存储的应用和优势如下:
可以使用列族+列名查询,不跨列族的查询性能高(类似innodb中的index查询方法,不需要回表),且减少IO
SELECT metrics:cpu FROM my_table
独立配置与管理:可以为不同的列族设置不同的存储属性,提高灵活性,例如:
压缩算法:可以为文本数据多的列族设置压缩算法例如GZIP,为数值多的列族设置Snappy或LZO
布隆过滤器:对经常进行随机读取的列族启用布隆过滤器,尤其是ROW、ROWCOL模式,可以快速判断某行或某单元格是否不存在,避免不必要的磁盘读取
块大小:设置HFile数据块大小,影响扫描效率和缓存利用率
版本数:看为列族设置保留数据版本数量
生存时间:可以为每个列族单独设置TTL
稀疏性:同一列族内,不同的行可以有不同的列 (
qualifier
)。只有实际存在的列才会占用存储空间。对于属性众多但每个实例只拥有部分属性的数据(如用户画像、商品属性)非常高效动态添加列:在列族内,可以随时动态添加新的列限定符 (
qualifier
),无需像关系型数据库那样预先ALTER TABLE ADD COLUMN
并迁移数据。只需在写入时指定新的列名即可
row key与列族的关系
Row Key 是第一维度: 数据首先按 Row Key 全局排序并分布到 Region
列族是第二维度: 在同一个 Region 内,同一个 Row Key 下的数据,再按列族拆分到不同的物理存储单元 (Store/HFile) 中
列限定符是第三维度: 在同一个列族内,同一个 Row Key 下,可以有多个不同的列 (
qualifier
)时间戳是第四维度: 每个单元格 (
Cell
) 可以有多个版本,通过时间戳区分
如果一个查询不使用row key,例如上面的例子:SELECT metrics:cpu FROM my_table
,这种场景将会触发一次真正意义上的全表扫描,这是由其基于Row Key排序和分区的存储模型决定的。因为无法通过一个列族定位具体的region,因此将会从第一个region遍历到最后一个region,同时取出所有结果
因此,我们需要将查询条件构造在row key中,如下:
public class HBaseRowKeyQueryExample {
// HBase 配置常量
private static final String TABLE_NAME = "user_metrics";
private static final String COLUMN_FAMILY = "metrics";
private static final String CPU_COLUMN = "cpu_usage";
private static final String MEMORY_COLUMN = "memory_usage";
private static final String ZK_QUORUM = "zk1.example.com,zk2.example.com,zk3.example.com";
private static final String ZK_PORT = "2181";
public static void main(String[] args) {
// 要查询的 Row Key
String rowKey = "server-node1-20230724";
try {
// 1. 创建 HBase 配置
Configuration config = HBaseConfiguration.create();
config.set("hbase.zookeeper.quorum", ZK_QUORUM);
config.set("hbase.zookeeper.property.clientPort", ZK_PORT);
// 2. 创建 HBase 连接
try (Connection connection = ConnectionFactory.createConnection(config);
Table table = connection.getTable(TableName.valueOf(TABLE_NAME))) {
// 3. 创建 Get 对象并指定 Row Key
Get get = new Get(Bytes.toBytes(rowKey));
// 4. (可选) 指定要获取的列
get.addColumn(Bytes.toBytes(COLUMN_FAMILY), Bytes.toBytes(CPU_COLUMN));
get.addColumn(Bytes.toBytes(COLUMN_FAMILY), Bytes.toBytes(MEMORY_COLUMN));
// 5. 执行查询
Result result = table.get(get);
// 6. 处理查询结果
if (result.isEmpty()) {
System.out.println("No data found for row key: " + rowKey);
return;
}
// 获取特定列的值
byte[] cpuValue = result.getValue(
Bytes.toBytes(COLUMN_FAMILY),
Bytes.toBytes(CPU_COLUMN)
);
byte[] memoryValue = result.getValue(
Bytes.toBytes(COLUMN_FAMILY),
Bytes.toBytes(MEMORY_COLUMN)
);
// 7. 输出结果
System.out.println("Query Results for Row Key: " + rowKey);
System.out.println("----------------------------------");
System.out.println(CPU_COLUMN + ": " + (cpuValue != null ? Bytes.toDouble(cpuValue) : "N/A"));
System.out.println(MEMORY_COLUMN + ": " + (memoryValue != null ? Bytes.toLong(memoryValue) + " MB" : "N/A"));
// 8. (可选) 遍历所有列族和列
System.out.println("\nAll available data for this row:");
result.getMap().forEach((columnFamily, qualifierMap) -> {
String familyStr = Bytes.toString(columnFamily);
qualifierMap.forEach((qualifier, valueList) -> {
String qualifierStr = Bytes.toString(qualifier);
valueList.forEach(cell -> {
System.out.printf("%s:%s = %s (timestamp=%d)%n",
familyStr,
qualifierStr,
Bytes.toString(cell.getValue()),
cell.getTimestamp());
});
});
});
}
} catch (IOException e) {
System.err.println("HBase query failed: " + e.getMessage());
e.printStackTrace();
}
}
}
评论区