HBase rowkey:字节数组(byte[]),不是数字也不是字符串

HBase的rowkey本质上是字节数组(byte\[\]),既不是数字也不是字符串。


它按照字节的字典序逐字节比较排序,而非数值大小。


用户使用时看似数字,实际上是字符串形式的数字字符(如"001"存储为字节48,48,49)。


要实现数值排序效果,需采用固定长度前导零设计(如"00001")。


生产环境中,rowkey设计需兼顾唯一性、有序性、散列性和定长原则,常见方案包括:

  1. 反转时间戳+随机数(如"9223370336854775807_5")
  2. 固定长度数字(如"0000000123")
  3. 哈希+原值组合(如"3a2b_12345")
  4. 复合键结构(如"SH_20250529_000001")

典型应用场景:

  • 用户行为日志:反转用户ID_反转时间戳
  • 订单系统:区域码+日期+序列号(主表)+用户ID索引表
  • 设备监控:设备ID_反转时间戳
  • 社交关系:用户ID小_用户ID大
  • 时序数据:股票代码_反转时间戳

关键要诀:rowkey设计必须匹配核心查询模式,通过巧妙组合业务字段与时间要素,在存储效率与查询性能间取得平衡。

rowkey的数据类型是数字还是字符串,为什么是按字符串顺序排序,使用的时候好像是当数字用的


一、结论先行

问题 答案
rowkey的数据类型 字节数组(byte\[\]),不是数字也不是字符串
按什么排序 字节的字典序(逐字节比较)
为什么像数字 使用时当字符串,但内容恰好是数字字符

二、核心原理解释

2.1 rowkey 在 HBase 里到底是什么?

java

复制代码
// HBase 底层存储
rowkey 是 byte[](字节数组)

例如:
'001' 存的是三个字节:[48, 48, 49]
 '1'  存的是一个字节:[49]
 1    存的是四个字节:[0, 0, 0, 1]  // int类型

2.2 你写入的是字符串,不是数字

bash

复制代码
# 你写的命令
put 'student', '001', 'info:name', '张三'
#                  ↑
#            这是字符串 "001",不是数字 1

put 'student', '002', 'info:name', '李四'
#                  ↑
#            这是字符串 "002"

你把数字当作字符串写进去了!


三、字符串 vs 数字 排序对比

字符串排序(字典序)

bash

复制代码
"001" → 字节 [48,48,49]
"002" → 字节 [48,48,50]
"010" → 字节 [48,49,48]
"100" → 字节 [49,48,48]

排序结果:
"001" → "002" → "010" → "100"

# 你看到的顺序:1,2,10,100(字符串比较)

数字排序(如果是真正的数字)

bash

复制代码
1 → 字节 [0,0,0,1]
2 → 字节 [0,0,0,2]
10→ 字节 [0,0,0,10]
100→字节 [0,0,0,100]

排序结果:
1 → 2 → 10 → 100

# 数字顺序:1,2,10,100

四、为什么你感觉"当数字用"?

因为你查询时这样写:

bash

复制代码
# 范围查询
scan 'student', {STARTROW => '001', ENDROW => '100'}
# 你期望查到 001-100 范围
# 实际也是这个范围(因为字符串"001"到"100"刚好包含"002","010"...)

# 但这只是巧合!
# 如果你的 rowkey 是 "1","2","10","100"
# 范围查询 {STARTROW => '1', ENDROW => '10'}
# 只能查到 "1","10",查不到 "2"

五、真正当数字用的方式

方式1:使用数字类型(不推荐,HBase不支持直接存数字)

HBase 没有数字类型,需要自己转换:

bash

复制代码
# Java API 中可以这样
byte[] rowkey = Bytes.toBytes(100)  // 转成4字节

方式2:使用补位字符串(推荐)

bash

复制代码
# 想让数字按数值排序,用固定长度+前导零
"00001"  # 1
"00002"  # 2  
"00010"  # 10
"00100"  # 100

# 排序结果:00001 → 00002 → 00010 → 00100 ✅ 正确!

六、实际生产中的 rowkey 设计

bash

复制代码
# 常见设计1:反转时间戳 + 随机数(让最新数据在前面)
rowkey = (Long.MAX_VALUE - timestamp) + "_" + random

# 常见设计2:固定长度数字
rowkey = String.format("%010d", userId)  # "0000000123"

# 常见设计3:哈希 + 原值(避免热点)
rowkey = hash(userId) + "_" + userId

# 常见设计4:复合键
rowkey = regionCode + "_" + String.format("%010d", timestamp)

七、验证你的 rowkey 类型

bash

复制代码
# 查看 rowkey 的字节表示
hbase(main):> import org.apache.hadoop.hbase.util.Bytes
hbase(main):> get 'a2608:student', '001'
# 或直接用 Java API 查看

# 在 HBase Shell 中看
scan 'a2608:student', {RAW => true, VERSIONS => 1}

八、总结

text

复制代码
┌─────────────────────────────────────────────────┐
│  rowkey 本质是 byte[],不是数字也不是字符串      │
│  排序按字节字典序,不是数值大小                  │
│  你写的 '001' 是字符串,不是数字 1              │
│  想要数值排序 → 必须用固定长度 + 前导零          │
└─────────────────────────────────────────────────┘

记忆口诀:

text

复制代码
rowkey是字节数组,
排序按字节顺序。
数字想要排对序,
补前导零定长度。

生产环境 Rowkey 设计实战案例

一、核心原则(先记住这4点)

原则 说明 反面例子
唯一性 每个rowkey唯一标识一行 用timestamp做rowkey(会重复)
有序性 利用字典序满足查询需求 随机字符串(范围查询慢)
散列性 避免热点(读写集中在少数Region) 递增数字(所有新数据写同一个Region)
定长 固定长度,查询效率高 110010000混用

二、案例1:用户行为日志表

场景需求

text

复制代码
需求:
- 记录用户每次点击、浏览、购买行为
- 常见查询:查询某用户某时间段的行为
- 每天数据量:10亿条

不好的设计 ❌

bash

复制代码
# 用自增ID做rowkey
rowkey = 1000001
# 问题:无法按用户和时间查询,必须全表扫描

好的设计 ✅

bash

复制代码
# 设计:用户ID(反转) + 时间戳(反转)
rowkey = reverse(userId) + "_" + (Long.MAX_VALUE - timestamp)

# 具体例子
用户ID=12345,时间戳=2025-05-29 10:30:00
反转userId:54321
反转时间戳:9223372036854775807 - 1700000000 = 9223370336854775807

最终rowkey:54321_9223370336854775807

# 为什么这么设计?
1. 反转userId:让数据均匀分布(避免某用户数据集中)
2. 反转时间戳:最新数据排在最前面(查询快)

实际查询

bash

复制代码
# 查询用户12345最近10条数据
scan 'user_behavior', {
    STARTROW => '54321_',
    LIMIT => 10
}

# 查询用户12345某时间段的
scan 'user_behavior', {
    STARTROW => '54321_9223370336854775807',
    ENDROW => '54321_9223370336000000000'
}

三、案例2:订单表

场景需求

text

复制代码
需求:
- 按订单号查询(主要)
- 按用户ID查询订单
- 查询某个时间段的所有订单
- 每天数据量:5000万条

设计方案(二级索引思维)

主表:用订单号做rowkey

bash

复制代码
rowkey = orderId
# 订单号格式:区域码(2位) + 日期(8位) + 序列号(6位)
# 例子:SH20250529000001

# 好处:查单个订单极快 O(1)
get 'order', 'SH20250529000001'
辅助表:用户订单索引表

bash

复制代码
# 用户ID反查订单号
rowkey = userId + "_" + orderId
value = orderId

# 查询用户所有订单
scan 'order_index', {
    STARTROW => '12345_',
    ENDROW => '12345_z'
}

四、案例3:设备监控数据表

场景需求

text

复制代码
需求:
- 10万台设备,每5秒上报一次数据
- 查询某个设备最近1小时的数据
- 查询某个区域所有设备的数据
- 每天数据量:17亿条

设备数据表设计

bash

复制代码
# rowkey设计
rowkey = deviceId + "_" + reversedTimestamp

# 例子
设备ID:DEV_00123
时间戳:2025-05-29 10:30:00
反转时间戳:9223372036854775807 - 1700000000

最终rowkey:DEV_00123_9223370336854775807

# 为什么用设备ID在前?
- 快速定位某设备的所有数据(前缀扫描)
- 设备ID是查询的主要条件

查询示例

bash

复制代码
# 查询设备DEV_00123最新100条数据
scan 'device_data', {
    STARTROW => 'DEV_00123_',
    LIMIT => 100
}

# 查询设备DEV_00123某时间段的数据
scan 'device_data', {
    STARTROW => 'DEV_00123_9223370336854775807',
    ENDROW => 'DEV_00123_9223370000000000000'
}

五、案例4:社交关系表(好友关系)

场景需求

text

复制代码
需求:
- 查询某用户的所有好友
- 查询两个用户是否是好友
- 双向查询(A->B 和 B->A)

设计方案

bash

复制代码
# rowkey设计:用户ID小_用户ID大(保证唯一)
rowkey = min(userIdA, userIdB) + "_" + max(userIdA, userIdB)

# 例子:用户100和用户200成为好友
rowkey = "100_200"
value = {"relation": "friend", "createTime": "2025-05-29"}

# 查询用户100的所有好友
scan 'friends', {STARTROW => '100_', ENDROW => '100_z'}

# 判断100和200是否是好友
get 'friends', '100_200'  # 存在就是好友

六、案例5:时序数据(股票价格)

场景需求

text

复制代码
需求:
- 每只股票每秒一条价格数据
- 查询某股票最近N条数据
- 查询某股票某时间段的价格

设计方案

bash

复制代码
# rowkey = 股票代码 + 时间戳(反转)
rowkey = stockCode + "_" + (Long.MAX_VALUE - timestamp)

# 例子
股票:AAPL
时间:2025-05-29 14:30:00

rowkey:AAPL_9223370336854775807

# 优势
1. 同一只股票数据连续存储
2. 最新数据在最前面(反转时间戳)
3. 范围查询直接用STARTROW/ENDROW

查询示例

bash

复制代码
# 查AAPL最新10条
scan 'stock_price', {
    STARTROW => 'AAPL_',
    LIMIT => 10
}

# 查AAPL某时间段
scan 'stock_price', {
    STARTROW => 'AAPL_9223370336854775807',
    ENDROW => 'AAPL_9223370000000000000'
}

七、rowkey设计对照表

场景 推荐rowkey设计 长度 查单条 查范围
用户行为 反转userId_反转时间戳 定长
订单 订单号 定长 极快
设备监控 设备ID_反转时间戳 定长
社交关系 小ID_大ID 定长 极快 一般
时序数据 对象ID_反转时间戳 定长

八、常见问题和解决方案

问题1:热点Region

bash

复制代码
# 问题:递增rowkey导致所有新数据写入同一个Region
# 解决:加盐(前缀散列)

# 不好的设计
rowkey = timestamp  # 递增,热点

# 好的设计
rowkey = hash(userId) % 10 + "_" + timestamp  # 分散到10个前缀
# 例子:3_20250529143000, 7_20250529143001

问题2:长rowkey浪费存储

bash

复制代码
# 不好的设计
rowkey = "user_id_" + userId + "_timestamp_" + timestamp  # 太长

# 好的设计
rowkey = userId + "_" + timestamp  # 短
rowkey = Bytes.toBytes(userId) + Bytes.toBytes(timestamp)  # 更短(字节)

问题3:无法按时间查询

bash

复制代码
# 解决方案:建立时间索引表
# 主表:rowkey = 业务ID
# 索引表:rowkey = 时间戳 + 业务ID,value = 业务ID

九、生产环境rowkey模板

bash

复制代码
# 通用模板(直接套用)
{散列前缀}_ {业务主键} _ {反转时间戳}

# 例子1:用户登录日志
salt_ userId_reversedTimestamp
→ 5_12345_9223370336854775807

# 例子2:商品浏览记录
salt_productId_reversedTimestamp  
→ 2_ABC123_9223370336854775807

# 例子3:API调用日志
salt_apiPath_reversedTimestamp
→ 8_/api/login_9223370336854775807

十、面试回答模板

面试官:你怎么设计HBase的rowkey?

根据业务查询场景来设计,遵循几个原则:

  1. 唯一性:保证每个rowkey唯一标识一行

  2. 有序性:利用字典序满足范围查询,比如需要查最新数据就反转时间戳

  3. 散列性:用加盐或反转userId避免热点

  4. 定长:固定长度便于查询和存储

比如用户行为表,我用反转userId_反转时间戳做rowkey,既能让数据均匀分布,又能快速查到用户的最新行为。

相关推荐
Irene19918 小时前
HBase 关键字及大小写问题,中文的十六进制编码
hbase
Irene199110 小时前
WSL 环境中安装 HBase(前置条件 Hadoop 已安装并正在运行)
hbase
头歌实践平台11 小时前
HBase 完全分布式安装(新)
数据库·分布式·hbase
Irene199113 小时前
(课堂笔记)HBase(分布式、面向列的 NoSQL 数据库)基础
hbase
Irene199113 小时前
HBase 典型应用场景与阿里实践
hbase
大帅点兵1 天前
设计一个金融交易监控系统
大数据·clickhouse·flink·spark·kafka·hbase
abcy0712131 天前
HBase Region数据恢复详解
hbase
abcy0712131 天前
RegionServer 自动重启原因详解
hbase
r-t-H6 天前
从零开始搭建CDH-第十二章
linux·hive·spark·centos·hbase