深入 HBase 10 - 行级原子性与一致性边界
前置问题与边界
HBase 的一致性边界从 row key 开始。Put、Delete、Increment、Append、CheckAndMutate 都围绕单行建立语义;同一行内的多个 column family、qualifier 和版本可以在一次 mutation 中共同提交,但这个边界不会扩展成跨行事务、跨表事务或关系数据库里的多语句事务。
本篇只回答一个核心问题:这些单行操作能保证什么,跨行场景缺少什么。WAL、MemStore 和 MVCC 的写入细节在第 04 篇已经铺底;RegionServer 崩溃后的 WAL 恢复放到第 11 篇;异步复制的一致性边界放到第 12 篇。
HBase 是宽列模型,不是关系数据库。行级原子性不等于外键约束、唯一索引、二级索引事务,也不等于任意 SQL 事务。它的强项是把同一 row key 下的 mutation 收束到一个 RegionServer 的单行执行边界内。
操作路径图
flowchart TD
C[Client] --> RS[RegionServer]
RS --> R[Region by row key]
R --> L[Row mutation boundary]
L --> W[WAL append if durability requires it]
L --> M[MemStore update]
M --> V[MVCC read point]
V --> G[Get or Scan sees committed cells]
L --> X[No cross-row transaction]
这张图的重点是边界。HBase 可以把同一 row key 的一组 Cell 作为一个原子 mutation 处理,但两个 row key 即使相邻,也可能分属不同 Region;即使当前在同一个 Region,也没有公共事务协议把它们包成一笔提交。
关键对象和状态
| 对象 | 作用 | 一致性含义 |
|---|---|---|
| RowKey | 单行边界 | 同一 row key 内的 mutation 可作为原子单元 |
| Region | row-key range 的服务单元 | 单行操作路由到负责该 row key 的 RegionServer |
| WAL | 按 durability 记录 edit | 支撑崩溃恢复,不提供跨行事务 |
| MemStore | 保存可读的最新状态 | 已提交 mutation 通过 MVCC read point 暴露给读路径 |
| MVCC | 管理写入序列和读点 | 读请求看到某个一致的提交点 |
| CheckAndMutate | 条件更新 API | 条件检查和 mutation 在同一行内原子执行 |
Durability 会影响 mutation 是否要求 WAL 同步。SKIP_WAL 可以降低日志开销,但崩溃恢复边界随之变弱;它不能改变单行原子性的范围,也不能让跨行操作变成事务。
Put 与 Delete 的单行原子性
一次 Put 可以包含同一 row key 下多个列族和多个 qualifier。HBase 在该行的 mutation 边界内处理这些 Cell,读请求不会看到同一次 mutation 的半截结果。Delete 也是 mutation,它通常写入 tombstone;旧版本是否立刻消失取决于版本策略、TTL、扫描可见性和 compaction 条件。
同一行里,原子性覆盖的是 mutation 的提交边界,不是所有历史版本的立即物理清理。Delete 返回成功以后,读语义会根据 tombstone 过滤旧 Cell,但 HFile 上的旧数据可能要等后续 compaction 才有机会被清掉。
模式提炼:把事务边界压到单个分片键。公式可以写成 atomic({partition_key}, {mutation_set})。同一思想也出现在 Redis 单 key 操作、分片数据库按 shard key 更新、Kafka 单分区顺序处理里。边界越小,提交协议越简单;代价是跨边界约束要交给应用层或外部系统。
Increment 与 Append 的语义
Increment 针对同一行的数值列做原子加法,Append 针对同一行的字节值追加内容。它们适合计数、游标和同一实体内的轻量状态更新,但不适合表达“更新 A 行后必须更新 B 行”的约束。
Increment 和 Append 的返回值代表服务端执行后的结果。这个结果仍受单行边界约束。跨行计数器、跨用户余额、全局唯一二级索引,都不能靠多个 Increment 拼出 ACID 事务。
CheckAndMutate 的条件提交
CheckAndMutate 把条件检查和 mutation 绑定在同一行内执行。典型用途是“如果某一列仍为期望值,则写入新值”。它可以实现单行 CAS(Compare-And-Set)式保护,常用于状态机流转、幂等占位和同一实体的乐观更新。
条件和 mutation 都必须围绕同一个 row key 设计。HBase 2.6 的公共 API 支持通过 CheckAndMutate builder 构造条件更新;RowMutations 也只能把同一 row key 下的 Put 和 Delete 组合起来。把不同 row key 塞进同一个 RowMutations 不属于合法建模。
1 | |
UNVERIFIED_RUNTIME:上面的 Java 示例只使用 HBase 2.x public API 形态,当前环境未安装 HBase 2.6.6、Hadoop 3.4.3 和 JDK 17 组合,未在本机编译运行。正式核验应在目标版本的 Maven 工程中完成。
时间范围和读点
HBase 的版本读取还受 timestamp 和 time range 影响。公共 API 的 setTimeRange(minStamp, maxStamp) 使用左闭右开区间,即包含 minStamp,不包含 maxStamp。这条规则影响多版本读取和条件判断的测试设计。
MVCC 读点让一次读请求看到一个提交边界上的 Cell 集合。它不是关系数据库隔离级别的完整替代品,也不是跨行快照事务。Scan 的一致视图、客户端重试和 Region 移动仍需要按 HBase 的访问路径理解。
最小实验
UNVERIFIED_RUNTIME:以下命令用于 HBase 2.6.6、Hadoop 3.4.3、JDK 17 环境复现,当前任务未采集真实输出。
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
实验只观察同一 row key 下状态变化是否作为一个实体处理,不记录性能数字。CheckAndMutate 建议用上面的 Java public API 验证,因为 Shell 语法随版本和命令封装有差异。
失败恢复
单行 mutation 返回成功后的恢复能力取决于 durability。默认路径会借助 WAL 支撑 RegionServer 崩溃后的 replay;如果 mutation 使用 SKIP_WAL,未 flush 数据在进程或节点失败时可能丢失。WAL 支撑的是 edit 重放,不是跨行事务补偿。
客户端重试也要配合幂等设计。Put 写同一个 Cell 通常容易设计成幂等,Increment 和 Append 在客户端超时后更容易出现“不知道服务端是否已经执行”的灰区。需要精确一次语义时,单行 CAS、幂等请求号和业务状态机应一起设计。
工程迁移
| 需求 | HBase 建模 | 风险边界 |
|---|---|---|
| 单订单状态流转 | row key 为订单号,CheckAndMutate 更新状态 |
只保护该订单行 |
| 用户计数器 | row key 为用户号,Increment 更新计数 |
跨用户汇总需要异步计算 |
| 幂等写入 | 同一行保存 request id 和业务状态 | 请求号必须位于同一 row key |
| 二级索引 | 主表和索引表分两行或两表写 | HBase 不自动提供跨表事务 |
模式速查:听到“同一实体内状态变化”,优先考虑单行原子;听到“多个实体必须一起成功”,HBase 原生行级语义已经不够,需要事务层、补偿流程或换用具备相应事务边界的数据库。
常见误解
- “HBase 保证强一致”过于宽泛;更准确的说法是单行操作具备明确原子边界,复制、跨行和客户端重试另有边界。
- “RowMutations 是小事务”容易误导;它只组合同一 row key 下的
Put和Delete。 - “WAL 开启就等于事务日志”不准确;WAL 支撑恢复,不提供关系数据库事务管理器。
- “CheckAndMutate 能做分布式锁”需要限定到同一行状态;锁持有者、过期和失败恢复仍由应用设计。
练习
- 把一个订单状态机设计成单行 CAS,列出每个状态允许的前置状态。
- 设计一个跨两行转账流程,标出 HBase 原生语义覆盖不到的位置。
- 比较
Put、Increment、Append在客户端超时后的幂等风险。
系列导航
- 00 导读:row key 决定数据位置
- 01 数据模型:Cell、Column Family 与版本
- 02 集群架构:HMaster、RegionServer、ZooKeeper 与 hbase:meta
- 03 Schema 与 row key 设计
- 04 写入路径:WAL、MVCC、MemStore 与 flush
- 05 读取路径:BlockCache、Bloom Filter 与 HFile block
- 06 HFile 内部结构
- 07 Compaction、TTL、版本与 Delete
- 08 Region 生命周期:split、merge 与 assignment
- 09 Client 路由与重试
- 10 行级原子性与一致性边界(本篇)
- 11 RegionServer 崩溃与 WAL 恢复
- 12 跨集群 Replication
- 13 Snapshot、Backup 与恢复
- 14 Get、Scan、Filter 与分页
- 15 BufferedMutator 与 Bulk Load
- 16 Coprocessor、Endpoint 与 Phoenix 边界
- 17 Kerberos、RPC 保护与 ACL
- 18 Metrics、hbtop、Compaction 与性能调优
- 19 HBase 3.0 与设计边界
参考资料
- Apache HBase Reference Guide:https://hbase.apache.org/docs/
- Apache HBase User API:https://hbase.apache.org/2.6/apidocs/
- Apache HBase
TableAPI:https://hbase.apache.org/2.6/devapidocs/org/apache/hadoop/hbase/client/Table.html - Apache HBase
CheckAndMutateAPI:https://hbase.apache.org/2.6/apidocs/org/apache/hadoop/hbase/client/CheckAndMutate.html - Apache HBase
DurabilityAPI:https://hbase.apache.org/2.6/devapidocs/org/apache/hadoop/hbase/client/Durability.html - Apache HBase Data Model:https://hbase.apache.org/docs/datamodel/
