HBase 分布式数据库 --- 完整知识点与案例代码
一、HBase 概述
1.1 HBase 的技术特点
HBase(Hadoop Database)是一个分布式的、面向列的开源数据库,基于 Google Bigtable 的设计思想构建。
核心技术特点:
| 特点 | 说明 |
|---|---|
| 海量存储 | 可存储数十亿行 × 数百万列的数据 |
| 面向列 | 按列族存储,同一列族的数据连续存放 |
| 稀疏性 | 空白单元格不占用存储空间 |
| 多版本 | 每个单元格可保存多个版本的数据,用时间戳区分 |
| 强一致性 | 同一行的读写操作是原子性的 |
| 高可靠性 | 基于 HDFS 实现数据自动备份 |
| 线性扩展 | 可通过增加 RegionServer 节点水平扩展 |
1.2 HBase 与传统关系数据库的区别
┌──────────────┬──────────────────────┬──────────────────────┐
│ 比较项 │ 传统关系数据库(RDBMS)│ HBase │
├──────────────┼──────────────────────┼──────────────────────┤
│ 数据类型 │ 丰富的数据类型 │ 只有字节数组(byte[]) │
│ 数据操作 │ SQL(增删改查+JOIN) │ 只有简单的Get/Put/Scan/Delete │
│ 存储模式 │ 基于行存储 │ 基于列族存储 │
│ 数据索引 │ 支持多级索引 │ 只有行键(RowKey)索引 │
│ 事务支持 │ 完整的ACID事务 │ 仅行级别的原子性 │
│ 扩展方式 │ 纵向扩展(升级硬件) │ 横向扩展(增加节点) │
│ 数据量级别 │ 百万~千万级 │ 十亿~百亿级 │
│ 稀疏数据 │ 用NULL填充,浪费空间 │ 空白不存储,节省空间 │
└──────────────┴──────────────────────┴──────────────────────┘
1.3 HBase 与 Hadoop 中其他组件的关系
┌─────────┐
│ HBase │ ← 提供实时读写能力
└────┬────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ HDFS │ │MapReduce │ │ ZooKeeper│
│ (底层存储)│ │ (批量计算)│ │ (协调服务) │
└──────────┘ └──────────┘ └──────────┘
- HDFS:HBase 的底层存储系统,所有数据最终存放在 HDFS 上
- MapReduce:HBase 可作为 MapReduce 的数据源和数据目标
- ZooKeeper:负责 HBase 的 Master 选举、RegionServer 监控、元数据管理
二、HBase 系统架构和数据访问流程
2.1 HBase 系统架构
┌──────────────────────────────────────────────────────────┐
│ Client │
│ (HBase Shell / Java API / REST) │
└────────────────────────┬─────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ ZooKeeper │ │ ZooKeeper │ │ ZooKeeper │ ← 集群协调
│ Server 1 │ │ Server 2 │ │ Server 3 │
└────────────┘ └────────────┘ └────────────┘
│
▼
┌────────────┐
│ HBase Master│ ← 管理Region分配、DDL操作
└────────────┘
│
┌──────┼──────┬──────────────┐
▼ ▼ ▼ ▼
┌──────┐┌──────┐┌──────┐ ┌──────────┐
│Region││Region││Region│ │HLog(WAL) │
│Server││Server││Server│ │预写日志 │
│ 1 ││ 2 ││ 3 │ └──────────┘
└──┬───┘└──┬───┘└──┬───┘
│ │ │
▼ ▼ ▼
┌──────────────────────────┐
│ HDFS │ ← 底层分布式文件存储
│ (DataNode 1, 2, 3...) │
└──────────────────────────┘
各组件功能:
| 组件 | 功能 |
|---|---|
| Master | 负责 Region 的分配、DDL 操作(创建/删除表)、负载均衡 |
| RegionServer | 负责处理数据的读写请求,管理多个 Region |
| Region | 表中数据按行键范围划分的子表,分布式存储的基本单位 |
| ZooKeeper | 存储元数据表位置、Master 地址、RegionServer 状态 |
| HLog (WAL) | 预写日志,保证数据写入的可靠性,防止内存数据丢失 |
| MemStore | 内存中的写缓存,数据先写入 MemStore,满后刷写到 HFile |
| StoreFile (HFile) | 磁盘上的存储文件,实际存储在 HDFS 上 |
2.2 HBase 数据访问流程
写数据流程:
Client → ZooKeeper(获取Meta表位置) → Meta表(定位RegionServer)
→ RegionServer → WAL(预写日志) → MemStore(内存缓存)
→ MemStore满时 → 刷写到StoreFile(HFile)
读数据流程:
Client → ZooKeeper(获取Meta表位置) → Meta表(定位RegionServer)
→ RegionServer → BlockCache(读缓存,命中则返回)
→ 未命中 → MemStore → StoreFile(HFile)
→ 合并结果返回Client
三、HBase 数据表
3.1 HBase 数据表逻辑视图
HBase 表由 行键(RowKey)、列族(Column Family)、列限定符(Qualifier)、时间戳(Timestamp)、值(Value) 五维组成。
RowKey │ Column Family: info │ Column Family: score
│ name age gender │ chinese math english
──────────┼────────────────────────────┼──────────────────────────
row_001 │ "Tom" "20" "M" │ "89" "95" "88"
row_002 │ "Jerry" "21" │ "76" "92"
row_003 │ "F" │ "90" "85"
关键概念:
- RowKey:行的唯一标识,按字典序排列,是 HBase 中唯一的索引
- Column Family:列族,必须在建表时预先定义
- Column Qualifier:列限定符,可以动态添加,无需提前定义
- Timestamp:每个单元格可存储多个版本,用时间戳区分
- Value:实际存储的数据,以字�组形式存储
3.2 HBase 数据表物理视图
在物理存储上,HBase 按 列族 分开存储:
StoreFile 1 (info列族):
row_001 → info:name = "Tom"
row_001 → info:age = "20"
row_002 → info:name = "Jerry"
row_002 → info:age = "21"
StoreFile 2 (score列族):
row_001 → score:chinese = "89"
row_001 → score:math = "95"
row_002 → score:english = "92"
每个列族对应一个 Store,Store 包含一个 MemStore 和多个 StoreFile。
3.3 HBase 数据表面向列的存储
面向行存储(RDBMS):
┌─────┬───────┬─────┬────────┬─────────┬───────┬─────────┐
│ ID │ Name │ Age │ Gender │ Chinese │ Math │ English │
│ 001 │ Tom │ 20 │ M │ 89 │ 95 │ 88 │
│ 002 │ Jerry │ 21 │ │ 76 │ │ 92 │
└─────┴───────┴─────┴────────┴─────────┴───────┴─────────┘
面向列存储(HBase):
info列族: score列族:
┌───────┬──────┬──────┐ ┌─────────┬──────┬─────────┐
│ RowKey│ name │ age │ │ RowKey │chinese│english │
│ 001 │ Tom │ 20 │ │ 001 │ 89 │ 88 │
│ 002 │ Jerry│ 21 │ │ 002 │ 76 │ 92 │
└───────┴──────┴──────┘ └─────────┴──────┴─────────┘
优势:
- 列式存储中,同列数据类型相同,压缩率更高
- 查询某几列时无需读取整行数据,I/O 更少
3.4 HBase 数据表的查询方式
HBase 提供四种基本查询操作:
| 操作 | 方法 | 说明 |
|---|---|---|
| Get | get(rowkey) |
根据行键获取单行数据 |
| Scan | scan() |
扫描表中多行数据,可设置起止行键和过滤器 |
| Put | put(row, cf, qualifier, value) |
写入/更新数据 |
| Delete | delete(row, cf, qualifier) |
删除数据 |
四、HBase 表结构设计
RowKey 设计原则
设计原则:
1. 长度原则:建议 10~100 字节,过长浪费存储
2. 唯一性原则:RowKey 必须唯一
3. 散列原则:避免热点问题(数据集中在一个 Region)
4. 业务原则:将最常用的查询条件放在 RowKey 中
避免热点的常见方法:
java
// 方法1:反转RowKey(将手机号后4位放前面)
// 原始: 13812345678 → 反转: 87654321831
// 方法2:加盐(在RowKey前加随机前缀)
// 原始: row001 → 加盐: a_row001, b_row001, c_row001
// 方法3:哈希(对RowKey取哈希值)
// 原始: user_001 → MD5哈希: 5d41402abc4b2...
五、HBase 的安装与配置
5.1 下载安装文件
bash
# 第一步:从Apache官网下载HBase安装包
wget https://archive.apache.org/dist/hbase/2.4.17/hbase-2.4.17-bin.tar.gz
# 第二步:解压到指定目录
tar -zxvf hbase-2.4.17-bin.tar.gz -C /usr/local/
# 第三步:重命名目录,方便后续操作
mv /usr/local/hbase-2.4.17 /usr/local/hbase
5.2 配置环境变量
bash
# 编辑系统环境变量配置文件
sudo vi /etc/profile
# 在文件末尾添加以下内容:
# 设置HBase的安装根目录
export HBASE_HOME=/usr/local/hbase
# 将HBase的bin目录添加到系统PATH中,使hbase命令全局可用
export PATH=$HBASE_HOME/bin:$PATH
# 使配置立即生效
source /etc/profile
5.3 添加用户权限
bash
# 确保当前用户对HBase目录有读写权限
sudo chown -R hadoop:hadoop /usr/local/hbase
# 验证权限
ls -ld /usr/local/hbase
# 输出类似: drwxr-xr-x 7 hadoop hadoop 4096 ...
5.4 查看HBase版本信息
bash
# 查看HBase版本
hbase version
# 输出示例:
# HBase 2.4.17
# Source code repository ...
# ...
5.5 单机模式配置
单机模式下 HBase 不依赖 HDFS 和 ZooKeeper,使用本地文件系统。
xml
<!-- 编辑文件:$HBASE_HOME/conf/hbase-site.xml -->
<configuration>
<!-- 指定HBase数据存储在本地文件系统的路径 -->
<property>
<name>hbase.rootdir</name>
<value>file:///usr/local/hbase/data</value>
</property>
<!-- 指定HBase的临时文件目录 -->
<property>
<name>hbase.tmp.dir</name>
<value>/usr/local/hbase/tmp</value>
</property>
</configuration>
bash
# 启动HBase
start-hbase.sh
# 验证是否启动成功
# 使用jps命令查看Java进程,应看到HMaster进程
jps
# 输出应包含:HMaster
# 进入HBase Shell
hbase shell
5.6 伪分布式模式配置
伪分布式模式下 HBase 使用 HDFS 存储数据,但所有进程运行在一台机器上。
xml
<!-- 编辑文件:$HBASE_HOME/conf/hbase-site.xml -->
<configuration>
<!-- 启用分布式模式(设为true表示运行在分布式环境) -->
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<!-- 指定HBase数据存储在HDFS上的路径 -->
<property>
<name>hbase.rootdir</name>
<value>hdfs://localhost:9000/hbase</value>
</property>
<!-- 指定ZooKeeper的数据目录 -->
<property>
<name>hbase.zookeeper.property.dataDir</name>
<value>/usr/local/zookeeper/data</value>
</property>
<!-- 指定ZooKeeper的端口号 -->
<property>
<name>hbase.zookeeper.property.clientPort</name>
<value>2181</value>
</property>
</configuration>
bash
# 确保HDFS已启动
start-dfs.sh
# 确保ZooKeeper已启动(HBase自带ZooKeeper,也可使用独立的)
# 如果使用独立ZooKeeper:
# zkServer.sh start
# 启动HBase
start-hbase.sh
# 验证各进程
jps
# 应看到:HMaster、HRegionServer、HQuorumPeer(自带ZK)等进程
六、HBase 的 Shell 操作
6.1 基本操作
bash
# 进入HBase Shell交互环境
hbase shell
# 查看所有可用命令帮助
help
# 查看集群状态
status
# 输出示例:1 active master, 0 backup masters, 1 servers, 0 dead, 3.0000 average load
# 查看当前所有表
list
# 输出示例:TABLE → student, teacher ...
# 退出HBase Shell
exit
6.2 创建表
bash
# 语法:create '表名', '列族1', '列族2', ...
# 创建student表,包含info和score两个列族
create 'student', 'info', 'score'
# 创建表时指定列族属性(版本数、压缩方式等)
# versions => 3 表示该列族最多保存3个版本的数据
# compression => 'SNAPPY' 表示使用Snappy压缩算法
create 'employee', {NAME => 'basic_info', VERSIONS => 3}, {NAME => 'work_info', BLOOMFILTER => 'ROW'}
# 查看表的描述信息
describe 'student'
# 输出示例:
# Table student is ENABLED
# student
# COLUMN FAMILIES DESCRIPTION
# {NAME => 'info', VERSIONS => '1', ...}
# {NAME => 'score', VERSIONS => '1', ...}
# 验证表是否存在
exists 'student'
# 输出示例:Table student does exist
6.3 插入与更新表中的数据
bash
# 语法:put '表名', '行键', '列族:列限定符', '值'
# ---- 插入第一条记录 ----
# 插入学生Tom的基本信息
put 'student', 'row_001', 'info:name', 'Tom'
put 'student', 'row_001', 'info:age', '20'
put 'student', 'row_001', 'info:gender', 'Male'
# 插入学生Tom的成绩信息
put 'student', 'row_001', 'score:chinese', '89'
put 'student', 'row_001', 'score:math', '95'
put 'student', 'row_001', 'score:english', '88'
# ---- 插入第二条记录 ----
put 'student', 'row_002', 'info:name', 'Jerry'
put 'student', 'row_002', 'info:age', '21'
put 'student', 'row_002', 'info:gender', 'Male'
put 'student', 'row_002', 'score:chinese', '76'
put 'student', 'row_002', 'score:math', '82'
put 'student', 'row_002', 'score:english', '92'
# ---- 插入第三条记录 ----
put 'student', 'row_003', 'info:name', 'Alice'
put 'student', 'row_003', 'info:age', '20'
put 'student', 'row_003', 'info:gender', 'Female'
put 'student', 'row_003', 'score:chinese', '90'
put 'student', 'row_003', 'score:math', '85'
# ---- 更新操作 ----
# 更新操作本质上也是put操作,新值会覆盖旧值(或作为新版本保留)
# 将Tom的数学成绩从95修改为98
put 'student', 'row_001', 'score:math', '98'
# 此时若列族配置了versions>1,则两条记录都会保留
# 最新版本的数据在查询时默认返回
6.4 查看表中的数据
bash
# ==== 方式1:get命令 ------ 获取单行数据 ====
# 语法:get '表名', '行键'
# 获取row_001的所有列族所有数据
get 'student', 'row_001'
# 输出示例:
# COLUMN CELL
# info:age timestamp=..., value=20
# info:gender timestamp=..., value=Male
# info:name timestamp=..., value=Tom
# score:chinese timestamp=..., value=89
# score:english timestamp=..., value=88
# score:math timestamp=..., value=98
# 获取row_001的info列族的所有列
get 'student', 'row_001', 'info'
# 获取row_001的info列族的name列
get 'student', 'row_001', 'info:name'
# 获取row_001的多个指定列
get 'student', 'row_001', {COLUMN => ['info:name', 'score:math']}
# 获取row_001的最近2个版本的数据(需要列族配置VERSIONS>1)
get 'student', 'row_001', {COLUMN => 'score:math', VERSIONS => 2}
# ==== 方式2:scan命令 ------ 扫描多行数据 ====
# 语法:scan '表名', {可选参数}
# 扫描整张表的所有数据
scan 'student'
# 扫描指定列族
scan 'student', {COLUMNS => 'info'}
# 扫描指定列
scan 'student', {COLUMNS => ['info:name', 'score:chinese']}
# 限制返回的行数(LIMIT)
scan 'student', {LIMIT => 2}
# 设置扫描的起始行键(STARTROW)和结束行键(STOPROW)
# 注意:STARTROW是包含的,STOPROW是不包含的
scan 'student', {STARTROW => 'row_001', STOPROW => 'row_003'}
# 反向扫描(REVERSED => true)
scan 'student', {REVERSED => true}
# 设置返回的版本数
scan 'student', {VERSIONS => 3}
# 只显示行键(RAW => true + LIMIT)
scan 'student', {RAW => true, LIMIT => 3}
6.5 删除表中的数据
bash
# ==== 删除指定单元格 ====
# 语法:delete '表名', '行键', '列族:列限定符'
# 注意:删除前必须先禁用表 或 直接delete(2.x版本支持直接删除单元格)
# 删除row_002的英语成绩
delete 'student', 'row_002', 'score:english'
# ==== 删除指定版本的数据 ====
# delete指定时间戳版本
delete 'student', 'row_001', 'score:math', 1634567890123
# ==== 删除整行数据 ====
# 语法:deleteall '表名', '行键'
# 删除row_003的所有数据(所有列族、所有列)
deleteall 'student', 'row_003'
# 删除row_001的某一列的所有版本
deleteall 'student', 'row_001', 'info:age'
# 删除时指定时间戳
deleteall 'student', 'row_001', 'info:gender', {TIMESTAMP => 1634567890123}
# ==== 清空整张表 ====
# 语法:truncate '表名'
# truncate会先禁用表、删除表、再重新创建表
truncate 'student'
6.6 表的启用/禁用
bash
# HBase中,修改表结构、删除表之前,必须先禁用表
# 查看表的状态(ENABLED 或 DISABLED)
is_enabled 'student'
is_disabled 'student'
# 禁用表
disable 'student'
# 启用表
enable 'student'
# 禁用所有表(慎用!)
# disable_all '.*'
# 启用所有表
# enable_all '.*'
6.7 修改表结构
bash
# ==== 修改列族属性 ====
# 语法:alter '表名', {NAME => '列族名', 属性 => 值}
# 将info列族的最大版本数改为5
alter 'student', {NAME => 'info', VERSIONS => 5}
# 为info列族设置数据存活时间(TTL),单位为秒
# 31536000秒 = 1年,超过时间的数据会被自动清理
alter 'student', {NAME => 'info', TTL => 31536000}
# ==== 新增列族 ====
# 为student表新增一个address列族
alter 'student', {NAME => 'address'}
# 同时新增多个列族
alter 'student', {NAME => 'contact', VERSIONS => 2}, {NAME => 'remark'}
# ==== 删除列族 ====
# 删除student表的address列族
alter 'student', {NAME => 'address', METHOD => 'delete'}
# 另一种删除列族的写法
alter 'student', 'delete' => 'address'
6.8 删除HBase表
bash
# HBase中删除表必须先禁用再删除
# 第一步:禁用表
disable 'student'
# 第二步:删除表
drop 'student'
# 验证表已删除
exists 'student'
# 输出:Table student does not exist
# 删除所有匹配正则表达式的表(慎用!)
# drop_all 'temp_.*'
七、HBase 的 Java API 操作
7.1 HBase 数据库管理 API(Admin 管理类)
java
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.HColumnDescriptor;
import java.io.IOException;
/**
* HBaseAdminDemo - 演示HBase Admin管理API
* 功能:创建表、删除表、修改表结构、查看表信息等DDL操作
*/
public class HBaseAdminDemo {
// 定义HBase连接和Admin对象(类级别,便于各方法复用)
private static Connection connection = null; // HBase连接对象
private static Admin admin = null; // HBase管理对象
/**
* 获取HBase连接和Admin实例
* 在所有操作之前必须先调用此方法建立连接
*/
public static void init() throws IOException {
// 第一步:创建HBase配置对象
// HBaseConfiguration.create() 会自动加载 hbase-site.xml 配置文件
Configuration conf = HBaseConfiguration.create();
// 第二步:设置ZooKeeper的连接地址
// HBase通过ZooKeeper来协调集群,客户端需要知道ZK的地址才能连接HBase
conf.set("hbase.zookeeper.quorum", "localhost");
// 第三步:设置ZooKeeper的端口号(默认2181)
conf.set("hbase.zookeeper.property.clientPort", "2181");
// 第四步:通过配置信息创建HBase连接(重量级操作,应复用)
connection = ConnectionFactory.createConnection(conf);
// 第五步:从连接中获取Admin对象,用于执行DDL操作
admin = connection.getAdmin();
}
/**
* 关闭HBase连接和Admin实例
* 在所有操作完成后必须调用此方法释放资源
*/
public static void close() throws IOException {
// 先关闭Admin对象
if (admin != null) {
admin.close();
}
// 再关闭连接对象
if (connection != null) {
connection.close();
}
}
/**
* 创建表
* @param tableName 表名
* @param columnFamilies 列族名称数组(可传入多个列族)
*/
public static void createTable(String tableName, String[] columnFamilies) throws IOException {
// 第一步:将String类型的表名转换为TableName对象
TableName table = TableName.valueOf(tableName);
// 第二步:判断表是否已存在,避免重复创建
if (admin.tableExists(table)) {
System.out.println("表 " + tableName + " 已存在!"); // 提示表已存在
return; // 直接返回,不重复创建
}
// 第三步:创建表描述器对象(HBase 1.x/2.x API)
HTableDescriptor tableDescriptor = new HTableDescriptor(table);
// 第四步:遍历列族数组,为表添加列族描述
for (String cf : columnFamilies) {
// 创建列族描述器,指定列族名称
HColumnDescriptor columnDescriptor = new HColumnDescriptor(cf);
// 设置列族最大版本数为5(该列族最多保存5个版本的数据)
columnDescriptor.setMaxVersions(5);
// 将列族描述器添加到表描述器中
tableDescriptor.addFamily(columnDescriptor);
}
// 第五步:调用Admin的createTable方法执行建表操作
admin.createTable(tableDescriptor);
// 第六步:验证表是否创建成功
if (admin.tableExists(table)) {
System.out.println("表 " + tableName + " 创建成功!");
}
}
/**
* 删除表
* @param tableName 要删除的表名
*/
public static void deleteTable(String tableName) throws IOException {
// 将表名字符串转换为TableName对象
TableName table = TableName.valueOf(tableName);
// 第一步:检查表是否存在
if (!admin.tableExists(table)) {
System.out.println("表 " + tableName + " 不存在,无法删除!");
return;
}
// 第二步:删除表之前必须先禁用表(HBase强制要求)
// 如果表处于ENABLED状态,直接调用deleteTable会抛出异常
if (admin.isTableEnabled(table)) {
admin.disableTable(table); // 禁用表
System.out.println("表 " + tableName + " 已禁用");
}
// 第三步:执行删除操作
admin.deleteTable(table);
System.out.println("表 " + tableName + " 已成功删除!");
}
/**
* 修改表结构 ------ 为表添加新的列族
* @param tableName 表名
* @param newFamily 要新增的列族名称
*/
public static void addColumnFamily(String tableName, String newFamily) throws IOException {
TableName table = TableName.valueOf(tableName);
// 检查表是否存在
if (!admin.tableExists(table)) {
System.out.println("表 " + tableName + " 不存在!");
return;
}
// 创建列族描述器
HColumnDescriptor newColumn = new HColumnDescriptor(newFamily);
// 使用TableDescriptorBuilder构建修改后的表描述器
// HBase 2.x 推荐使用TableDescriptorBuilder
admin.modifyColumnFamily(table, newColumn);
// 或者使用以下方式修改表结构(新增列族):
// admin.addColumnFamily(table, newColumn);
// 注意:HBase 2.x API 中 modifyColumnFamily 用于修改已有列族属性
// 新增列族应使用 admin.addColumnFamily()
admin.addColumnFamily(table, newColumn);
System.out.println("列族 " + newFamily + " 添加成功!");
}
/**
* 修改表结构 ------ 删除表中的指定列族
* @param tableName 表名
* @param familyName 要删除的列族名称
*/
public static void deleteColumnFamily(String tableName, String familyName) throws IOException {
TableName table = TableName.valueOf(tableName);
// 检查表是否存在
if (!admin.tableExists(table)) {
System.out.println("表 " + tableName + " 不存在!");
return;
}
// 删除指定列族(需要将列族名转换为byte[])
admin.deleteColumnFamily(table, Bytes.toBytes(familyName));
System.out.println("列族 " + familyName + " 删除成功!");
}
/**
* 列出所有表名
*/
public static void listTables() throws IOException {
// 获取所有表的描述器数组
HTableDescriptor[] tables = admin.listTables();
System.out.println("===== HBase中的所有表 =====");
// 遍历并打印每张表的名称
for (HTableDescriptor table : tables) {
System.out.println("表名:" + table.getNameAsString());
}
}
/**
* 查看表的详细描述信息
* @param tableName 表名
*/
public static void describeTable(String tableName) throws IOException {
TableName table = TableName.valueOf(tableName);
// 检查表是否存在
if (!admin.tableExists(table)) {
System.out.println("表 " + tableName + " 不存在!");
return;
}
// 获取表的描述器
HTableDescriptor tableDescriptor = admin.getTableDescriptor(table);
System.out.println("表名:" + tableDescriptor.getNameAsString());
// 获取表中所有列族的描述器
HColumnDescriptor[] columnFamilies = tableDescriptor.getColumnFamilies();
// 遍历并打印每个列族的详细信息
for (HColumnDescriptor cf : columnFamilies) {
System.out.println(" 列族名:" + cf.getNameAsString()); // 列族名称
System.out.println(" 最大版本数:" + cf.getMaxVersions()); // 最大版本数
System.out.println(" 压缩方式:" + cf.getCompressionType()); // 压缩算法
System.out.println(" TTL:" + cf.getTimeToLive()); // 数据存活时间
System.out.println(" ---");
}
}
/**
* 主方法 ------ 演示完整的Admin操作流程
*/
public static void main(String[] args) {
try {
// 1. 初始化连接
init();
// 2. 创建名为"students"的表,包含info和score两个列族
createTable("students", new String[]{"info", "score"});
// 3. 列出所有表
listTables();
// 4. 查看students表的详细信息
describeTable("students");
// 5. 为students表添加一个新列族address
addColumnFamily("students", "address");
// 6. 再次查看表结构,确认address列族已添加
describeTable("students");
// 7. 删除address列族
deleteColumnFamily("students", "address");
// 8. 删除students表
// deleteTable("students");
// 9. 关闭连接
close();
} catch (IOException e) {
// 捕获并打印IO异常
e.printStackTrace();
}
}
}
7.2 HBase 数据库表 API(Connection 和 Table 接口)
java
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import java.io.IOException;
/**
* HBaseTableDemo - 演示HBase的Connection和Table接口
* 功能:获取连接、获取表对象、获取Region信息等
*/
public class HBaseTableDemo {
public static void main(String[] args) throws IOException {
// ========== 第一步:创建HBase配置 ==========
// HBaseConfiguration.create() 方法会自动读取classpath下的hbase-site.xml
Configuration conf = HBaseConfiguration.create();
// 手动设置ZooKeeper集群地址(如果hbase-site.xml中未配置)
// 多个ZK地址用逗号分隔,如"zk1:2181,zk2:2181,zk3:2181"
conf.set("hbase.zookeeper.quorum", "localhost");
// 设置ZK客户端端口
conf.set("hbase.zookeeper.property.clientPort", "2181");
// ========== 第二步:创建Connection连接 ==========
// ConnectionFactory.createConnection() 创建一个与HBase集群的连接
// 注意:Connection是重量级对象,整个应用应只创建一个,后续复用
Connection connection = ConnectionFactory.createConnection(conf);
System.out.println("HBase连接创建成功!");
System.out.println("连接状态:" + (connection.isClosed() ? "已关闭" : "正常"));
// ========== 第三步:获取Table对象 ==========
// 通过Connection.getTable()获取指定表的操作句柄
// Table是轻量级对象,用于执行数据的增删改查操作
TableName tableName = TableName.valueOf("students");
Table table = connection.getTable(tableName);
System.out.println("获取表对象成功:" + table.getName());
// ========== 第四步:获取表名信息 ==========
System.out.println("表的限定名:" + table.getName().getNameAsString());
// getNameAsString() 返回完整的表名字符串
// ========== 第五步:获取Region信息 ==========
// 通过Connection获取RegionLocator,查看表在集群中的Region分布
RegionLocator regionLocator = connection.getRegionLocator(tableName);
// 获取表的所有Region信息
// 每个HRegionInfo包含Region的起始行键、结束行键、RegionServer地址等
for (HRegionLocation location : regionLocator.getAllRegionLocations()) {
System.out.println("Region名称:" + location.getRegion().getEncodedName());
System.out.println("RegionServer地址:" + location.getHostname());
System.out.println("Region端口:" + location.getPort());
System.out.println("起始行键:" + Bytes.toString(location.getRegion().getStartKey()));
System.out.println("结束行键:" + Bytes.toString(location.getRegion().getEndKey()));
System.out.println("---");
}
// ========== 第六步:获取Admin对象并查看集群信息 ==========
Admin admin = connection.getAdmin();
// 获取集群ID
System.out.println("集群ID:" + admin.getClusterId());
// 获取HBase Master地址
System.out.println("Master地址:" + admin.getMaster().getServerName());
// 获取正在运行的RegionServer列表
ServerName[] servers = admin.getRegionServers().toArray(new ServerName[0]);
for (ServerName server : servers) {
System.out.println("RegionServer:" + server.getHostname() + ":" + server.getPort());
}
// ========== 第七步:释放资源 ==========
// 按照创建的反顺序关闭,先关闭Table,再关闭Connection
regionLocator.close(); // 关闭Region定位器
table.close(); // 关闭表对象
admin.close(); // 关闭Admin
connection.close(); // 关闭连接
System.out.println("所有资源已释放!");
}
}
7.3 HBase 数据库表行列 API(CRUD 操作)
java
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.filter.*;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* HBaseCRUDDemo - 演示HBase的完整CRUD操作
* 包括:单条插入、批量插入、单条查询、扫描查询、过滤器查询、更新、删除等
*/
public class HBaseCRUDDemo {
private static Connection connection = null; // HBase连接对象
private static Table table = null; // 表操作对象
/**
* 初始化连接
*/
public static void init() throws IOException {
// 创建HBase配置
Configuration conf = HBaseConfiguration.create();
// 设置ZooKeeper地址
conf.set("hbase.zookeeper.quorum", "localhost");
conf.set("hbase.zookeeper.property.clientPort", "2181");
// 创建连接
connection = ConnectionFactory.createConnection(conf);
// 获取表对象(操作students表)
table = connection.getTable(TableName.valueOf("students"));
}
/**
* 关闭连接,释放资源
*/
public static void close() throws IOException {
if (table != null) table.close();
if (connection != null) connection.close();
}
// ==================== 插入数据 ====================
/**
* 插入单条数据(Put操作)
*/
public static void putData() throws IOException {
System.out.println("===== 插入单条数据 =====");
// 第一步:创建Put对象,参数为行键(byte[]类型)
// Bytes.toBytes() 将字符串转换为字节数组
Put put = new Put(Bytes.toBytes("row_001"));
// 第二步:向Put对象中添加列数据
// addColumn(列族, 列限定符, 值)
put.addColumn(
Bytes.toBytes("info"), // 列族名
Bytes.toBytes("name"), // 列限定符
Bytes.toBytes("Tom") // 值
);
// 继续添加其他列
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes("20"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("gender"), Bytes.toBytes("Male"));
put.addColumn(Bytes.toBytes("score"), Bytes.toBytes("chinese"), Bytes.toBytes("89"));
put.addColumn(Bytes.toBytes("score"), Bytes.toBytes("math"), Bytes.toBytes("95"));
put.addColumn(Bytes.toBytes("score"), Bytes.toBytes("english"), Bytes.toBytes("88"));
// 第三步:执行Put操作,将数据写入HBase
table.put(put);
System.out.println("row_001 数据插入成功!");
}
/**
* 批量插入数据(Batch Put操作)
*/
public static void putBatchData() throws IOException {
System.out.println("===== 批量插入数据 =====");
// 创建一个Put列表,用于存放多条Put操作
List<Put> putList = new ArrayList<>();
// ---- 构造row_002的数据 ----
Put put2 = new Put(Bytes.toBytes("row_002"));
put2.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("Jerry"));
put2.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes("21"));
put2.addColumn(Bytes.toBytes("info"), Bytes.toBytes("gender"), Bytes.toBytes("Male"));
put2.addColumn(Bytes.toBytes("score"), Bytes.toBytes("chinese"), Bytes.toBytes("76"));
put2.addColumn(Bytes.toBytes("score"), Bytes.toBytes("math"), Bytes.toBytes("82"));
put2.addColumn(Bytes.toBytes("score"), Bytes.toBytes("english"), Bytes.toBytes("92"));
putList.add(put2); // 将put2加入列表
// ---- 构造row_003的数据 ----
Put put3 = new Put(Bytes.toBytes("row_003"));
put3.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("Alice"));
put3.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes("20"));
put3.addColumn(Bytes.toBytes("info"), Bytes.toBytes("gender"), Bytes.toBytes("Female"));
put3.addColumn(Bytes.toBytes("score"), Bytes.toBytes("chinese"), Bytes.toBytes("90"));
put3.addColumn(Bytes.toBytes("score"), Bytes.toBytes("math"), Bytes.toBytes("85"));
put3.addColumn(Bytes.toBytes("score"), Bytes.toBytes("english"), Bytes.toBytes("91"));
putList.add(put3); // 将put3加入列表
// ---- 构造row_004的数据 ----
Put put4 = new Put(Bytes.toBytes("row_004"));
put4.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("Bob"));
put4.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes("22"));
put4.addColumn(Bytes.toBytes("score"), Bytes.toBytes("chinese"), Bytes.toBytes("88"));
put4.addColumn(Bytes.toBytes("score"), Bytes.toBytes("math"), Bytes.toBytes("90"));
putList.add(put4);
// 批量写入:一次调用提交所有Put操作,效率远高于逐条写入
table.put(putList);
System.out.println("批量插入 " + putList.size() + " 条数据成功!");
}
// ==================== 查询数据 ====================
/**
* Get查询 ------ 根据行键获取单行数据
*/
public static void getData() throws IOException {
System.out.println("===== Get查询单行数据 =====");
// 创建Get对象,指定要查询的行键
Get get = new Get(Bytes.toBytes("row_001"));
// 可选:只获取指定列族的指定列(减少网络传输,提高查询效率)
// 如果不指定,则获取该行的所有列
get.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
get.addColumn(Bytes.toBytes("score"), Bytes.toBytes("math"));
// 可选:设置返回的最大版本数
get.setMaxVersions(3);
// 执行查询,返回Result对象
Result result = table.get(get);
// 检查查询结果是否为空
if (result.isEmpty()) {
System.out.println("未找到数据!");
return;
}
// 遍历Result中的所有单元格(Cell)
for (Cell cell : result.rawCells()) {
// CellUtil工具类用于从Cell中提取各部分信息
String row = Bytes.toString(CellUtil.cloneRow(cell)); // 行键
String family = Bytes.toString(CellUtil.cloneFamily(cell)); // 列族
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));// 列限定符
String value = Bytes.toString(CellUtil.cloneValue(cell)); // 值
long timestamp = cell.getTimestamp(); // 时间戳
// 格式化输出每个单元格的信息
System.out.printf("行键:%s | 列族:%s | 列:%s | 值:%s | 时间戳:%d%n",
row, family, qualifier, value, timestamp);
}
}
/**
* 查询指定行键的所有列数据
*/
public static void getFullRow() throws IOException {
System.out.println("===== Get查询整行数据 =====");
Get get = new Get(Bytes.toBytes("row_002"));
// 不指定列,获取该行所有列族的所有列
Result result = table.get(get);
for (Cell cell : result.rawCells()) {
System.out.printf("%s:%s = %s%n",
Bytes.toString(CellUtil.cloneFamily(cell)),
Bytes.toString(CellUtil.cloneQualifier(cell)),
Bytes.toString(CellUtil.cloneValue(cell)));
}
}
/**
* Scan扫描查询 ------ 获取多行数据
*/
public static void scanData() throws IOException {
System.out.println("===== Scan扫描查询 =====");
// 创建Scan对象(不指定参数则扫描全表)
Scan scan = new Scan();
// 可选:设置扫描的起止行键范围
// 包含startRow,不包含stopRow(左闭右开区间)
scan.withStartRow(Bytes.toBytes("row_001"));
scan.withStopRow(Bytes.toBytes("row_005"));
// 可选:只扫描指定列族
scan.addFamily(Bytes.toBytes("info"));
// 可选:设置缓存的行数(提高扫描性能,默认100)
scan.setCaching(100);
// 可选:设置每次RPC返回的最大结果数
scan.setBatch(10);
// 执行扫描,返回ResultScanner(结果迭代器)
ResultScanner scanner = table.getScanner(scan);
// 遍历每一行结果
for (Result result : scanner) {
// 打印当前行的行键
System.out.println("行键:" + Bytes.toString(result.getRow()));
// 遍历该行的所有单元格
for (Cell cell : result.rawCells()) {
System.out.printf(" %s:%s = %s%n",
Bytes.toString(CellUtil.cloneFamily(cell)), // 列族
Bytes.toString(CellUtil.cloneQualifier(cell)), // 列限定符
Bytes.toString(CellUtil.cloneValue(cell))); // 值
}
}
// 必须关闭ResultScanner,释放服务端资源
scanner.close();
}
/**
* 带过滤器的Scan查询 ------ 条件过滤
*/
public static void scanWithFilter() throws IOException {
System.out.println("===== 带过滤器的Scan查询 =====");
Scan scan = new Scan();
// ========== 过滤器1:行键前缀过滤器 ==========
// 只返回行键以"row_00"开头的行
// PrefixFilter prefixFilter = new PrefixFilter(Bytes.toBytes("row_00"));
// ========== 过滤器2:列值过滤器 ==========
// 只返回info:age的值等于"20"的行
SingleColumnValueFilter ageFilter = new SingleColumnValueFilter(
Bytes.toBytes("info"), // 列族
Bytes.toBytes("age"), // 列限定符
CompareOperator.EQUAL, // 比较运算符:等于
Bytes.toBytes("20") // 比较的值
);
// 如果某行没有info:age列,是否过滤掉该行(true=过滤掉)
ageFilter.setFilterIfMissing(true);
// ========== 过滤器3:列名过滤器 ==========
// 只返回info:name和info:age这两列的数据
ColumnPrefixFilter nameFilter = new ColumnPrefixFilter(Bytes.toBytes("name"));
// ========== 过滤器4:值比较过滤器 ==========
// 只返回score:math的值大于等于90的行
SingleColumnValueFilter mathFilter = new SingleColumnValueFilter(
Bytes.toBytes("score"), // 列族
Bytes.toBytes("math"), // 列限定符
CompareOperator.GREATER_OR_EQUAL, // 比较运算符:大于等于
Bytes.toBytes("90") // 比较的值
);
mathFilter.setFilterIfMissing(true);
// ========== 组合过滤器:AND组合 ==========
// 同时满足 age=20 AND math>=90
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
// MUST_PASS_ALL = AND(所有条件都满足)
// MUST_PASS_ONE = OR(任一条件满足即可)
filterList.addFilter(ageFilter);
filterList.addFilter(mathFilter);
// 将过滤器列表设置到Scan中
scan.setFilter(filterList);
// 执行带过滤器的扫描
ResultScanner scanner = table.getScanner(scan);
for (Result result : scanner) {
System.out.println("匹配行键:" + Bytes.toString(result.getRow()));
for (Cell cell : result.rawCells()) {
System.out.printf(" %s:%s = %s%n",
Bytes.toString(CellUtil.cloneFamily(cell)),
Bytes.toString(CellUtil.cloneQualifier(cell)),
Bytes.toString(CellUtil.cloneValue(cell)));
}
}
scanner.close();
}
// ==================== 更新数据 ====================
/**
* 更新数据(本质上就是重新Put,新值覆盖旧值)
*/
public static void updateData() throws IOException {
System.out.println("===== 更新数据 =====");
// HBase中没有专门的update方法
// 更新操作就是重新执行Put操作,相同行键+列族+列限定符的新值会覆盖旧值
Put put = new Put(Bytes.toBytes("row_001"));
// 将Tom的数学成绩从95更新为98
put.addColumn(
Bytes.toBytes("score"), // 列族
Bytes.toBytes("math"), // 列限定符
Bytes.toBytes("98") // 新值
);
// 将Tom的年龄从20更新为21
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes("21"));
// 执行Put操作
table.put(put);
System.out.println("row_001 数据更新成功!");
// 验证更新结果
getFullRow();
}
// ==================== 删除数据 ====================
/**
* 删除指定单元格数据(Delete操作)
*/
public static void deleteData() throws IOException {
System.out.println("===== 删除数据 =====");
// ---- 删除单个单元格 ----
// 创建Delete对象,指定要删除的行键
Delete delete = new Delete(Bytes.toBytes("row_004"));
// 删除指定列(精确到列限定符级别)
// 参数:列族, 列限定符
delete.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"));
delete.addColumn(Bytes.toBytes("score"), Bytes.toBytes("chinese"));
// 执行删除操作
table.delete(delete);
System.out.println("row_004 的 info:age 和 score:chinese 已删除");
// ---- 删除整行 ----
Delete deleteAll = new Delete(Bytes.toBytes("row_004"));
// 不指定列,直接删除该行所有数据
table.delete(deleteAll);
System.out.println("row_004 整行已删除");
// ---- 批量删除 ----
List<Delete> deleteList = new ArrayList<>();
Delete del1 = new Delete(Bytes.toBytes("row_003"));
deleteList.add(del1);
// 批量删除多行
table.delete(deleteList);
System.out.println("row_003 已批量删除");
}
/**
* 检查行/列是否存在(使用checkAndPut实现乐观锁)
*/
public static void checkAndPut() throws IOException {
System.out.println("===== checkAndPut 乐观锁操作 =====");
// 场景:只有当 row_001 的 info:name 等于 "Tom" 时,才更新 info:age 为 "22"
// 这实现了类似CAS(Compare-And-Swap)的原子操作
Put put = new Put(Bytes.toBytes("row_001"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes("22"));
// checkAndPut(行键, 列族, 列限定符, 期望值, 要执行的Put操作)
boolean success = table.checkAndPut(
Bytes.toBytes("row_001"), // 要检查的行键
Bytes.toBytes("info"), // 检查的列族
Bytes.toBytes("name"), // 检查的列限定符
Bytes.toBytes("Tom"), // 期望值(只有当前值等于此值才执行put)
put // 条件满足时执行的Put操作
);
if (success) {
System.out.println("条件满足,数据更新成功!");
} else {
System.out.println("条件不满足,数据未更新!");
}
}
/**
* 主方法 ------ 按顺序执行所有CRUD操作
*/
public static void main(String[] args) {
try {
init(); // 初始化连接
putData(); // 1. 插入单条数据
putBatchData(); // 2. 批量插入数据
getData(); // 3. Get查询单行
getFullRow(); // 4. Get查询整行
scanData(); // 5. Scan扫描查询
scanWithFilter(); // 6. 带过滤器的查询
updateData(); // 7. 更新数据
checkAndPut(); // 8. 乐观锁操作
deleteData(); // 9. 删除数据
close(); // 关闭连接
} catch (IOException e) {
e.printStackTrace();
}
}
}
八、HBase 案例实战
8.1 在 IDEA 中创建工程并添加 JAR 包
Maven pom.xml 依赖配置:
xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>hbase-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- 统一管理HBase版本号 -->
<hbase.version>2.4.17</hbase.version>
<!-- Hadoop版本需与HBase兼容 -->
<hadoop.version>3.3.4</hadoop.version>
</properties>
<dependencies>
<!-- HBase客户端依赖(包含HBase Client API) -->
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>${hbase.version}</version>
</dependency>
<!-- HBase服务端依赖(包含Admin、RegionServer等API) -->
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-server</artifactId>
<version>${hbase.version}</version>
</dependency>
<!-- Hadoop Common依赖(提供HDFS相关类和Configuration) -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>${hadoop.version}</version>
</dependency>
<!-- Hadoop HDFS客户端依赖 -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs-client</artifactId>
<version>${hadoop.version}</version>
</dependency>
</dependencies>
</project>
8.2 完整案例:学生信息管理系统
java
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.filter.*;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* StudentHBaseManager - 学生信息管理系统
* 功能:
* 1. 创建学生信息表
* 2. 批量导入学生数据
* 3. 查询单个学生信息
* 4. 查询所有学生信息
* 5. 按条件过滤查询
* 6. 更新学生信息
* 7. 删除学生信息
* 8. 统计学生数量
* 9. 删除表
*/
public class StudentHBaseManager {
// ---- 常量定义 ----
private static final String TABLE_NAME = "student_info"; // 表名
private static final String CF_INFO = "info"; // 基本信息列族
private static final String CF_SCORE = "score"; // 成绩信息列族
private static final String CF_ADDRESS = "address"; // 地址信息列族
private static Connection connection; // 连接对象
private static Admin admin; // 管理对象
/**
* 初始化HBase连接
*/
public static void init() throws IOException {
Configuration conf = HBaseConfiguration.create(); // 创建配置
conf.set("hbase.zookeeper.quorum", "localhost"); // ZK地址
conf.set("hbase.zookeeper.property.clientPort", "2181"); // ZK端口
connection = ConnectionFactory.createConnection(conf); // 建立连接
admin = connection.getAdmin(); // 获取Admin
System.out.println("=== HBase连接初始化成功 ===");
}
/**
* 关闭连接,释放所有资源
*/
public static void destroy() throws IOException {
if (admin != null) admin.close();
if (connection != null) connection.close();
System.out.println("=== HBase连接已关闭 ===");
}
/**
* 功能1:创建学生信息表
* 创建包含三个列族的表:info(基本信息)、score(成绩)、address(地址)
*/
public static void createStudentTable() throws IOException {
TableName tableName = TableName.valueOf(TABLE_NAME);
// 检查表是否已存在
if (admin.tableExists(tableName)) {
System.out.println("表 " + TABLE_NAME + " 已存在,跳过创建");
return;
}
// 创建表描述器
HTableDescriptor tableDesc = new HTableDescriptor(tableName);
// 创建info列族,设置最大版本数为3
HColumnDescriptor infoCF = new HColumnDescriptor(CF_INFO);
infoCF.setMaxVersions(3); // 最多保存3个版本
infoCF.setMinVersions(1); // 最少保留1个版本
// 创建score列族,设置压缩和布隆过滤器
HColumnDescriptor scoreCF = new HColumnDescriptor(CF_SCORE);
scoreCF.setMaxVersions(1); // 只保留最新版本
scoreCF.setBloomFilterType(BloomType.ROW); // 布隆过滤器(按行键)
// 创建address列族
HColumnDescriptor addressCF = new HColumnDescriptor(CF_ADDRESS);
addressCF.setMaxVersions(1);
// 将三个列族添加到表描述器
tableDesc.addFamily(infoCF);
tableDesc.addFamily(scoreCF);
tableDesc.addFamily(addressCF);
// 执行建表
admin.createTable(tableDesc);
System.out.println("表 " + TABLE_NAME + " 创建成功!");
}
/**
* 功能2:批量导入学生数据
* 模拟批量插入10个学生的信息
*/
public static void batchImportStudents() throws IOException {
Table table = connection.getTable(TableName.valueOf(TABLE_NAME));
// 创建学生数据数组(二维数组,每行一个学生)
// 格式:{学号, 姓名, 年龄, 性别, 语文, 数学, 英语, 省份, 城市}
String[][] students = {
{"2021001", "张三", "20", "男", "88", "92", "85", "北京市", "海淀区"},
{"2021002", "李四", "21", "女", "90", "88", "92", "上海市", "浦东区"},
{"2021003", "王五", "20", "男", "76", "95", "80", "广东省", "深圳市"},
{"2021004", "赵六", "22", "女", "92", "78", "90", "浙江省", "杭州市"},
{"2021005", "孙七", "21", "男", "85", "90", "88", "江苏省", "南京市"},
{"2021006", "周八", "20", "女", "91", "86", "93", "湖北省", "武汉市"},
{"2021007", "吴九", "22", "男", "78", "91", "82", "四川省", "成都市"},
{"2021008", "郑十", "21", "女", "87", "94", "89", "湖南省", "长沙市"},
{"2021009", "陈一", "20", "男", "93", "82", "91", "福建省", "福州市"},
{"2021010", "林二", "22", "女", "80", "88", "86", "山东省", "青岛市"}
};
// 创建Put列表用于批量插入
List<Put> puts = new ArrayList<>();
// 遍历学生数据,构造Put对象
for (String[] s : students) {
// 行键设计:使用学号作为RowKey
Put put = new Put(Bytes.toBytes(s[0])); // s[0]=学号
// 写入info列族
put.addColumn(Bytes.toBytes(CF_INFO), Bytes.toBytes("name"), Bytes.toBytes(s[1]));
put.addColumn(Bytes.toBytes(CF_INFO), Bytes.toBytes("age"), Bytes.toBytes(s[2]));
put.addColumn(Bytes.toBytes(CF_INFO), Bytes.toBytes("gender"), Bytes.toBytes(s[3]));
// 写入score列族
put.addColumn(Bytes.toBytes(CF_SCORE), Bytes.toBytes("chinese"), Bytes.toBytes(s[4]));
put.addColumn(Bytes.toBytes(CF_SCORE), Bytes.toBytes("math"), Bytes.toBytes(s[5]));
put.addColumn(Bytes.toBytes(CF_SCORE), Bytes.toBytes("english"), Bytes.toBytes(s[6]));
// 写入address列族
put.addColumn(Bytes.toBytes(CF_ADDRESS), Bytes.toBytes("province"), Bytes.toBytes(s[7]));
put.addColumn(Bytes.toBytes(CF_ADDRESS), Bytes.toBytes("city"), Bytes.toBytes(s[8]));
puts.add(put); // 加入批量列表
}
// 执行批量插入
table.put(puts);
System.out.println("成功批量导入 " + students.length + " 条学生数据!");
table.close();
}
/**
* 功能3:根据学号查询单个学生信息
* @param studentId 学号(RowKey)
*/
public static void getStudentById(String studentId) throws IOException {
Table table = connection.getTable(TableName.valueOf(TABLE_NAME));
// 创建Get对象
Get get = new Get(Bytes.toBytes(studentId));
// 执行查询
Result result = table.get(get);
if (result.isEmpty()) {
System.out.println("未找到学号为 " + studentId + " 的学生");
table.close();
return;
}
// 打印学生信息
System.out.println("====== 学生信息 ======");
System.out.println("学号:" + studentId);
// 遍历所有单元格
for (Cell cell : result.rawCells()) {
String family = Bytes.toString(CellUtil.cloneFamily(cell)); // 列族
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell)); // 列名
String value = Bytes.toString(CellUtil.cloneValue(cell)); // 值
System.out.println(" " + family + ":" + qualifier + " = " + value);
}
table.close();
}
/**
* 功能4:查询所有学生信息
*/
public static void getAllStudents() throws IOException {
Table table = connection.getTable(TableName.valueOf(TABLE_NAME));
// 创建全表扫描
Scan scan = new Scan();
// 设置缓存行数,提高扫描效率
scan.setCaching(50);
ResultScanner scanner = table.getScanner(scan);
int count = 0; // 计数器
System.out.println("====== 所有学生信息 ======");
System.out.printf("%-12s %-8s %-4s %-4s %-8s %-8s %-8s%n",
"学号", "姓名", "年龄", "性别", "语文", "数学", "英语");
System.out.println("---------------------------------------------------------------");
for (Result result : scanner) {
// 获取行键(学号)
String id = Bytes.toString(result.getRow());
// 获取各列的值,如果某列不存在则显示"-"
String name = getColumnValue(result, CF_INFO, "name");
String age = getColumnValue(result, CF_INFO, "age");
String gender = getColumnValue(result, CF_INFO, "gender");
String chinese = getColumnValue(result, CF_SCORE, "chinese");
String math = getColumnValue(result, CF_SCORE, "math");
String english = getColumnValue(result, CF_SCORE, "english");
// 格式化输出
System.out.printf("%-12s %-8s %-4s %-4s %-8s %-8s %-8s%n",
id, name, age, gender, chinese, math, english);
count++;
}
System.out.println("---------------------------------------------------------------");
System.out.println("共 " + count + " 名学生");
scanner.close();
table.close();
}
/**
* 功能5:按条件过滤查询
* 查询数学成绩大于等于90分的学生
*/
public static void queryByCondition() throws IOException {
Table table = connection.getTable(TableName.valueOf(TABLE_NAME));
Scan scan = new Scan();
// 创建过滤器:数学成绩 >= 90
SingleColumnValueFilter mathFilter = new SingleColumnValueFilter(
Bytes.toBytes(CF_SCORE), // 列族
Bytes.toBytes("math"), // 列限定符
CompareOperator.GREATER_OR_EQUAL, // 比较操作:>=
Bytes.toBytes("90") // 比较值
);
mathFilter.setFilterIfMissing(true); // 没有math列的行不返回
// 创建过滤器:性别 = 男
SingleColumnValueFilter genderFilter = new SingleColumnValueFilter(
Bytes.toBytes(CF_INFO),
Bytes.toBytes("gender"),
CompareOperator.EQUAL,
Bytes.toBytes("男")
);
genderFilter.setFilterIfMissing(true);
// 组合过滤器:同时满足(AND关系)
FilterList filters = new FilterList(FilterList.Operator.MUST_PASS_ALL);
filters.addFilter(mathFilter);
filters.addFilter(genderFilter);
scan.setFilter(filters);
System.out.println("====== 数学>=90且性别=男的学生 ======");
ResultScanner scanner = table.getScanner(scan);
for (Result result : scanner) {
String id = Bytes.toString(result.getRow());
String name = getColumnValue(result, CF_INFO, "name");
String math = getColumnValue(result, CF_SCORE, "math");
System.out.println(" 学号:" + id + " | 姓名:" + name + " | 数学:" + math);
}
scanner.close();
table.close();
}
/**
* 功能6:更新学生信息
* @param studentId 学号
* @param family 列族
* @param qualifier 列限定符
* @param newValue 新值
*/
public static void updateStudent(String studentId, String family, String qualifier, String newValue) throws IOException {
Table table = connection.getTable(TableName.valueOf(TABLE_NAME));
// 先查询原值
Get get = new Get(Bytes.toBytes(studentId));
Result result = table.get(get);
if (result.isEmpty()) {
System.out.println("学号 " + studentId + " 不存在,无法更新!");
table.close();
return;
}
// 获取原值
byte[] oldValue = result.getValue(Bytes.toBytes(family), Bytes.toBytes(qualifier));
String oldVal = oldValue != null ? Bytes.toString(oldValue) : "无";
// 执行更新(Put覆盖)
Put put = new Put(Bytes.toBytes(studentId));
put.addColumn(Bytes.toBytes(family), Bytes.toBytes(qualifier), Bytes.toBytes(newValue));
table.put(put);
System.out.println("更新成功:" + studentId + " 的 " + family + ":" + qualifier
+ " 从 \"" + oldVal + "\" 更新为 \"" + newValue + "\"");
table.close();
}
/**
* 功能7:删除学生信息
* @param studentId 学号
*/
public static void deleteStudent(String studentId) throws IOException {
Table table = connection.getTable(TableName.valueOf(TABLE_NAME));
// 先检查是否存在
Get get = new Get(Bytes.toBytes(studentId));
Result result = table.get(get);
if (result.isEmpty()) {
System.out.println("学号 " + studentId + " 不存在!");
table.close();
return;
}
// 删除整行
Delete delete = new Delete(Bytes.toBytes(studentId));
table.delete(delete);
System.out.println("学号 " + studentId + " 的信息已删除!");
table.close();
}
/**
* 功能8:统计学生总数(使用ResultScanner遍历计数)
*/
public static long countStudents() throws IOException {
Table table = connection.getTable(TableName.valueOf(TABLE_NAME));
Scan scan = new Scan();
// 只扫描info列族的name列(减少数据传输量,只需要确认行存在即可)
scan.addColumn(Bytes.toBytes(CF_INFO), Bytes.toBytes("name"));
scan.setCaching(500); // 设置较大的缓存行数
ResultScanner scanner = table.getScanner(scan);
long count = 0;
// 遍历每一行,逐行计数
for (Result result : scanner) {
count++; // 每有一个Result就代表一行数据
}
scanner.close();
table.close();
System.out.println("学生总数:" + count);
return count;
}
/**
* 功能9:删除学生信息表
*/
public static void dropStudentTable() throws IOException {
TableName tableName = TableName.valueOf(TABLE_NAME);
if (!admin.tableExists(tableName)) {
System.out.println("表 " + TABLE_NAME + " 不存在!");
return;
}
// 先禁用再删除
admin.disableTable(tableName);
admin.deleteTable(tableName);
System.out.println("表 " + TABLE_NAME + " 已成功删除!");
}
/**
* 工具方法:从Result中获取指定列族和列限定符的值
* @param result 查询结果
* @param family 列族名
* @param qualifier 列限定符名
* @return 列值的字符串形式,如果不存在返回"-"
*/
private static String getColumnValue(Result result, String family, String qualifier) {
// 从Result中获取指定列的字节数组值
byte[] value = result.getValue(Bytes.toBytes(family), Bytes.toBytes(qualifier));
// 如果值不为null,转换为字符串返回;否则返回"-"
return value != null ? Bytes.toString(value) : "-";
}
/**
* 主方法 ------ 完整流程演示
*/
public static void main(String[] args) {
try {
// 初始化连接
init();
// 1. 创建表
createStudentTable();
// 2. 批量导入数据
batchImportStudents();
// 3. 查询单个学生
getStudentById("2021001");
// 4. 查询所有学生
getAllStudents();
// 5. 按条件查询
queryByCondition();
// 6. 更新学生信息
updateStudent("2021001", CF_SCORE, "math", "98");
// 7. 验证更新
getStudentById("2021001");
// 8. 删除学生
deleteStudent("2021010");
// 9. 统计学生总数
countStudents();
// 10. 查看所有学生(确认删除后)
getAllStudents();
// 11. 删除表(可选,注释掉以保留数据)
// dropStudentTable();
// 关闭连接
destroy();
} catch (IOException e) {
e.printStackTrace();
}
}
}
九、利用 Python 操作 HBase
9.1 HappyBase 的安装
bash
# 使用pip安装HappyBase(Python的HBase客户端库)
# HappyBase基于Thrift协议与HBase通信
pip install happybase
# 如果使用Python3,可能需要指定pip3
pip3 install happybase
# 验证安装
python3 -c "import happybase; print(happybase.__version__)"
前置条件:启动HBase Thrift Server
bash
# HappyBase通过Thrift协议连接HBase,因此需要先启动Thrift服务
# 在HBase的Master节点上执行:
hbase-daemon.sh start thrift
# 验证Thrift Server是否启动(默认监听9090端口)
netstat -tlnp | grep 9090
9.2 Connection 类
python
"""
Connection类 - 演示HappyBase的连接管理和数据库/表管理操作
Connection是HappyBase的入口类,用于连接HBase集群并管理表
"""
import happybase
# ==================== 创建连接 ====================
# 创建与HBase Thrift Server的连接
# host: HBase Thrift Server的主机地址
# port: Thrift Server的端口号(默认9090)
# autoconnect: 是否自动建立连接(True=立即连接,False=延迟连接)
# protocol: 协议版本,通常使用'binary'
connection = happybase.Connection(
host='localhost', # HBase所在主机的IP或主机名
port=9090, # Thrift Server的端口号
autoconnect=True, # 设置为True,创建时自动连接
protocol='binary' # 使用binary协议(推荐)
)
print("成功连接到HBase集群!")
# ==================== 查看集群信息 ====================
# 获取HBase集群的版本信息
# 返回字节字符串,需要解码
print("HBase集群版本:", connection.client.getVersion())
# ==================== 管理表 ====================
# ---- 查看所有表 ----
# tables() 方法返回所有表名的列表(表名为bytes类型)
tables = connection.tables()
print("当前所有表:", [t.decode('utf-8') for t in tables])
# ---- 创建表 ----
# create_table() 方法创建新表
# 参数1:表名(字符串类型)
# 参数2:字典,键=列族名,值=列族属性字典
# 创建students表,包含info和score两个列族
connection.create_table(
'students', # 表名
{ # 列族配置字典
'info': { # info列族
'max_versions': 5, # 最大版本数为5
'bloom_filter_type': 'ROW', # 布隆过滤器类型
'compression': 'snappy' # 压缩方式
},
'score': { # score列族
'max_versions': 3, # 最大版本数为3
'compression': 'snappy' # 压缩方式
}
}
)
print("表 students 创建成功!")
# ---- 查看表是否存在 ----
# 通过尝试获取表对象来判断表是否存在
try:
table = connection.table('students')
print("表 students 存在")
except Exception as e:
print("表 students 不存在")
# ---- 获取表对象 ----
# table() 方法获取指定表的操作句柄
students_table = connection.table('students')
print("获取表对象:", students_table.name.decode('utf-8'))
# ---- 禁用并删除表 ----
# 注意:HappyBase的create_table在表已存在时会抛出异常
# 删除表需要先通过HBase Shell或Admin API禁用表
# connection.delete_table('表名', disable=True) # disable=True会自动禁用表
# ==================== 关闭连接 ====================
# 使用完毕后必须关闭连接
connection.close()
print("连接已关闭!")
9.3 Table 类
python
"""
Table类 - 演示HappyBase的Table类操作
Table类提供了对HBase表的CRUD操作
"""
import happybase
# 建立连接
connection = happybase.Connection(host='localhost', port=9090, autoconnect=True)
# 获取表对象
table = connection.table('students')
# ==================== 插入数据 ====================
# ---- 插入单条数据 ----
# put() 方法插入或更新一条数据
# 参数:行键(bytes), {列族:列限定符: 值}
table.put(
b'row_001', # 行键(必须为bytes类型)
{ # 数据字典
b'info:name': b'Tom', # 列族:列限定符 = 值
b'info:age': b'20', # info列族的age列
b'info:gender': b'Male', # info列族的gender列
b'score:chinese': b'89', # score列族的chinese列
b'score:math': b'95', # score列族的math列
b'score:english': b'88' # score列族的english列
}
)
print("row_001 插入成功!")
# ---- 批量插入数据 ----
# 使用batch()上下文管理器进行批量写入,效率更高
# batch()会在with块结束时自动提交所有写入操作
with table.batch() as batch:
# 批量插入row_002
batch.put(b'row_002', {
b'info:name': b'Jerry',
b'info:age': b'21',
b'info:gender': b'Male',
b'score:chinese': b'76',
b'score:math': b'82',
b'score:english': b'92'
})
# 批量插入row_003
batch.put(b'row_003', {
b'info:name': b'Alice',
b'info:age': b'20',
b'info:gender': b'Female',
b'score:chinese': b'90',
b'score:math': b'85',
b'score:english': b'91'
})
# 批量插入row_004
batch.put(b'row_004', {
b'info:name': b'Bob',
b'info:age': b'22',
b'info:gender': b'Male',
b'score:chinese': b'88',
b'score:math': b'90',
b'score:english': b'87'
})
print("批量插入3条数据成功!")
# ==================== 查询数据 ====================
# ---- 查询单行数据 ----
# row() 方法根据行键获取一行数据
# 返回字典,键为列族:列限定符(bytes),值为单元格值(bytes)
row = table.row(b'row_001') # 获取row_001的所有列
print("\nrow_001 的所有数据:")
for key, value in row.items():
# 将bytes类型的键和值解码为字符串
print(f" {key.decode('utf-8')} = {value.decode('utf-8')}")
# ---- 查询单行指定列 ----
# columns参数指定要查询的列,减少网络传输
row_filtered = table.row(
b'row_001', # 行键
columns=[b'info:name', b'score:math'] # 指定查询的列
)
print("\nrow_001 的指定列:")
for key, value in row_filtered.items():
print(f" {key.decode('utf-8')} = {value.decode('utf-8')}")
# ---- 查询多行数据(指定多个行键)----
# rows() 方法根据多个行键批量查询
# 参数:行键列表
# 返回:列表,每个元素为(行键字典)的元组
rows = table.rows([b'row_001', b'row_002', b'row_003'])
print("\n多行查询结果:")
for row_key, row_data in rows:
name = row_data.get(b'info:name', b'N/A').decode('utf-8')
print(f" 行键:{row_key.decode('utf-8')} | 姓名:{name}")
# ---- 扫描全表 ----
# scan() 方法扫描表中数据
# 返回一个生成器(迭代器),逐行返回数据
print("\n扫描全表所有数据:")
for key, data in table.scan():
name = data.get(b'info:name', b'N/A').decode('utf-8')
age = data.get(b'info:age', b'N/A').decode('utf-8')
print(f" 行键:{key.decode('utf-8')} | 姓名:{name} | 年龄:{age}")
# ---- 扫描指定范围 ----
# row_start和row_end参数设置扫描的起止行键(左闭右开)
print("\n扫描 row_001 到 row_003(不含row_003):")
for key, data in table.scan(row_start=b'row_001', row_stop=b'row_003'):
name = data.get(b'info:name', b'N/A').decode('utf-8')
print(f" 行键:{key.decode('utf-8')} | 姓名:{name}")
# ---- 扫描指定列 ----
# columns参数限制扫描返回的列
print("\n扫描所有行的info:name和score:math:")
for key, data in table.scan(columns=[b'info:name', b'score:math']):
name = data.get(b'info:name', b'N/A').decode('utf-8')
math = data.get(b'score:math', b'N/A').decode('utf-8')
print(f" 行键:{key.decode('utf-8')} | 姓名:{name} | 数学:{math}")
# ---- 限制扫描行数 ----
# limit参数限制返回的最大行数
print("\n扫描前2行数据:")
for key, data in table.scan(limit=2):
name = data.get(b'info:name', b'N/A').decode('utf-8')
print(f" 行键:{key.decode('utf-8')} | 姓名:{name}")
# ==================== 更新数据 ====================
# HBase中更新和插入操作相同,使用put()覆盖旧值
# 将row_001的数学成绩从95更新为98
table.put(b'row_001', {b'score:math': b'98'})
print("\nrow_001 的 math 更新为 98")
# 验证更新结果
row = table.row(b'row_001', columns=[b'info:name', b'score:math'])
print(f"更新后验证:{row[b'info:name'].decode('utf-8')} 数学={row[b'score:math'].decode('utf-8')}")
# ==================== 删除数据 ====================
# ---- 删除指定列 ----
# delete() 方法删除数据
# 参数:行键, 要删除的列列表
table.delete(b'row_004', columns=[b'info:age', b'score:chinese'])
print("\nrow_004 的 info:age 和 score:chinese 已删除")
# ---- 删除整行 ----
# 不指定columns参数则删除整行
table.delete(b'row_004')
print("row_004 整行已删除")
# ---- 批量删除 ----
with table.batch() as batch:
# 使用batch的delete方法批量删除
batch.delete(b'row_003', columns=[b'score:english'])
print("批量删除完成")
# ==================== 获取行数(近似值)====================
# regions() 方法返回表的Region信息
# 注意:HappyBase没有直接的count方法
# 实际计数需要扫描全表
count = 0
for _ in table.scan():
count += 1
print(f"\n表中共有 {count} 行数据")
# ==================== 关闭连接 ====================
connection.close()
print("操作完成,连接已关闭!")
十、HBase MapReduce 数据转移案例
10.1 HBase 不同表间数据转移
将 student_info 表中的数据经过 MapReduce 处理后写入 student_backup 表。
java
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.mapreduce.TableMapReduceUtil;
import org.apache.hadoop.hbase.mapreduce.TableMapper;
import org.apache.hadoop.hbase.mapreduce.TableReducer;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import java.io.IOException;
/**
* HBaseTableTransfer - HBase表间数据转移
* 功能:从student_info表读取数据,经过Map处理后,写入student_backup表
*
* 工作流程:
* Mapper: 从源表读取数据,提取行键和列值
* Reducer: 将处理后的数据写入目标表
*/
public class HBaseTableTransfer {
/**
* ========== Mapper类 ==========
* TableMapper<KEYOUT, VALUEOUT> 是HBase提供的专用Mapper
* 输入:ImmutableBytesWritable(行键), Result(一行数据)
*/
public static class StudentMapper extends TableMapper<Text, Text> {
/**
* map方法:每读取一行数据调用一次
* @param key 行键(ImmutableBytesWritable类型)
* @param value 该行的所有数据(Result类型)
* @param context 上下文对象,用于输出中间结果
*/
@Override
protected void map(ImmutableBytesWritable key, Result value, Context context)
throws IOException, InterruptedException {
// 1. 提取行键并转为字符串
String rowKey = Bytes.toString(key.get());
// 2. 遍历该行的所有单元格(Cell)
for (Cell cell : value.rawCells()) {
// 3. 提取列族名
String family = Bytes.toString(CellUtil.cloneFamily(cell));
// 4. 提取列限定符名
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
// 5. 提取值
String cellValue = Bytes.toString(CellUtil.cloneValue(cell));
// 6. 将列族、列限定符、值拼接为一个字符串,用"|"分隔
String outputValue = family + "|" + qualifier + "|" + cellValue;
// 7. 输出中间结果:key=行键, value="列族|列限定符|值"
// Text是Hadoop的字符串包装类
context.write(new Text(rowKey), new Text(outputValue));
}
}
}
/**
* ========== Reducer类 ==========
* TableReducer<KEYIN, VALUEIN, KEYOUT> 是HBase提供的专用Reducer
* 将Mapper的输出写入HBase目标表
*/
public static class StudentReducer extends TableReducer<Text, Text, NullWritable> {
/**
* reduce方法:每个行键调用一次
* @param key 行键(来自Mapper的输出键)
* @param values 该行键对应的所有列值(来自Mapper的输出值)
* @param context 上下文对象,用于写入目标HBase表
*/
@Override
protected void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
// 1. 创建Put对象,行键来自Mapper的key
Put put = new Put(Bytes.toBytes(key.toString()));
// 2. 遍历该行键下的所有列值
for (Text val : values) {
// 3. 将value按"|"分割,还原为列族、列限定符、值
String[] parts = val.toString().split("\\|");
// 4. 检查数据格式是否正确(应为3部分)
if (parts.length == 3) {
String family = parts[0]; // 列族
String qualifier = parts[1]; // 列限定符
String cellValue = parts[2]; // 值
// 5. 向Put对象中添加列数据
put.addColumn(
Bytes.toBytes(family), // 列族
Bytes.toBytes(qualifier), // 列限定符
Bytes.toBytes(cellValue) // 值
);
}
}
// 6. 将Put对象写入目标表
// NullWritable.get() 表示输出键为空(因为TableReducer会自动写入HBase)
context.write(NullWritable.get(), put);
}
}
/**
* 主方法:配置并运行MapReduce作业
*/
public static void main(String[] args) throws Exception {
// 1. 创建HBase配置
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum", "localhost");
conf.set("hbase.zookeeper.property.clientPort", "2181");
// 2. 创建MapReduce作业,作业名为"hbase-table-transfer"
Job job = Job.getInstance(conf, "hbase-table-transfer");
// 3. 设置作业的主类(包含main方法的类)
job.setJarByClass(HBaseTableTransfer.class);
// 4. 配置Mapper阶段
// TableMapReduceUtil.initTableMapperJob() 是HBase提供的工具方法
// 参数1:源表名(从哪里读数据)
// 参数2:Scan对象(null表示扫描全表)
// 参数3:Mapper类
// 参数4:Mapper输出的Key类型
// 参数5:Mapper输出的Value类型
// 参数6:Job对象
TableMapReduceUtil.initTableMapperJob(
"student_info", // 源表:student_info
new Scan(), // 扫描全表
StudentMapper.class, // 使用自定义的Mapper
Text.class, // 输出Key类型:Text(行键)
Text.class, // 输出Value类型:Text(列数据)
job // 作业对象
);
// 5. 配置Reducer阶段
// TableMapReduceUtil.initTableReducerJob() 配置将结果写入目标表
// 参数1:目标表名(写入哪里)
// 参数2:Reducer类
// 参数3:Job对象
TableMapReduceUtil.initTableReducerJob(
"student_backup", // 目标表:student_backup
StudentReducer.class, // 使用自定义的Reducer
job // 作业对象
);
// 6. 提交作业并等待完成
// waitForCompletion(true) 表示等待作业完成,并输出进度信息
boolean success = job.waitForCompletion(true);
// 7. 根据作业结果退出
// 成功返回0,失败返回1
System.exit(success ? 0 : 1);
}
}
10.2 HDFS 数据转移至 HBase
将 HDFS 上的 CSV 文件数据导入到 HBase 表中。
HDFS 上的 CSV 数据示例(student.csv):
2021001,张三,20,男,88,92,85
2021002,李四,21,女,90,88,92
2021003,王五,20,男,76,95,80
java
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.mapreduce.TableMapReduceUtil;
import org.apache.hadoop.hbase.mapreduce.TableReducer;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import java.io.IOException;
/**
* HdfsToHBase - 将HDFS上的CSV文件数据导入HBase表
*
* 工作流程:
* Mapper: 读取HDFS上的CSV文件,按逗号分割,提取各字段
* Reducer: 将解析后的数据组装成HBase的Put对象,写入目标表
*/
public class HdfsToHBase {
/**
* ========== Mapper类 ==========
* 读取HDFS文件,每行数据按逗号分割
* 输入:<行偏移量(LongWritable), 每行文本(Text)>
* 输出:<行键(Text), 各字段值(Text)>
*/
public static class CsvMapper extends Mapper<LongWritable, Text, Text, Text> {
/**
* map方法:处理CSV文件的每一行
* @param key 行偏移量(本案例中不使用)
* @param value CSV文件的一行文本,如 "2021001,张三,20,男,88,92,85"
* @param context 上下文
*/
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 1. 读取一行CSV数据
String line = value.toString().trim();
// 2. 跳过空行
if (line.isEmpty()) {
return;
}
// 3. 按逗号分割各字段
// CSV格式:学号,姓名,年龄,性别,语文,数学,英语
String[] fields = line.split(",");
// 4. 检查字段数量是否正确(应为7个字段)
if (fields.length != 7) {
// 字段数不正确,跳过该行(或记录日志)
return;
}
// 5. 提取各字段
String studentId = fields[0].trim(); // 学号(用作RowKey)
String name = fields[1].trim(); // 姓名
String age = fields[2].trim(); // 年龄
String gender = fields[3].trim(); // 性别
String chinese = fields[4].trim(); // 语文成绩
String math = fields[5].trim(); // 数学成绩
String english = fields[6].trim(); // 英语成绩
// 6. 构造输出值:将所有字段拼接为一个字符串,用"|"分隔
String outputValue = name + "|" + age + "|" + gender + "|"
+ chinese + "|" + math + "|" + english;
// 7. 输出:key=学号(RowKey), value=拼接后的字段字符串
context.write(new Text(studentId), new Text(outputValue));
}
}
/**
* ========== Reducer类 ==========
* 接收Mapper的输出,构造Put对象写入HBase目标表
*/
public static class HBaseReducer extends TableReducer<Text, Text, NullWritable> {
/**
* reduce方法:每个行键(学号)调用一次
*/
@Override
protected void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
// 1. 创建Put对象,使用学号作为行键
Put put = new Put(Bytes.toBytes(key.toString()));
// 2. 遍历values(正常情况下每个学号只有一个value)
for (Text val : values) {
// 3. 按"|"分割还原字段
String[] fields = val.toString().split("\\|");
// 4. 检查字段数量
if (fields.length == 6) {
// 5. 将各字段写入对应的列
// 写入info列族
put.addColumn(
Bytes.toBytes("info"), // 列族
Bytes.toBytes("name"), // 列限定符
Bytes.toBytes(fields[0]) // 姓名
);
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes(fields[1]));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("gender"), Bytes.toBytes(fields[2]));
// 写入score列族
put.addColumn(Bytes.toBytes("score"), Bytes.toBytes("chinese"), Bytes.toBytes(fields[3]));
put.addColumn(Bytes.toBytes("score"), Bytes.toBytes("math"), Bytes.toBytes(fields[4]));
put.addColumn(Bytes.toBytes("score"), Bytes.toBytes("english"), Bytes.toBytes(fields[5]));
}
}
// 6. 将Put写入HBase目标表
context.write(NullWritable.get(), put);
}
}
/**
* 主方法:配置并运行MapReduce作业
*/
public static void main(String[] args) throws Exception {
// 1. 创建HBase配置
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum", "localhost");
conf.set("hbase.zookeeper.property.clientPort", "2181");
// 2. 创建Job实例
Job job = Job.getInstance(conf, "hdfs-to-hbase");
job.setJarByClass(HdfsToHBase.class);
// 3. 设置Mapper
job.setMapperClass(CsvMapper.class); // 使用自定义的CSV解析Mapper
job.setMapOutputKeyClass(Text.class); // Mapper输出Key类型
job.setMapOutputValueClass(Text.class); // Mapper输出Value类型
// 4. 设置输入路径(HDFS上的CSV文件路径)
// 第一个命令行参数为输入路径
FileInputFormat.addInputPath(job, new Path(args[0]));
// 示例:hdfs://localhost:9000/input/student.csv
// 5. 配置Reducer并设置目标表
// 使用TableMapReduceUtil.initTableReducerJob()将Reducer输出写入HBase
TableMapReduceUtil.initTableReducerJob(
"student_from_hdfs", // 目标HBase表名
HBaseReducer.class, // 使用自定义的Reducer
job // 作业对象
);
// 6. 提交作业
boolean success = job.waitForCompletion(true);
System.exit(success ? 0 : 1);
}
}
十一、HBase 数据备份与恢复
11.1 使用 Export/Import 工具进行备份和恢复
备份(Export):
bash
# 使用HBase自带的Export工具将表数据导出到HDFS
# Export工具本质上是一个MapReduce作业
# 导出students表到HDFS的/hbase-backup/students目录
hbase org.apache.hadoop.hbase.mapreduce.Export \
students \
hdfs://localhost:9000/hbase-backup/students
# 导出时指定版本数(导出最近3个版本的数据)
hbase org.apache.hadoop.hbase.mapreduce.Export \
students \
hdfs://localhost:9000/hbase-backup/students \
3
# 导出时指定起止时间戳(只导出指定时间范围内的数据)
# 时间戳需要查看数据中的实际时间戳值
hbase org.apache.hadoop.hbase.mapreduce.Export \
students \
hdfs://localhost:9000/hbase-backup/students \
1 \
1634567890000 \ # 起始时间戳
1634667890000 # 结束时间戳
恢复(Import):
bash
# 使用HBase自带的Import工具从HDFS导入数据到表
# 注意:目标表必须事先存在,且表结构(列族)需与导出时一致
# 先创建目标表
hbase shell << EOF
create 'students_restored', 'info', 'score'
exit
EOF
# 从HDFS导入数据到students_restored表
hbase org.apache.hadoop.hbase.mapreduce.Import \
students_restored \
hdfs://localhost:9000/hbase-backup/students
11.2 使用 Snapshot 进行备份和恢复(推荐方式)
bash
# ========== 创建快照 ==========
# 快照是表的元数据指针,不会复制实际数据,创建速度快,不占用额外存储空间
# 在HBase Shell中创建快照
hbase shell << 'EOF'
# 语法:snapshot '表名', '快照名'
snapshot 'students', 'students_snapshot_20240101'
# 查看所有快照
list_snapshots
# 输出示例:
# students_snapshot_20240101 students ...
EOF
# ========== 从快照恢复 ==========
# 恢复操作会将表恢复到快照创建时的状态
hbase shell << 'EOF'
# 第一步:禁用表(恢复前必须禁用)
disable 'students'
# 第二步:从快照恢复
# 语法:restore_snapshot '快照名'
restore_snapshot 'students_snapshot_20240101'
# 第三步:重新启用表
enable 'students'
# 验证恢复结果
scan 'students', {LIMIT => 3}
EOF
# ========== 从快照克隆新表 ==========
# 克隆操作创建一个新表,数据与快照时完全一致
hbase shell << 'EOF'
# 语法:clone_snapshot '快照名', '新表名'
clone_snapshot 'students_snapshot_20240101', 'students_clone'
# 验证克隆表
scan 'students_clone', {LIMIT => 3}
EOF
# ========== 删除快照 ==========
hbase shell << 'EOF'
# 语法:delete_snapshot '快照名'
delete_snapshot 'students_snapshot_20240101'
# 删除所有快照(慎用!)
# delete_all_snapshot 'students'
EOF
11.3 编程实现数据备份与恢复
java
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FSDataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* HBaseBackupRestore - 编程实现HBase数据备份与恢复
*
* 方法1:基于Scan+Put的全表复制(适用于小表)
* 方法2:调用Snapshot API(适用于大表)
*/
public class HBaseBackupRestore {
private static Connection connection;
private static Admin admin;
/**
* 初始化连接
*/
public static void init() throws IOException {
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum", "localhost");
conf.set("hbase.zookeeper.property.clientPort", "2181");
connection = ConnectionFactory.createConnection(conf);
admin = connection.getAdmin();
}
/**
* 关闭连接
*/
public static void close() throws IOException {
if (admin != null) admin.close();
if (connection != null) connection.close();
}
/**
* 方法1:基于Scan和Put的表数据复制
* 将源表的所有数据复制到目标表
*
* @param srcTableName 源表名
* @param destTableName 目标表名
* @param batchSize 每批次写入的行数(控制内存使用)
*/
public static void copyTable(String srcTableName, String destTableName, int batchSize) throws IOException {
System.out.println("===== 开始表数据复制 =====");
System.out.println("源表:" + srcTableName + " → 目标表:" + destTableName);
// 第一步:获取源表和目标表的Table对象
Table srcTable = connection.getTable(TableName.valueOf(srcTableName));
Table destTable = connection.getTable(TableName.valueOf(destTableName));
// 第二步:创建全表扫描
Scan scan = new Scan();
scan.setCaching(batchSize); // 设置每次RPC缓存的行数
scan.setBatch(100); // 设置每次返回的最大列数
// 第三步:执行扫描
ResultScanner scanner = srcTable.getScanner(scan);
List<Put> puts = new ArrayList<>(); // 批量写入列表
long totalRows = 0; // 总行数计数器
long totalCells = 0; // 总单元格计数器
// 第四步:遍历源表的每一行
for (Result result : scanner) {
// 获取当前行的行键
byte[] rowKey = result.getRow();
// 创建Put对象(目标表的行键与源表相同)
Put put = new Put(rowKey);
// 遍历该行的所有单元格
for (Cell cell : result.rawCells()) {
// 将源表的单元格直接复制到目标表的Put对象中
// CellUtil.cloneFamily/Qualifier/Value 提取Cell的各部分
put.addColumn(
CellUtil.cloneFamily(cell), // 列族(与源相同)
CellUtil.cloneQualifier(cell), // 列限定符(与源相同)
cell.getTimestamp(), // 时间戳(保留原始时间戳)
CellUtil.cloneValue(cell) // 值(与源相同)
);
totalCells++; // 单元格计数+1
}
puts.add(put); // 将Put加入批量列表
totalRows++; // 行计数+1
// 当积累了足够的Put操作时,执行批量写入
if (puts.size() >= batchSize) {
destTable.put(puts); // 批量写入目标表
System.out.println(" 已复制 " + totalRows + " 行...");
puts.clear(); // 清空列表,释放内存
}
}
// 第五步:写入剩余的Put(最后一不满batchSize的部分)
if (!puts.isEmpty()) {
destTable.put(puts);
}
// 第六步:关闭资源
scanner.close();
srcTable.close();
destTable.close();
System.out.println("数据复制完成!");
System.out.println("总计复制:" + totalRows + " 行," + totalCells + " 个单元格");
}
/**
* 方法2:基于Snapshot的备份
* 创建表的快照(不复制数据,仅记录元数据)
*
* @param tableName 表名
* @param snapshotName 快照名称
*/
public static void createSnapshot(String tableName, String snapshotName) throws IOException {
TableName table = TableName.valueOf(tableName);
// 检查表是否存在
if (!admin.tableExists(table)) {
System.out.println("表 " + tableName + " 不存在!");
return;
}
// 检查快照是否已存在
if (admin.listSnapshots(".*" + snapshotName + ".*").size() > 0) {
System.out.println("快照 " + snapshotName + " 已存在!");
return;
}
// 创建快照
// snapshot操作非常快,因为不复制数据
admin.snapshot(snapshotName, table);
System.out.println("快照 " + snapshotName + " 创建成功!");
}
/**
* 方法2续:从快照恢复表
*
* @param snapshotName 快照名称
* @param tableName 要恢复到的表名(可以是原表名或新表名)
*/
public static void restoreFromSnapshot(String snapshotName, String tableName) throws IOException {
TableName table = TableName.valueOf(tableName);
// 如果目标表已存在,需要先禁用
if (admin.tableExists(table)) {
if (admin.isTableEnabled(table)) {
admin.disableTable(table); // 禁用表
System.out.println("表 " + tableName + " 已禁用");
}
}
// 从快照恢复到指定表
admin.restoreSnapshot(snapshotName);
System.out.println("从快照 " + snapshotName + " 恢复成功!");
// 如果表被禁用了,重新启用
if (admin.tableExists(table) && admin.isTableDisabled(table)) {
admin.enableTable(table);
System.out.println("表 " + tableName + " 已重新启用");
}
}
/**
* 方法2续:从快照克隆新表
*
* @param snapshotName 快照名称
* @param newTableName 新表名称
*/
public static void cloneFromSnapshot(String snapshotName, String newTableName) throws IOException {
TableName newTable = TableName.valueOf(newTableName);
// 检查新表是否已存在
if (admin.tableExists(newTable)) {
System.out.println("表 " + newTableName + " 已存在,无法克隆!");
return;
}
// 从快照克隆一个新表
// 克隆操作不复制数据,只是创建新表引用相同的数据文件
admin.cloneSnapshot(snapshotName, newTable);
System.out.println("从快照 " + snapshotName + " 克隆新表 " + newTableName + " 成功!");
}
/**
* 方法3:将表数据导出到HDFS文件(简化版,作为参考)
* 实际生产中建议使用HBase自带的Export工具
*
* @param tableName 表名
* @param hdfsPath HDFS目标路径
*/
public static void exportToHdfs(String tableName, String hdfsPath) throws IOException {
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
// 获取HDFS文件系统
Configuration conf = new Configuration();
conf.set("fs.defaultFS", "hdfs://localhost:9000");
FileSystem fs = FileSystem.get(conf);
// 创建HDFS输出文件
FSDataOutputStream out = fs.create(new Path(hdfsPath));
ResultScanner scanner = table.getScanner(scan);
long count = 0;
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
for (Cell cell : result.rawCells()) {
// 格式:行键\t列族:列限定符\t时间戳\t值
String line = rowKey + "\t"
+ Bytes.toString(CellUtil.cloneFamily(cell))
+ ":" + Bytes.toString(CellUtil.cloneQualifier(cell))
+ "\t" + cell.getTimestamp()
+ "\t" + Bytes.toString(CellUtil.cloneValue(cell))
+ "\n";
out.writeBytes(line); // 写入HDFS文件
count++;
}
}
// 关闭资源
out.close();
scanner.close();
table.close();
fs.close();
System.out.println("导出完成!共导出 " + count + " 条记录到 " + hdfsPath);
}
/**
* 主方法:演示备份与恢复的完整流程
*/
public static void main(String[] args) {
try {
init();
// ===== 方式1:表间数据复制 =====
// 将students表的数据完整复制到students_backup表
// 前提:students_backup表已创建且列族与students表一致
copyTable("students", "students_backup", 100);
// ===== 方式2:Snapshot快照备份与恢复 =====
// 创建快照
createSnapshot("students", "students_snap_20240101");
// 查看所有快照
System.out.println("所有快照:");
admin.listSnapshots().forEach(snap ->
System.out.println(" " + snap.getName() + " | 表:" + snap.getTable())
);
// 从快照克隆新表
cloneFromSnapshot("students_snap_20240101", "students_clone");
// 从快照恢复原表(恢复到快照时的状态)
restoreFromSnapshot("students_snap_20240101", "students");
// ===== 方式3:导出到HDFS =====
exportToHdfs("students", "hdfs://localhost:9000/hbase-backup/students_export.txt");
close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
知识点总结速查表
┌─────────────────────────────────────────────────────────────────────┐
│ HBase 核心知识点总结 │
├────────────────────────┬────────────────────────────────────────────┤
│ 数据模型五要素 │ RowKey + Column Family + Qualifier │
│ │ + Timestamp + Value │
├────────────────────────┼────────────────────────────────────────────┤
│ Shell基本操作 │ create / put / get / scan / delete │
│ │ deleteall / disable / drop / alter │
├────────────────────────┼────────────────────────────────────────────┤
│ Java Admin API │ createTable / deleteTable / listTables │
│ (DDL操作) │ disableTable / enableTable │
├────────────────────────┼────────────────────────────────────────────┤
│ Java Table API │ put / get / scan / delete │
│ (DML操作) │ checkAndPut / checkAndDelete │
├────────────────────────┼────────────────────────────────────────────┤
│ Python操作 │ happybase.Connection / .table() │
│ │ table.put() / .row() / .scan() / .delete() │
├────────────────────────┼────────────────────────────────────────────┤
│ MapReduce集成 │ TableMapper / TableReducer │
│ │ TableMapReduceUtil.initTableMapperJob() │
├────────────────────────┼────────────────────────────────────────────┤
│ 数据备份方式 │ 1. Export/Import(MapReduce方式) │
│ │ 2. Snapshot快照(推荐,速度快) │
│ │ 3. CopyTable(Scan+Put方式) │
├────────────────────────┼────────────────────────────────────────────┤
│ RowKey设计 │ 唯一性 + 散列性 + 长度合理 + 业务相关 │
├────────────────────────┼────────────────────────────────────────────┤
│ 过滤器 │ SingleColumnValueFilter / PrefixFilter │
│ │ ColumnPrefixFilter / FilterList │
└────────────────────────┴────────────────────────────────────────────┘