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;对于行存储数据库的步骤:

  1. 找到那一行
  2. 直接把 age 从 NULL 改成 26
  3. 结束

因为整行是连在一起的,改一个字段只需要改一小块。而对于列存储数据库步骤如下:

  1. 找到 id=2 对应的行号(比如第 2 行)
  2. 打开 age 列文件
  3. 修改 age 第 2 个值(还要改 NULL 位图)
  4. 重写整个 age 数据块
  5. 刷盘

这还只是改一个字段,如果一行有 50 个字段,改 1 个字段,也要:定位行号,打开对应列文件,重写整个数据块,刷盘。成本是行存的几十倍。

如果要 DELETE 某一行?DELETE FROM user WHERE id=2;,对于列存储数据库必须:

  1. 找到行号
  2. 打开 所有列文件
  3. 把每一列里第 2 个位置删掉
  4. 后面的数据全部往前挪
  5. 重新生成位图
  6. 重写所有列文件
    这相当于全表重写。所以列存天生不适合频繁删改,只适合批量写入 + 大量分析。

列存为了压缩,都是一批数据打包成一个大 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)

总结

列存数据库的优缺点非常明显

优点

  1. 批量插入极快
  2. 按列压缩强,空间极小
  3. 分析查询(统计、聚合)飞快

缺点(也是为什么它不适合做业务库)

  1. 单条插入很慢(要等攒批、排序、重组)
  2. 更新、删除极弱(很多列存不支持行级更新)
  3. 小批量频繁插入会产生大量小文件,性能暴跌
操作 行存储 (MySQL、InnoDB) 列存储 (HBase、Parquet、ClickHouse)
单行 UPDATE 极快,只修改当前行数据 极慢,需要重写整个列数据块
单行 DELETE 极快,直接删除当前行 极慢,需要重写所有相关列数据块
批量插入 极快
统计分析(sum/avg 等) 慢,需要读取整行数据 极快,只读取目标列即可

Hbase集群架构

详细介绍参考:Apache HBase 架构详解

HBase集群角色主要包括 Master(HMaster)、RegionServer(HRegionServer)、ZooKeeper 和 Client。

  1. HMaster (Master角色):管理元数据和分配Region
    职责:集群的管理者。处理表级别的DDL操作(创建、修改、删除表),监控RegionServer状态,处理Region的分配或故障转移(Failover),维护元数据。
    高可用:通常会部署多个HMaster,由ZooKeeper选举出一个Active Master,其他为Backup Master。

  2. HRegionServer (RegionServer角色):负责数据读写
    职责:实际干活的节点。直接对接用户读写请求(DML),处理分配给它的Region,负责数据的分片(Region)管理、刷新缓存到HDFS以及处理Compact操作。表被切分成很多 Region,Region 就存在 RegionServer 里,它负责管理数据文件、索引、缓存,以及处理所有读写请求

  3. ZooKeeper (协调服务):协调节点高可用和状态监控
    职责:集群的管家。实现Active Master的选举,监控RegionServer上下线状态并通过回调机制通知Master,存储HBase的元数据入口(如.META.表位置)。

  4. Client (客户端):用于访问HBase服务
    职责:访问HBase接口。Client通过RPC机制与Master和RegionServer通信,通常会缓存元数据以加速访问。

  5. HDFS (存储底层)
    职责:虽非HBase进程,但HBase依赖HDFS存储实际数据。

HBase安装

参考:HBase环境搭建

单机模式(Standalone):完全不用 HDFS

所有进程(Master/RegionServer/ZooKeeper)跑在一个 JVM
轻量、开箱即用、适合学习 / 开发
缺点:无分布式、无副本、无高可用、数据不可靠、性能有限 → 不能用于生产

下载:https://archive.apache.org/dist/hbase/

  1. 安装及配置

安装:下面贴一个 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"
  1. 配置环境变量

手动执行:source /etc/profile.d/hbase.sh,让环境变量生效

  1. 关闭日志

不操作这一步,执行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,注意替换相应的命令

  1. 验证启动成功

启动后发现启动了一个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

image

  1. 停止 Hbase

执行安装目录下的脚本:stop-hbase.sh

使用

hbase shell 是一个命令行客户端,命令行可以使用tab进行补全

hbase:001:0> 的含义:

  1. hbase:固定开头,表示这是 HBase Shell
  2. (main):表示在 main 命名空间(可以忽略,不用管),这个刚进入shell时不会出现
  3. 001:这是你已经输入的命令序号(第几条命令),每执行一条命令,数字 +1
  4. 0:上一条命令的返回状态码,0 = 执行成功,非 0 = 执行失败

基本概念

关系型数据库(MySQL) HBase 说明
数据库(Database) 命名空间(Namespace) 类似分组、隔离权限
表(Table) 表(Table) 基本存储单元
行(Row) 行(Row) RowKey 唯一标识
主键(Primary Key) RowKey 字典序排序,查询只能按 RowKey
列(Column) 列(Column)= 列族:限定符 列可以动态随意加
列定义(Schema) 只定义列族 列不用提前定义
字段类型 全部是 字节数组 bytes 无类型,自己解析
事务 强事务 仅行级原子性
查询 SQL 丰富 只能:全表扫 /get/scan
存储结构 行存储 列簇存储(面向列)
扩展性 垂直扩展 水平线性扩展

HBase 核心概念详解

  1. RowKey(行键)
    相当于主键
    字典序排序
    查询只能:根据 RowKey 精确查,根据 RowKey 范围查
    全表扫描
    设计非常关键(散列、避免热点)
  2. Column Family(列族)
    建表时必须指定,物理上在一起存储,列族不宜多,一般 1~3 个
  3. Column Qualifier(列限定符)
    属于某个列族:info:name、info:age
    动态添加,无需预先定义,一行有 10 列,另一行可以有 100 列
  4. Cell(单元格)
    由 表 + RowKey + 列族:列 + 版本号 确定
    存储的是 二进制字节数组,自带多版本(默认保留 3 个)
  5. TimeStamp(版本 / 时间戳)
    每次 put 自动生成版本,可用于数据回溯

命名空间

HBase 的命名空间 = MySQL 的数据库(Database),就是一个分组 / 隔离的概念。mysql 里先建库然后建表,hbase 里是一样的,先建namespace后建表。

HBase 自带两个默认命名空间,和mysql也一样,默认有information_shcema,mysql等库

  1. default:不写命名空间,表就放这里,create 'user' 等价于 create 'default:user'
  2. 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,它的设计原则是:

  1. 只存字节,不理解数据含义
  2. 无 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 设计必须满足两点:

  1. 查询能用到(能根据业务查出来)
  2. 均匀打散到所有节点(不热点)

常见设计方案,本质上都是对原始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

  1. 禁止纯自增 ID 作为 RowKey
  2. 禁止纯时间戳作为 RowKey
  3. 前缀必须能打散数据
  4. RowKey 越短越好(不要超过 64 字节)
  5. 能哈希就哈希,能反转就反转
    Hbase架构
posted @ 2026-04-12 17:19  vonlinee  阅读(3)  评论(0)    收藏  举报