hbase基础总结
Hbase是什么
基于 HDFS / 本地文件的 分布式、面向列的 NoSQL 数据库
适合:海量数据、高并发、简单 KV 读写、稀疏数据
不适合:事务、复杂关联查询、频繁更新删除
列存储格式
行存储 vs 列存储
行存储:行存储在物理磁盘上,是把一整行所有字段紧紧挨在一起存的。
1,张三,20,北京,13800001111 → 2,李四,25,上海,13900002222 → 3,王五,30,广州,13700003333
查询 name :要读整行数据 → 再把 name 挑出来,读了很多没用的数据(age、city、phone)
因为磁盘只能一块一块读,不能精准跳到 “张三” 这个位置单独读。磁盘的最小读取单位是页(Page),比如 16KB。数据库读取时,必须:读入一整页数据,里面包含一整行甚至很多行,然后在内存里解析,再把 name 字段提取出来
列存储:按 “一列” 连续存在一起,查询 name ,只需要读取 name 那列的文件即可就可全部提取出来
id:1,2,3
name:张三,李四,王五
age:20,25,30
city:北京,上海,广州
phone:138...,139...,137...
NULL值处理
行存储里,NULL 不会让行变短,字段位置依然占坑,只是标记为空。读一列依然要读完整行,哪怕 age、phone 是 NULL,数据库还是要把整行读进来,再通过 NULL 位图判断哪些字段有效。不会因为有 NULL 就 “跳过不读”,行是一个整体,必须整块读取。而列存储会对整列统一压缩 NULL,非常省空间。
列存数据库几乎不支持 UPDATE / DELETE
因为列存是按列分开存储的,修改 / 删除一行,需要同时修改 N 个列文件,代价巨大到不划算。
一行数据在列存里是撕碎了散落在各个列文件中的。执行UPDATE user SET age=26 WHERE id=2;对于行存储数据库的步骤:
- 找到那一行
- 直接把 age 从 NULL 改成 26
- 结束
因为整行是连在一起的,改一个字段只需要改一小块。而对于列存储数据库步骤如下:
- 找到 id=2 对应的行号(比如第 2 行)
- 打开 age 列文件
- 修改 age 第 2 个值(还要改 NULL 位图)
- 重写整个 age 数据块
- 刷盘
这还只是改一个字段,如果一行有 50 个字段,改 1 个字段,也要:定位行号,打开对应列文件,重写整个数据块,刷盘。成本是行存的几十倍。
如果要 DELETE 某一行?DELETE FROM user WHERE id=2;,对于列存储数据库必须:
- 找到行号
- 打开 所有列文件
- 把每一列里第 2 个位置删掉
- 后面的数据全部往前挪
- 重新生成位图
- 重写所有列文件
这相当于全表重写。所以列存天生不适合频繁删改,只适合批量写入 + 大量分析。
列存为了压缩,都是一批数据打包成一个大 Block。
比如 100 万行一个 Block。删其中 1 行 = 必须重写整个 Block 的所有列文件。这就像是一本书 1000 页,要删掉第 500 页的一个字,结果必须重印整本书
不做真正的 UPDATE/DELETE
标记删除(Delete Mark):不删数据,只加一个删除位图,查询时自动跳过。但数据还在,空间不释放。
异步合并(Compaction/Merge):后台慢慢把多个小 Block 合并成大 Block,合并时真正扔掉被删除的数据。
不支持 UPDATE,只支持 INSERT + 覆盖
比如:
先插入一条新数据,查询时取最新一条,旧数据等合并时扔掉。这就是所谓的 “LSM 树 + 读时合并” 思路。
实际上关系型数据库对于删除也是类似的做法,删除只是更新,设置一个删除标志位,后面再异步删除(比如innodb里的purge操作:https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-purge)
总结
列存数据库的优缺点非常明显
优点
- 批量插入极快
- 按列压缩强,空间极小
- 分析查询(统计、聚合)飞快
缺点(也是为什么它不适合做业务库)
- 单条插入很慢(要等攒批、排序、重组)
- 更新、删除极弱(很多列存不支持行级更新)
- 小批量频繁插入会产生大量小文件,性能暴跌
| 操作 | 行存储 (MySQL、InnoDB) | 列存储 (HBase、Parquet、ClickHouse) |
|---|---|---|
| 单行 UPDATE | 极快,只修改当前行数据 | 极慢,需要重写整个列数据块 |
| 单行 DELETE | 极快,直接删除当前行 | 极慢,需要重写所有相关列数据块 |
| 批量插入 | 快 | 极快 |
| 统计分析(sum/avg 等) | 慢,需要读取整行数据 | 极快,只读取目标列即可 |
Hbase集群架构
详细介绍参考:Apache HBase 架构详解
HBase集群角色主要包括 Master(HMaster)、RegionServer(HRegionServer)、ZooKeeper 和 Client。
-
HMaster (Master角色):管理元数据和分配Region
职责:集群的管理者。处理表级别的DDL操作(创建、修改、删除表),监控RegionServer状态,处理Region的分配或故障转移(Failover),维护元数据。
高可用:通常会部署多个HMaster,由ZooKeeper选举出一个Active Master,其他为Backup Master。 -
HRegionServer (RegionServer角色):负责数据读写
职责:实际干活的节点。直接对接用户读写请求(DML),处理分配给它的Region,负责数据的分片(Region)管理、刷新缓存到HDFS以及处理Compact操作。表被切分成很多 Region,Region 就存在 RegionServer 里,它负责管理数据文件、索引、缓存,以及处理所有读写请求 -
ZooKeeper (协调服务):协调节点高可用和状态监控
职责:集群的管家。实现Active Master的选举,监控RegionServer上下线状态并通过回调机制通知Master,存储HBase的元数据入口(如.META.表位置)。 -
Client (客户端):用于访问HBase服务
职责:访问HBase接口。Client通过RPC机制与Master和RegionServer通信,通常会缓存元数据以加速访问。 -
HDFS (存储底层)
职责:虽非HBase进程,但HBase依赖HDFS存储实际数据。
HBase安装
参考:HBase环境搭建
单机模式(Standalone):完全不用 HDFS
所有进程(Master/RegionServer/ZooKeeper)跑在一个 JVM
轻量、开箱即用、适合学习 / 开发
缺点:无分布式、无副本、无高可用、数据不可靠、性能有限 → 不能用于生产
下载:https://archive.apache.org/dist/hbase/
- 安装及配置
安装:下面贴一个 AI 写的安装脚本
#!/bin/bash
# ====================== 配置项 ======================
HBASE_VERSION="2.5.6"
INSTALL_DIR="/opt"
HBASE_HOME="${INSTALL_DIR}/hbase-${HBASE_VERSION}"
DATA_DIR="/opt/hbase-data"
# ====================================================
echo "===== 开始安装 HBase ${HBASE_VERSION}(单机无 HDFS)====="
# 1. 安装 wget
if ! command -v wget &>/dev/null; then
echo "安装 wget..."
yum install -y wget || apt install -y wget
fi
# 2. 自动获取本机IP(优先内网IP)
echo "正在自动获取本机IP..."
IP=$(hostname -I | awk '{print $1}')
echo "本机IP => $IP"
# 3. 下载 HBase
cd $INSTALL_DIR
if [ ! -f "hbase-${HBASE_VERSION}-bin.tar.gz" ]; then
echo "下载 HBase..."
wget "https://archive.apache.org/dist/hbase/${HBASE_VERSION}/hbase-${HBASE_VERSION}-bin.tar.gz"
fi
# 4. 解压
echo "解压..."
tar zxf hbase-${HBASE_VERSION}-bin.tar.gz
# 5. 创建数据目录
mkdir -p $DATA_DIR
# 6. 生成 hbase-site.xml
echo "生成配置文件..."
cat > "${HBASE_HOME}/conf/hbase-site.xml" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<property>
<name>hbase.rootdir</name>
<value>file://${DATA_DIR}</value>
</property>
<property>
<name>hbase.cluster.distributed</name>
<value>false</value>
</property>
<property>
<name>hbase.master.address</name>
<value>${IP}:16000</value>
</property>
<property>
<name>hbase.master.info.bindAddress</name>
<value>${IP}</value>
</property>
<property>
<name>hbase.unsafe.stream.capability.enforce</name>
<value>false</value>
</property>
</configuration>
EOF
# 7. 配置 JAVA_HOME
echo "配置 JAVA_HOME..."
java_path=$(readlink -f $(which java) | sed "s:/bin/java::")
sed -i "s|^# export JAVA_HOME=.*|export JAVA_HOME=${java_path}|" "${HBASE_HOME}/conf/hbase-env.sh"
# 8. 环境变量
echo "配置环境变量..."
cat > /etc/profile.d/hbase.sh <<EOF
export HBASE_HOME=${HBASE_HOME}
export PATH=\$PATH:\${HBASE_HOME}/bin
EOF
source /etc/profile.d/hbase.sh
# 9. 启动
echo "启动 HBase..."
${HBASE_HOME}/bin/start-hbase.sh
echo
echo "===== 安装完成 ====="
echo "Web UI: http://$IP:16010"
echo "命令行: hbase shell"
- 配置环境变量
手动执行:source /etc/profile.d/hbase.sh,让环境变量生效
- 关闭日志
不操作这一步,执行hbase shell,控制台会打印一大堆日志
# 进入 HBase 配置目录
cd $HBASE_HOME/conf
# 把 INFO 改成 ERROR = 只显示错误,不显示普通日志
sed -i 's/INFO/ERROR/' log4j2.properties
恢复日志级别
sed -i 's/ERROR/INFO/' $HBASE_HOME/conf/log4j2.properties
有些版本使用的日志是log4j,注意替换相应的命令
- 验证启动成功
启动后发现启动了一个HMaster进程
[root@localhost hbase]# jps
3750 Jps
3231 HMaster
原因是使用的是HBase 单机模式(Standalone),hbase.cluster.distributed值为false,这个模式下只有 1 个进程:HMaster。HRegionServer、ZooKeeper 全部内嵌在 HMaster 里,不会出现独立进程
验证 HBase 启动成功:执行 hbase shell
hbase:001:0> status
1 active master, 0 backup masters, 1 servers, 0 dead, 2.0000 average load
Took 1.1044 seconds
访问 UI 界面:http://192.168.65.130:16010

- 停止 Hbase
执行安装目录下的脚本:stop-hbase.sh
使用
hbase shell 是一个命令行客户端,命令行可以使用tab进行补全
hbase:001:0> 的含义:
- hbase:固定开头,表示这是 HBase Shell
- (main):表示在 main 命名空间(可以忽略,不用管),这个刚进入shell时不会出现
- 001:这是你已经输入的命令序号(第几条命令),每执行一条命令,数字 +1
- 0:上一条命令的返回状态码,0 = 执行成功,非 0 = 执行失败
基本概念
| 关系型数据库(MySQL) | HBase | 说明 |
|---|---|---|
| 数据库(Database) | 命名空间(Namespace) | 类似分组、隔离权限 |
| 表(Table) | 表(Table) | 基本存储单元 |
| 行(Row) | 行(Row) | 由 RowKey 唯一标识 |
| 主键(Primary Key) | RowKey | 字典序排序,查询只能按 RowKey |
| 列(Column) | 列(Column)= 列族:限定符 | 列可以动态随意加 |
| 列定义(Schema) | 只定义列族 | 列不用提前定义 |
| 字段类型 | 全部是 字节数组 bytes | 无类型,自己解析 |
| 事务 | 强事务 | 仅行级原子性 |
| 查询 | SQL 丰富 | 只能:全表扫 /get/scan |
| 存储结构 | 行存储 | 列簇存储(面向列) |
| 扩展性 | 垂直扩展 | 水平线性扩展 |
HBase 核心概念详解
- RowKey(行键)
相当于主键
字典序排序
查询只能:根据 RowKey 精确查,根据 RowKey 范围查
全表扫描
设计非常关键(散列、避免热点) - Column Family(列族)
建表时必须指定,物理上在一起存储,列族不宜多,一般 1~3 个 - Column Qualifier(列限定符)
属于某个列族:info:name、info:age
动态添加,无需预先定义,一行有 10 列,另一行可以有 100 列 - Cell(单元格)
由 表 + RowKey + 列族:列 + 版本号 确定
存储的是 二进制字节数组,自带多版本(默认保留 3 个) - TimeStamp(版本 / 时间戳)
每次 put 自动生成版本,可用于数据回溯
命名空间
HBase 的命名空间 = MySQL 的数据库(Database),就是一个分组 / 隔离的概念。mysql 里先建库然后建表,hbase 里是一样的,先建namespace后建表。
HBase 自带两个默认命名空间,和mysql也一样,默认有information_shcema,mysql等库
- default:不写命名空间,表就放这里,create 'user' 等价于 create 'default:user'
- hbase:系统表,存元数据,不要动
# 创建命名空间
create_namespace 'test_db'
# 查看所有命名空间
list_namespace
hbase:002:0> list_namespace
NAMESPACE
default
hbase
test_db
2 row(s)
Took 0.0758 seconds
# 在命名空间建表
create 'test_db:user', 'id', "username"
# 切换到命名空间,切换后,直接操作表 不需要写 命名空间: 前缀。
use 'test_db'
# 删除命名空间(需先删表)
drop_namespace 'test_db'
表操作
建表
建表语句格式就是:create '表名', '列族1', '列族2',这里指定的不是列名,而是列族,不需要类型。列名是动态添加的
# 建表:表名, 列族1, 列族2
create 'user', 'info', 'ext'
列族:建表时必须提前定义,不能随便改,物理存储在一起,一个表一般 1~3 个列族
列名:不用提前定义,插入时随便加,属于某个列族,格式:列族:列名
HBase 建表时不能指定字段类型,因为 HBase 没有 int、string、date、boolean 这种数据类型。所有数据,不管是数字、名字、时间,存进去一律都是字节数组(byte[])。因为 HBase 是面向列的 NoSQL,它的设计原则是:
- 只存字节,不理解数据含义
- 无 Schema 约束,字段动态添加,不需要提前定义
查看表
# 查看当前namespace的所有表
list
# 查看指定namespace的所有表
list_namespace_tables 'test_db'
查看表结构
查看表结构:表名可以加命名空间:desc 'default:user'
desc 'user'
删除禁用表
truncate = disable → drop → create
# 禁用表(删除、修改前必须禁用)
disable 'user'
# 启用表
enable 'user'
# 删除表(必须先 disable)
drop 'user'
# 清空表(truncate 会自动禁用再清空)
disable 'user'
truncate 'user'
插入数据 put
命令格式:put '表名', 'RowKey', '列族:列名', '值',列必须带列族,前面建表的时候没有指定列名,这里就用到了。
put 'user', 'row1', 'info:name', 'zhangsan'
put 'user', 'row1', 'info:age', '20'
put 'user', 'row1', 'ext:phone', '13800001111'
put 'user', 'row2', 'info:name', 'lisi'
put 'user', 'row2', 'info:city', 'shanghai'
查询数据
这里先介绍下 RowKey 的概念:RowKey = HBase 表的 主键 + 唯一标识 + 排序依据 + 索引键,HBase 所有查询、存储、分布,全都只认 RowKey!
例如 mysql 中:SELECT * FROM user WHERE id = 1001; 这么一个查询类似于 HBase 里:get 'user', '1001',这里的 1001 就是 RowKey
简单查询
# 根据 RowKey 精确查询
hbase:015:0> get 'user', 'row1'
COLUMN CELL
info:age timestamp=2026-04-12T04:19:47.856, value=20
info:name timestamp=2026-04-12T04:19:42.044, value=zhangsan
1 row(s)
Took 0.0119 seconds
# 只查某个列族
hbase:016:0> get 'user', 'row1', {COLUMNS => 'info'}
COLUMN CELL
info:age timestamp=2026-04-12T04:19:47.856, value=20
info:name timestamp=2026-04-12T04:19:42.044, value=zhangsan
1 row(s)
Took 0.0126 seconds
# 只查指定列
hbase:017:0> get 'user', 'row1', {COLUMNS => 'info:name'}
COLUMN CELL
info:name timestamp=2026-04-12T04:19:42.044, value=zhangsan
1 row(s)
Took 0.0242 seconds
全表扫描 scan
scan 'user'
# 限制条数
scan 'user', {LIMIT => 2}
# RowKey 范围查询 [start, stop)
scan 'user', {STARTROW => 'row1', ENDROW => 'row3'}
查看版本 / 历史数据
# 获取多个版本
get 'user', 'row1', {VERSIONS => 3}
Hbase 不支持 SQL、join、复杂查询,适合海量日志、用户画像、行为明细、Feeds 流
删除数据
删除必须加列族,删除某一列的值
# 删除某一行的某一列(最小粒度),只删一个字段
delete 'user', 'row1', 'info:age'
# 删除整行 deleteall '表名', 'RowKey',删除这一行所有列族、所有列,deleteall 不会删表
deleteall 'user', 'row1'
# 删除某个列 + 指定版本(几乎不用)
delete 'user', 'row1', 'info:age', 1712345678999
这里删除不是把数据擦掉,而是追加一条标记,这行数据已删除,查询时忽略它
如果想让删除的数据彻底消失,执行合并:major_compact 'user',这命令生产不要随便执行,非常耗 IO
RowKey 设计
HBase 数据是按 RowKey 字典序 排序的,一段连续的 RowKey 会落在 同一个 RegionServer 上。
查询是根据RowKey来的,执行:get 'user', 'row1',客户端去找 HMaster 问:row1 在哪?
HMaster 会告诉你:在 RegionServer 2,那么客户端直接连接 RegionServer 2 拿数据,而HMaster 全程不碰数据
如果针对某些RowKey的所有写入、查询都打在 一台机器上,其他空闲,那么这些RowKey就是热点。
RowKey 设计必须满足两点:
- 查询能用到(能根据业务查出来)
- 均匀打散到所有节点(不热点)
常见设计方案,本质上都是对原始RowKey做变换,以适配不同的场景
加盐(Salting)—— 最简单通用
前面加随机前缀对原始RowKey进行变换,前缀用 09、0 总数 等随机数,强制打散到不同 Region
user001 -> 9_user001
user002 -> 5_user002
user003 -> 7_user003
user004 -> 2_user004
user005 -> 6_user005
优点:简单、打散效果极好
缺点:不能范围查询,因为加盐把原本连续的 RowKey 彻底打乱了,连续的数据散落在不同前缀下。只有 RowKey 连续、有序 才能范围查
哈希 / MD5 截取
原理:对 ID 做 Hash,取前几位,离散度高,无业务含义,纯打散
适合:只做 K-V 查询,同样不做范围查询
反转(Reversing)—— 解决自增 ID 热点
将key进行反转,像下面这样
100001 → 100001
100002 → 200001
100003 → 300001
将连续变成不连续了,虽然反转前后都是有序的,反转不是为了变无序,而是把前面连续变成前面不连续
支持范围查询:比如查 ID 100001 ~ 100003,反转后的范围是100001 ~ 300001
适用场景:ID 自增、时间戳、订单号连续
拼接业务字段(最常用业务设计)
RowKey = 前缀 + 业务 ID + 时间
userid_行为类型_时间
uid001_click_202505011200
优点:
可以查某个用户的所有行为,可以查某个用户某段时间行为,天然是离散的
时间戳后补随机数
如果你直接用 时间戳 做 RowKey,那么会造成热点:全部连续 → 全部打到同一个 Region
这个方案简单说就是把随机数 放到 RowKey 最前面,时间戳放在后面,前缀打散热点,后缀保留范围查询
20250501120000 -> 03_20250501120000
20250501120001 -> 17_20250501120001
20250501120002 -> 09_20250501120002
前缀不同 → 数据打散到所有节点 → 无热点
后面还是时间戳 → 可以按时间范围查询
这个方案和 加盐(随机前缀) 的唯一区别:
加盐:完全乱序 → 不能范围查
时间戳 + 随机前缀:前缀打散,后面有序 → 可以范围查
随机数前缀
范围查询实现原理
前缀虽然是随机的,但是范围是知道的,查询的时候进行拼接即可,比如想查 2025-05-01 12:00 ~ 12:10 的数据
:
scan 'log_table', {
STARTROW => '00_20250501120000',
ENDROW => '99_20250501121000'
}
规避热点Key
- 禁止纯自增 ID 作为 RowKey
- 禁止纯时间戳作为 RowKey
- 前缀必须能打散数据
- RowKey 越短越好(不要超过 64 字节)
- 能哈希就哈希,能反转就反转
Hbase架构

浙公网安备 33010602011771号