1.2 数据模型:RowKey、列族与版本


文档摘要

1.2 数据模型:RowKey、列族与单元格版本 本节摘要:HBase 的逻辑模型是一张按行键字典序排序的稀疏多维表。一个单元格由四维坐标唯一定位:行键 + 列族 + 列限定符 + 时间戳。理解"列族是物理存储单位、列限定符无需预先定义、单元格可存多版本"这三点,才算真正入门 HBase 数据模型。 从关系表到稀疏宽表 先看一段 Shell 里的真实数据,再谈概念。用 scan 输出一张用户行为表: 左边那列(u1001、u2002)就是 RowKey(行键),字节数组,最大约 64 KB(工程上建议别超 100 字节)。 和 是两个列族(Column Family),建表时就要定好、不建议后改;

1.2 数据模型:RowKey、列族与单元格版本

本节摘要:HBase 的逻辑模型是一张按行键字典序排序的稀疏多维表。一个单元格由四维坐标唯一定位:行键 + 列族 + 列限定符 + 时间戳。理解"列族是物理存储单位、列限定符无需预先定义、单元格可存多版本"这三点,才算真正入门 HBase 数据模型。

从关系表到稀疏宽表

先看一段 Shell 里的真实数据,再谈概念。用 scan 输出一张用户行为表:

hbase:012:0> scan 'user_actions', {LIMIT => 3} ROW COLUMN+CELL u_1001 cf:action=click, ts=1724055123, value=product_page u_1001 cf:action=click, ts=1724055101, value=home_banner u_1001 cf:device=os, ts=1724055123, value=android u_1001 detail:page=product_page, ts=1724055123, value=3c_sku_98 u_2002 cf:action=click, ts=1724055120, value=cart_icon 2 row(s)

左边那列(u_1001、u_2002)就是 RowKey(行键),字节数组,最大约 64 KB(工程上建议别超 100 字节)。cf:detail: 是两个列族(Column Family),建表时就要定好、不建议后改;冒号后面的 actionospage列限定符(Column Qualifier),写数据时随手就能新增,完全不用 DDL。列族加列限定符合称"列"。ts 是时间戳,默认取写入时刻的毫秒值,也可以客户端显式指定。

这三层结构带来一个关键性质:表是稀疏的。u_1001 有 detail:page,u_2002 没有,后者在存储里就真的不存在,而不是存一个 NULL 占位。一千万个用户每人几十个不重复的行为字段,在关系库里要几百列宽表、九成单元格为空;在 HBase 里只存真实出现过的值。这也是"列不限、行键定"的动态 schema 含义。

四维坐标定位一个单元格

HBase 里最小的存取单位是单元格(Cell),由四个维度唯一确定:

(rowkey, column family, column qualifier, timestamp)→ value

比如 ('u_1001', 'cf', 'action', 1724055123) 的值是 click。注意"列"不是一个容器而是一个坐标——cf:action 这个坐标在同一个行键下可以有多个时间戳,即多个版本的值。默认保留 1 个版本(建表参数 VERSIONS),可以设成更多:

hbase:013:0> create 'user_actions', {NAME => 'cf', VERSIONS => 5}, {NAME => 'detail'} hbase:014:0> get 'user_actions', 'u_1001', {COLUMN => 'cf:action', VERSIONS => 3} COLUMN CELL cf:action timestamp=1724055123, value=product_page cf:action timestamp=1724055101, value=home_banner 2 row(s)

多版本的典型用法:设备传感器把最新值反复写同一个单元格,历史值自动留痕;或者做"撤销"——写一个新版本覆盖旧值,TTL 到期后旧版本在 Compaction 时被清掉。反过来,HBase 没有真正的更新,所谓 update 就是同一坐标写一个新时间戳的值。删除也不是立刻抹掉,而是先写一条墓碑标记(tombstone),等 Compaction 时才真正清除——这个细节到第 3 章讲读路径时会回来咬人:被"删掉"的数据在合并前仍然占据读路径。

图 1.2-1 四维坐标与版本堆叠

图 1.2-1 四维坐标与版本堆叠

概念视图与物理视图的错位

教程爱画那张"宽表"图,但要警惕:逻辑上一行在物理上并不连续存放。物理存储按列族拆开——cf 的数据放在 cf 目录的 HFile 里,detail 的在另一个目录,一个列族一个 Store、一组 HFile。所以:

  • 查询只触碰一个列族时,另一个列族的数据完全不被读取——这就是"面向列"省 IO 的来源;
  • 一行写入两个列族,等于两个 Store 各写一份 MemStore,Flush 与 Compaction 也各自独立;
  • 列族数量建议一两个,三五 个以上基本是设计事故:每次 Flush 要产生的文件数等于列族数,小文件风暴和 Store 级膨胀都会随之而来(第 2 章细讲)。

行内的多列倒是连续存放的(KeyValue 格式里行键会重复出现在每个 KeyValue 上,用压缩缓解)。所以准确的一句话是:HBase 按"行键排序"组织,按"列族"拆分物理存储

💡 关键直觉:把列族当"文件的分组方式"来设计,把列限定符当"数据本身"来用。凡是想加第二个列族的时候,先问自己这两个组的读写模式是否真的不同到需要分开存。

RowKey 是字节数组,不是字符串

还有一个从 Shell 输出里看不出来的事实:RowKey 是原始字节数组,排序按字节逐位无符号比较,跟"字符串排序"并不总是一回事。这带来三条设计纪律,先埋个种子(第 5 章展开):

其一,数字要定长补零。行键里的序号写成 1210 时字典序是 1、10、2——位数不齐的数字排序必乱,必须格式化成定长(如八位补零)。

其二,数值编码要保序。把一个带符号整数直接按内存字节拼进行键,负数的字节会排在正数前面且顺序错乱。想表达"数值大小与字典序一致",得用保序编码(把符号位翻转的技巧,或直接用框架的 OrderedBytes 编码器)。时间戳这类恒正的数用可读字符串反倒简单安全。

其三,分隔符要挑在编码字母表中间。用 -: 这类比数字与字母都小的字符做字段分隔,前缀区间扫描时边界好算(1.4 节 u_1001~ 那个技巧就是同一思路的变体)。

顺带把命名空间说了:表的全名是 命名空间:表名,默认命名空间叫 default,系统表在 hbase 命名空间下(1.3 节要见的 Meta 表全名就是它)。业务上拿命名空间区分环境与业务线,比表名前缀更规范,权限也能按命名空间整体授予(第 7 章安全一节会用到)。

用 get/scan 体感一下模型

hbase:015:0> get 'user_actions', 'u_1001', 'cf' COLUMN CELL cf:action timestamp=1724055123, value=product_page cf:os timestamp=1724055123, value=android 2 row(s) hbase:016:0> scan 'user_actions', {STARTROW => 'u_1001', STOPROW => 'u_1001~'}

scan 是按行键区间推进的,STARTROW 含头、STOPROW 不含尾。注意 u_1001 后面拼一个 ~(ASCII 较大)是前缀匹配的常用技巧——这已经提前泄露了第 5 章的主题:所有查询模式最终都会被翻译成行键区间的算术。设计一张 HBase 表,本质是设计 RowKey 的编码。

本节要点回顾

  • 四维坐标:(行键,列族,限定符,时间戳)定位一个单元格,value 是纯字节数组;
  • 稀疏与动态:列限定符随时可加,空单元格不占空间,schema 在写入端演化;
  • 多版本:更新即写新时间戳,删除先写墓碑,物理清除要等 Compaction;
  • 物理按列族拆分:列族是 Store、Flush、Compaction 的单位,个数从严控制;
  • 一切查询皆行键区间:get 是长度为 1 的 scan,这句结论贯穿全册。

逻辑模型说完了,这行数据到底被谁管着?下一节进入机房视角:HMaster、RegionServer 与 ZooKeeper 的三层架构。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U