HBase的rowkey本质上是字节数组(byte\[\]),既不是数字也不是字符串。
它按照字节的字典序逐字节比较排序,而非数值大小。
用户使用时看似数字,实际上是字符串形式的数字字符(如"001"存储为字节48,48,49)。
要实现数值排序效果,需采用固定长度前导零设计(如"00001")。
生产环境中,rowkey设计需兼顾唯一性、有序性、散列性和定长原则,常见方案包括:
- 反转时间戳+随机数(如"9223370336854775807_5")
- 固定长度数字(如"0000000123")
- 哈希+原值组合(如"3a2b_12345")
- 复合键结构(如"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) |
| 定长 | 固定长度,查询效率高 | 1、100、10000混用 |
二、案例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?
根据业务查询场景来设计,遵循几个原则:
唯一性:保证每个rowkey唯一标识一行
有序性:利用字典序满足范围查询,比如需要查最新数据就反转时间戳
散列性:用加盐或反转userId避免热点
定长:固定长度便于查询和存储
比如用户行为表,我用
反转userId_反转时间戳做rowkey,既能让数据均匀分布,又能快速查到用户的最新行为。