2. HBase数据模型:表、行、列族、列限定符、时间戳、版本号的概念与关系

说实话,HBase的数据模型是我当年刚接触时最头疼的部分。它不像MySQL那样规规矩矩,也不像Redis那样简单粗暴。但一旦你理解了它的设计哲学,你会发现——嗯,这玩意儿天生就是为海量数据准备的。

咱们今天就把这六个核心概念掰开揉碎了讲。我保证,看完这一章,你脑子里会有一个清晰的立体模型。

2.1 整体印象:一张稀疏的、多维的、带版本的大表

先给你一个宏观画面。HBase本质上是一张稀疏的、可排序的、多维的Map。你可以把它想象成一个Excel表格,但有几个关键区别:

  • 行可以无限多(真的无限,只要集群够大)
  • 列可以动态添加(不需要提前定义所有列)
  • 每个单元格可以存多个历史版本(时间戳自动管理)
  • 大部分单元格是空的(稀疏性,不占存储)

我个人习惯用这个公式来理解:

RowKey + ColumnFamily + ColumnQualifier + Timestamp → Value

说白了,你拿着这四个坐标,就能定位到唯一的一个数据版本。

2.2 表(Table)

表是HBase中最顶层的逻辑容器。它由多个Region组成,每个Region负责一部分行键范围。我在项目中遇到过有人把HBase表设计成几百个列族,结果性能惨不忍睹——这个后面会细说。

创建一张表很简单:

create 'user_profile', 'info', 'behavior'

这里user_profile是表名,infobehavior是两个列族。注意,列族必须在建表时定义,而列限定符可以随时添加。

小提示:表名建议用有意义的英文,不要用中文。我见过有人用拼音做表名,维护起来那叫一个痛苦。

2.3 行(Row)与行键(RowKey)

行是HBase中数据的基本单位。每一行由一个唯一的行键(RowKey)标识。行键的设计,直接决定了你的查询性能。

行键有几个重要特性:

  • 唯一性:同一张表里,行键不能重复
  • 字典序排序:所有行按行键的字节序排序
  • 不可变:行键一旦写入,不能修改(只能删除后重新插入)

我曾经踩过一个坑:用时间戳作为行键前缀,结果所有新数据都写到了同一个Region上,造成了严重的热点问题。后来改成了用户ID哈希 + 时间戳的组合,才把负载分散开。

避坑指南:千万不要用单调递增的ID或时间戳作为行键!否则你的写入会全部打到一个Region上,集群性能直接腰斩。

2.4 列族(Column Family)

列族是HBase中物理隔离的单位。同一个列族下的所有列,在物理存储上放在一起。不同列族的数据,存储在独立的HFile中。

这意味着什么?

  • 列族不宜过多(官方建议2-3个)
  • 经常一起查询的列,放在同一个列族里
  • 每个列族可以单独设置属性(压缩、TTL、版本数等)

举个例子,假设我们要存储用户信息:

create 'user', 
  {NAME => 'base', VERSIONS => 3, COMPRESSION => 'SNAPPY'},
  {NAME => 'ext', VERSIONS => 1, TTL => 86400}

这里base列族存基本信息(保留3个版本,启用Snappy压缩),ext列族存扩展信息(只保留1个版本,1天后自动过期)。

我建议你把列族想象成MySQL里的表分区,只不过HBase帮你自动管理了。

2.5 列限定符(Column Qualifier)

列限定符就是列的名字。它和列族组合在一起,形成完整的列标识:列族:列限定符

比如:

base:name     -- 属于base列族,列名叫name
base:age      -- 属于base列族,列名叫age
ext:address   -- 属于ext列族,列名叫address

列限定符有几个特点:

  • 动态添加:不需要预先定义,写入时直接指定就行
  • 数量不限:理论上可以有上百万个不同的列限定符
  • 按字典序排序:同一行内,列按列限定符排序

我在项目中遇到过一种场景:用列限定符来存储时间序列数据。比如传感器每5分钟上报一次数据,我就用时间戳作为列限定符。这样一行就能存一天的采样数据,查询时按列范围扫描,效率非常高。

2.6 时间戳(Timestamp)与版本号(Version)

这两个概念经常被混为一谈,其实它们是一体两面的关系。

时间戳是HBase自动为每个单元格添加的版本标识,默认是写入时的毫秒级时间。你也可以手动指定时间戳,但我不建议你这么做——除非你有非常特殊的业务需求。

版本号是指同一个单元格(同一行、同一列族、同一列限定符)下,保留的历史数据个数。

看个例子就明白了:

put 'user', 'row1', 'base:name', '张三', 1000
put 'user', 'row1', 'base:name', '张三丰', 2000
put 'user', 'row1', 'base:name', '张无忌', 3000

如果列族base设置了VERSIONS => 3,那么查询时:

get 'user', 'row1', {COLUMN => 'base:name', VERSIONS => 3}

会返回三个版本:

时间戳
3000 张无忌
2000 张三丰
1000 张三

默认情况下,HBase只返回最新版本。你需要显式指定版本数才能拿到历史数据。

核心要点:时间戳是版本标识,版本号是保留个数。两者配合,实现了HBase的多版本控制能力。

2.7 六个概念的关系总结

来,咱们用一张图把关系理清楚:

表 (Table)
 └── 行 (Row) —— 由 RowKey 唯一标识
      └── 列族 (Column Family) —— 物理存储单元
           └── 列限定符 (Column Qualifier) —— 列名
                └── 时间戳 (Timestamp) —— 版本标识
                     └── 值 (Value) —— 实际数据

说白了,HBase的数据模型就是一个四层嵌套的Map

  • 第一层:RowKey → 行数据
  • 第二层:ColumnFamily → 列族数据
  • 第三层:ColumnQualifier → 列数据
  • 第四层:Timestamp → 版本数据

每个层级都按字典序排序,这就是HBase能高效扫描的底层原因。

2.8 实战建议

最后,给你几个我在实际项目中总结的经验:

  1. 列族控制在3个以内。超过3个,RegionServer的MemStore管理会变得复杂,影响写入性能。
  2. 行键设计要反范式。把经常一起查询的字段拼接到行键里,避免全表扫描。
  3. 版本数不要设太大。默认1个版本就够了,除非你有明确的审计或回滚需求。我见过有人设了100个版本,结果存储量暴涨10倍。
  4. 善用TTL。对于日志类数据,设置TTL自动过期,省得手动清理。

嗯,这一章的内容就到这儿。下一章咱们聊聊HBase的物理存储结构,看看数据到底是怎么落到磁盘上的。