揭秘HBase Get 单行读取核心机制

客户端会先通过Zookeeper查找Meta表在的RegionServer(Meta表和普通表存储上没有差别),通过Meta表查找 key所在的Region 对应的 RegionServer,然后找到对应的HRegion,在每个Region进行查找。

客户端会​​缓存Meta表的信息​​(如Region位置),这能显著减少对Zookeeper和Meta表的访问压力,提升查询效率只有当缓存失效(如Region被迁移)时,才需要重新查询。

hbase:meta表记录了所有用户Region的详细信息(如Region所属的表、起始和结束RowKey、所在的RegionServer地址等),是客户端定位数据的“地图”

​Region的定位​​:每个Region负责存储表中一段​​连续​​的RowKey范围的数据。因此,只要知道RowKey,就可以通过hbase:meta表找到它所属的Region及其所在的RegionServer

HRegion响应

getInternal  是 HBase Region Server 处理单个 Get 请求的核心实现(最终调用这个函数进行实际的get)。首先看一下这个函数的签名和整体结构:

private List<Cell> getInternal(Get get, boolean withCoprocessor, long nonceGroup, long nonce)
  throws IOException {
  List<Cell> results = new ArrayList<>();

  // 1. pre-get 协处理器钩子
  if (withCoprocessor && (coprocessorHost != null)) {
    if (coprocessorHost.preGet(get, results)) {
      metricsUpdateForGet();
      return results;
    }
  }
  
  // 2. 将 Get 请求转换为 Scan
  Scan scan = new Scan(get);
  if (scan.getLoadColumnFamiliesOnDemandValue() == null) {
    scan.setLoadColumnFamiliesOnDemand(isLoadingCfsOnDemandDefault());
  }
  
  // 3. 获取 RegionScanner 并读取数据
  try (RegionScanner scanner = getScanner(scan, null, nonceGroup, nonce)) {
    List<Cell> tmp = new ArrayList<>();
    scanner.next(tmp);
    // 4. 克隆 Cell,确保数据安全
    for (Cell cell : tmp) {
      results.add(CellUtil.cloneIfNecessary(cell));
    }
  }

  // 5. post-get 协处理器钩子
  if (withCoprocessor && (coprocessorHost != null)) {
    coprocessorHost.postGet(get, results);
  }

  // 6. 更新统计指标
  metricsUpdateForGet();

  return results;
}

函数签名分析:

  • private List<Cell> getInternal(...):私有方法,是 HRegion 内部处理 Get 请求的具体实现。它返回一个 Cell 列表,每个 Cell 代表一个单元格(即一个列的值)。
  • Get get: 客户端发起的 Get 请求对象,包含了要查询的行键(Row Key)、列族(Column Family)、列(Qualifier)等信息。
  • boolean withCoprocessor: 一个标志,用于控制是否触发协处理器(Coprocessor)的钩子方法。
  • long nonceGroup, long nonce: 用于实现操作的幂等性,防止因重试导致同一操作被执行多次。

详细执行流程分析

1. 协处理器 preGet 钩子
// ...
if (withCoprocessor && (coprocessorHost != null)) {
  if (coprocessorHost.preGet(get, results)) {
    metricsUpdateForGet();
    return results;
  }
}
// ...

这是 Get 操作的第一个扩展点。在真正执行数据查找之前,HBase 会检查是否加载了协处理器。

  • coprocessorHost.preGet(): 如果存在协处理器,则会调用其 preGet 方法。
  • 作用: 协处理器可以在此阶段完全接管 Get 请求。例如,它可以进行权限校验,如果发现用户没有权限,可以直接抛出异常。或者,它可以实现自定义的缓存逻辑,如果数据在自定义缓存中命中,就可以直接填充 results 列表,并返回 true 来短路(Bypass)后续的HBase标准读取流程。
  • 如果 preGet 返回 true,意味着协处理器已经处理了这次请求,getInternal 方法会立即更新统计信息并返回结果。
2. 将 Get 转换为 Scan
// ...
Scan scan = new Scan(get);
if (scan.getLoadColumnFamiliesOnDemandValue() == null) {
  scan.setLoadColumnFamiliesOnDemand(isLoadingCfsOnDemandDefault());
}
// ...

在HBase内部,Get 操作被视为一种特殊的 Scan 操作,即只扫描一行数据。

  • new Scan(get): 这个构造函数会将 Get 对象的所有属性(如行键、列族、时间戳范围等)转换成一个 Scan 对象。这个 Scan 的 startRow 和 stopRow 都被设置为 Get 的行键,从而将扫描范围限定在这一行。
  • setLoadColumnFamiliesOnDemand: 这是一个重要的性能优化。如果开启,HBase在扫描时只会加载那些在查询中明确需要的列族的数据。如果一个表有多个列族,而查询只涉及其中一两个,这个特性可以显著减少I/O。这里会设置一个Region级别的默认值。
3. 获取 RegionScanner 并读取数据
// ...
try (RegionScanner scanner = getScanner(scan, null, nonceGroup, nonce)) {
  List<Cell> tmp = new ArrayList<>();
  scanner.next(tmp);
  // ...
}
// ...

这是整个 Get 操作的核心。

  • getScanner(scan, ...): 此方法会创建一个 RegionScanner 实例,它是用于在单个Region内扫描数据的迭代器。
  • scanner.next(tmp): 调用 next() 方法来获取下一行数据。对于由 Get 转换来的 Scan,这个方法最多只会返回一行数据。所有符合条件的 Cell 都会被添加到 tmp 列表中。
子函数分析: getScanner()

getScanner 的作用是构建一个复杂的扫描器体系,这个体系能够统一地、有序地访问存储在不同位置(内存和磁盘文件)的数据。其内部结构大致如下:

  1. RegionScannerImplgetScanner 方法最终会返回一个 RegionScannerImpl 对象。这是 RegionScanner 接口的实现类。
  2. StoreScanner: 在 RegionScannerImpl 内部,会为 Scan 请求中涉及的每一个列族(Store)创建一个 StoreScannerStoreScanner 负责扫描一个列族下的所有数据。
  3. KeyValueScanner 合并StoreScanner 本身是一个合并扫描器,它内部包含了一个 KeyValueScanner 列表,并将它们合并成一个有序的视图。这个列表通常包括:
    • 一个 MemStoreScanner: 用于扫描内存中(MemStore)还未刷写到磁盘的数据。
    • 多个 StoreFileScanner: 每个 HFile (在代码中称为 StoreFile) 对应一个 StoreFileScanner,用于扫描持久化在磁盘上的数据文件。
  4. 数据合并StoreScanner 会使用一个最小堆(Min-Heap)的数据结构,将来自 MemStoreScanner 和所有 StoreFileScanner 的数据进行归并排序,确保 Cell 按照行键、列、时间戳(降序)的顺序返回。
  5. 最终视图RegionScannerImpl 再将来自不同 StoreScanner(不同列族)的数据进行合并,最终呈现给调用者一个完整的、有序的行视图。

这个分层的扫描器设计是HBase读取路径的核心,它屏蔽了数据存储在内存还是磁盘的复杂性。

4. 克隆 Cell,确保数据安全
// ...
// Copy EC to heap, then close the scanner.
// This can be an EXPENSIVE call. It may make an extra copy from offheap to onheap buffers.
// See more details in HBASE-26036.
for (Cell cell : tmp) {
  results.add(CellUtil.cloneIfNecessary(cell));
}
// ...

scanner.next() 返回的 Cell 对象可能指向不稳定的内存区域。

  • 背景: 为了性能,HBase广泛使用堆外内存(Off-heap memory),例如 BucketCachescanner 返回的 Cell 可能只是一个指向堆外 ByteBuffer 的引用。
  • try-with-resources: 这个语法块会在执行完毕后自动调用 scanner.close()close() 方法会释放扫描器占用的所有资源,包括它引用的堆外内存块。
  • CellUtil.cloneIfNecessary(cell): 这个方法的关键作用是:检查 Cell 的数据是否在即将被释放的内存中。如果是,它会把这个 Cell 的内容(行键、列族、列、时间戳、值)完整地复制到一个新的、位于Java堆内存(On-heap)的 Cell 对象中。这样,即使扫描器关闭,返回给上层应用的数据依然是安全有效的。
  • 性能: 正如注释所说,这个克隆操作可能涉及内存拷贝,是有性能开销的。
5. 协处理器 postGet 钩子
// ...
if (withCoprocessor && (coprocessorHost != null)) {
  coprocessorHost.postGet(get, results);
}
// ...

在数据被成功检索并克隆后,返回给客户端之前,HBase提供了第二个协处理器扩展点。

  • coprocessorHost.postGet(): 调用协处理器的 postGet 方法。
  • 作用: 协处理器可以在此阶段对 results 列表进行检查、过滤或修改。例如,一个数据脱敏的协处理器可以在这里将结果中的敏感信息替换为*号。
6. 更新统计指标
// ...
metricsUpdateForGet();
// ...

最后,调用 metricsUpdateForGet() 方法来更新与 Get 操作相关的性能监控指标。

子函数分析: metricsUpdateForGet()
void metricsUpdateForGet() {
  if (this.metricsRegion != null) {
    this.metricsRegion.updateGet();
  }
  if (this.rsServices != null && this.rsServices.getMetrics() != null) {
    rsServices.getMetrics().updateReadQueryMeter(this, 1);
  }
}

这个方法会更新两个层面的统计:

  • Region层面metricsRegion.updateGet() 会增加该Region的get操作计数器。
  • RegionServer层面rsServices.getMetrics().updateReadQueryMeter() 会更新整个RegionServer的读请求计数器。

这些指标对于监控系统健康状况和性能调优至关重要。

总结

HRegion.getInternal 方法是HBase单行读取操作的“指挥中心”。它的执行流程可以概括为:

请求 -> preGet (可短路) -> Get转Scan 

-> 构建扫描器体系(RegionScanner -> StoreScanner -> MemStore/HFile Scanners)

 -> 数据检索与合并 -> 数据克隆(保证安全) -> postGet (可修改) -> 更新Metrics -> 返回结果

HRegion.getScanner

这个方法是 HBase Region Server 处理所有扫描类型请求(包括 Get 和 Scan)的入口点,其核心职责是构建一个能够遍历 Region 数据的迭代器。

首先看 HRegion 中的 getScanner 方法。它有几个重载版本,但最终都调用到下面这个私有方法:

private RegionScannerImpl getScanner(Scan scan, List<KeyValueScanner> additionalScanners,
  long nonceGroup, long nonce) throws IOException {
  return TraceUtil.trace(() -> {
    // 1. 开始一个 Region 操作
    startRegionOperation(Operation.SCAN);
    try {
      // 2. 校验和准备 Scan 对象中的列族
      if (!scan.hasFamilies()) {
        // 如果 Scan 未指定列族,则默认扫描所有列族
        for (byte[] family : this.htableDescriptor.getColumnFamilyNames()) {
          scan.addFamily(family);
        }
      } else {
        // 如果指定了列族,则逐一校验是否存在
        for (byte[] family : scan.getFamilyMap().keySet()) {
          checkFamily(family);
        }
      }
      // 3. 实例化真正的 Scanner
      return instantiateRegionScanner(scan, additionalScanners, nonceGroup, nonce);
    } finally {
      // 4. 结束 Region 操作
      closeRegionOperation(Operation.SCAN);
    }
  }, () -> createRegionSpan("Region.getScanner"));
}

函数逻辑分析:

  1. startRegionOperation: 这是一个重要的生命周期管理方法。它会检查 Region 是否正在关闭(closing),如果是则抛出异常。这确保了在扫描期间,Region 不会被关闭或分裂,保证了扫描过程的稳定性。
  2. 校验和准备列族: 代码确保 Scan 对象是合法的。如果客户端没有指定要扫描哪些列族,它会默认扫描表的所有列族。如果指定了,它会检查这些列族是否真实存在于表定义中。
  3. instantiateRegionScanner: 这是创建扫描器的核心步骤,我们将重点分析它。
  4. closeRegionOperation: 在 finally 块中调用,与 startRegionOperation 配对,表示扫描操作的生命周期结束。
  5. TraceUtil.trace: 整个逻辑被包裹在追踪工具中,用于分布式追踪系统(如 OpenTelemetry/Zipkin)记录该操作的耗时和跨度。

子函数分析: instantiateRegionScanner

这个方法是实际创建 RegionScanner 实例的地方。

// ... existing code ...
protected RegionScannerImpl instantiateRegionScanner(Scan scan,
  List<KeyValueScanner> additionalScanners, long nonceGroup, long nonce) throws IOException {
  if (scan.isReversed()) {
    if (scan.getFilter() != null) {
      scan.getFilter().setReversed(true);
    }
    return new ReversedRegionScannerImpl(scan, additionalScanners, this, nonceGroup, nonce);
  }
  return new RegionScannerImpl(scan, additionalScanners, this, nonceGroup, nonce);
}
// ... existing code ...

逻辑分析:

  • 该方法的主要作用是根据 Scan 对象是否是反向扫描(isReversed())来决定实例化哪种扫描器。
  • 正向扫描: 创建 RegionScannerImpl 实例。
  • 反向扫描: 创建 ReversedRegionScannerImpl 实例。ReversedRegionScannerImpl 继承自 RegionScannerImpl,并重写了部分逻辑以支持从后往前的扫描。

为了理解核心机制,我们接下来深入分析 RegionScannerImpl 的构造和初始化过程。

RegionScannerImpl 的构造与初始化

RegionScannerImpl 的构造函数是整个扫描器体系建立的核心。它调用了一系列初始化方法来构建一个分层的扫描器结构。

RegionScannerImpl(Scan scan, List<KeyValueScanner> additionalScanners, HRegion region,
    long nonceGroup, long nonce) throws IOException {
  // ...
  // 1. 确定读快照点 (Read Point)
  if (mvccReadPoint > 0) {
    this.readPt = mvccReadPoint;
  } else {
    this.readPt = region.getReadPoint(isolationLevel);
  }
  // ...
  // 2. 初始化所有底层的 KeyValueScanner
  initializeScanners(scan, additionalScanners);
}
1. 确定读快照点 (Read Point)

readPt (Read Point) 是一个非常关键的概念,它代表了本次扫描可见数据的“快照”版本。所有序列号(Sequence ID)大于 readPt 的数据变更(Put/Delete)对本次扫描都是不可见的。这保证了扫描操作的读一致性

2. initializeScanners - 构建扫描器金字塔

这是最核心的初始化方法,它构建了一个“扫描器金字塔”来统一访问来自不同列族、不同文件以及内存中的数据。

private void initializeScanners(Scan scan, List<KeyValueScanner> additionalScanners)
  throws IOException {
  // ...
  List<KeyValueScanner> scanners = new ArrayList<>(scan.getFamilyMap().size());
  List<KeyValueScanner> joinedScanners = new ArrayList<>(scan.getFamilyMap().size());
  // ...
  try {
    // a. 为每个列族创建一个 StoreScanner
    for (Map.Entry<byte[], NavigableSet<byte[]>> entry : scan.getFamilyMap().entrySet()) {
      HStore store = region.getStore(entry.getKey());
      KeyValueScanner scanner = store.getScanner(scan, entry.getValue(), this.readPt);
      // ...
      // b. 根据是否"按需加载"决定放入哪个列表
      if (
        this.filter == null || !scan.doLoadColumnFamiliesOnDemand()
          || this.filter.isFamilyEssential(entry.getKey())
      ) {
        scanners.add(scanner);
      } else {
        joinedScanners.add(scanner);
      }
    }
    // ...
    // c. 初始化顶层合并堆
    initializeKVHeap(scanners, joinedScanners, scan);
  } 
  // ...
}

逻辑分析:

  • a. 获取 StoreScanner: 循环遍历 Scan 请求中涉及的每个列族(Column Family)。

    • region.getStore(family): 获取代表该列族的 HStore 对象。
    • store.getScanner(...)这是又一层关键的递归调用HStore 的 getScanner 方法会创建一个 StoreScannerStoreScanner 负责扫描单个列族的所有数据,它内部会再次创建一个合并堆(KeyValueHeap),用于合并来自该列族 内存(MemStore)和所有磁盘文件(HFile) 的数据流。
  • b. 按需加载列族 (Load Column Families On Demand): 这是一个性能优化。如果开启了此功能,并且 Filter 认为某个列族不是“必需”的(isFamilyEssential返回false),那么这个列族的 StoreScanner 会被放入 joinedScanners 列表。这些列族的数据只有在确定某一行需要被返回时,才会去读取,从而避免了不必要的 I/O。

  • c. initializeKVHeap: 这个方法负责构建“金字塔”的顶端。它接收来自所有“必需”列族的 StoreScanner 列表(scanners),并使用一个顶层的 KeyValueHeap(在 RegionScannerImpl 中名为 storeHeap)将它们合并成一个统一的、跨所有列族的、有序的 KeyValue 流。

扫描器体系结构总结

HRegion.getScanner 的调用链最终构建了一个层次化的扫描器结构,可以总结如下:

  1. RegionScannerImpl (顶层): 客户端直接交互的对象。它管理着整个扫描过程的生命周期和状态。

    • 内部包含一个 KeyValueHeap (名为 storeHeap)。这个堆的作用是跨列族合并数据。它从每个 StoreScanner 中取出最小的 KeyValue(Cell),从而保证 RegionScannerImpl 对外总是返回全局有序的数据。
  2. StoreScanner (中间层,每个列族一个): 负责扫描单个列族的数据。

    • 内部也包含一个 KeyValueHeap。这个堆的作用是合并单个列族内部的数据源
  3. MemStoreScanner 和 StoreFileScanner (底层,每个数据源一个):

    • MemStoreScanner: 扫描内存(MemStore)中的数据。
    • StoreFileScanner: 扫描单个 HFile 磁盘文件中的数据。

这个金字塔形的结构,通过两层 KeyValueHeap 的归并排序,优雅地解决了从多个物理位置(内存、多个HFile)和逻辑单元(多个列族)中读取数据,并最终呈现给用户一个统一、有序的数据视图的问题。

StoreScanner

StoreScanner 是 HBase 扫描器体系中非常关键的一环,它位于 RegionScanner 之下,负责扫描 单个列族(Store) 的数据。一个 Store 的数据分布在两处:内存中的 MemStore 和磁盘上的多个 HFile(在代码中称为 StoreFile)。StoreScanner 的核心职责就是将这些来源的数据聚合成一个统一、有序的 KeyValue(Cell)流。

我们来分析构造函数,它是为 用户扫描(User Scan) 创建 StoreScanner 的主要入口。

// ... existing code ...
  /**
   * Opens a scanner across memstore, snapshot, and all StoreFiles. Assumes we are not in a
   * compaction.
   * @param store   who we scan
   * @param scan    the spec
   * @param columns which columns we are scanning
   */
  public StoreScanner(HStore store, ScanInfo scanInfo, Scan scan, NavigableSet<byte[]> columns,
    long readPt) throws IOException {
    // 1. 调用私有构造函数,初始化基本成员变量
    this(store, scan, scanInfo, columns != null ? columns.size() : 0, readPt, scan.getCacheBlocks(),
      ScanType.USER_SCAN);
    if (columns != null && scan.isRaw()) {
      throw new DoNotRetryIOException("Cannot specify any column for a raw scan");
    }
    // 2. 创建查询匹配器 (Query Matcher)
    matcher = UserScanQueryMatcher.create(scan, scanInfo, columns, oldestUnexpiredTS, now,
      store.getCoprocessorHost());

    // 3. 注册为观察者,用于处理 flush
    store.addChangedReaderObserver(this);

    List<KeyValueScanner> scanners = null;
    try {
      // 4. 从 Store 获取并筛选底层的 KeyValueScanner
      // Pass columns to try to filter out unnecessary StoreFiles.
      scanners = selectScannersFrom(store,
        store.getScanners(cacheBlocks, scanUsePread, false, matcher, scan.getStartRow(),
          scan.includeStartRow(), scan.getStopRow(), scan.includeStopRow(), this.readPt,
          isOnlyLatestVersionScan(scan)));

      // 5. 对所有底层 Scanner 执行 seek 操作
      // Seek all scanners to the start of the Row (or if the exact matching row
      // key does not exist, then to the start of the next matching Row).
      // Always check bloom filter to optimize the top row seek for delete
      // family marker.
      seekScanners(scanners, matcher.getStartKey(), explicitColumnQuery && lazySeekEnabledGlobally,
        parallelSeekEnabled);

      // 6. 设置 per-family 的 limit 和 offset
      // set storeLimit
      this.storeLimit = scan.getMaxResultsPerColumnFamily();

      // set rowOffset
      this.storeOffset = scan.getRowOffsetPerColumnFamily();
      addCurrentScanners(scanners);
      // 7. 创建 KeyValueHeap,合并所有 Scanner
      // Combine all seeked scanners with a heap
      resetKVHeap(scanners, comparator);
    } catch (IOException e) {
      clearAndClose(scanners);
      // remove us from the HStore#changedReaderObservers here or we'll have no chance to
      // and might cause memory leak
      store.deleteChangedReaderObserver(this);
      throw e;
    }
  }
// ... existing code ...

详细执行流程解析:

  1. 调用私有构造函数this(...) 调用会初始化一些基础属性,如 comparatorscan 对象、readPt(读快照点)、oldestUnexpiredTS(最老的未过期时间戳)等。它还会根据扫描类型(Get/Scan)和配置决定是使用 PREAD 还是 STREAM 模式读取 HFile。

  2. 创建 UserScanQueryMatchermatcher 是一个非常重要的组件。它封装了所有查询条件,包括行键范围、列、时间范围、版本数以及过滤器(Filter)。在后续的扫描过程中,StoreScanner 会用 matcher 来判断一个 KeyValue 是否符合用户查询,以及是否可以跳过某些数据。

  3. 注册观察者store.addChangedReaderObserver(this) 将自己注册到 HStore。当 MemStore 发生 flush 时,HStore 会通知所有观察者(调用 updateReaders 方法),StoreScanner 就可以动态地将新生成的 HFile 的 Scanner 加入到当前的扫描中,保证扫描能看到 flush 后的数据。

  4. 获取并筛选 KeyValueScanner:

    • store.getScanners(...): 这是关键的一步。它会返回代表当前 Store 中所有数据源的 Scanner 列表,这通常包括:
      • 一个或多个 MemStoreScanner(用于扫描内存数据)。
      • 多个 StoreFileScanner(每个 HFile 对应一个)。
    • selectScannersFrom(...): 这是一个优化步骤。它会遍历 store.getScanners 返回的所有 Scanner,并根据 Scan 的条件(如时间范围、TTL)和 HFile 的元数据(如布隆过滤器、时间范围)来提前排除掉那些肯定不包含所需数据的 HFile。例如,如果一个 HFile 的时间范围与查询的时间范围完全没有交集,那么对应的 StoreFileScanner 就会被直接关闭,根本不会参与后续的扫描。
  5. 执行 seek 操作seekScanners(...) 会在所有通过筛选的 Scanner 上执行 seek 操作,将它们的内部指针定位到 scan 的起始行键(startRow)。这也是一个优化点,避免了从头扫描每个 HFile。

  6. 设置 Limit/Offset: 从 Scan 对象中获取针对该列族的行数限制和偏移量。

  7. 创建 KeyValueHeapresetKVHeap(...) 会创建一个 KeyValueHeap 实例。KeyValueHeap 是一个最小堆(Min-Heap),它将所有底层的 Scanner(来自 MemStore 和 HFiles)作为输入源。通过反复调用堆的 peek() 和 next() 方法,StoreScanner 可以高效地以正确的顺序(按 KeyValue 排序)从所有数据源中取出最小的 KeyValue,从而实现归并排序的效果。

UserScanQueryMatcher类详细分析

UserScanQueryMatcher是HBase中专门处理用户扫描请求的查询匹配器,它继承自ScanQueryMatcher抽象基类,负责决定每个Cell是否应该被包含在扫描结果中。与StoreScanner配合,它构成了HBase扫描操作的核心决策引擎。

protected final boolean hasNullColumn;     // 是否查询空列
protected final Filter filter;             // 用户指定的过滤器
protected final byte[] stopRow;            // 扫描终止行
protected final TimeRange tr;                // 时间范围约束
private final int versionsAfterFilter;     // 过滤后的版本数限制
private int count = 0;                       // 当前列的计数器
private ExtendedCell curColCell = null;    // 当前列的引用

核心方法实现

列匹配逻辑 (matchColumn方法)

这是UserScanQueryMatcher最核心的方法,它实现了完整的列匹配决策链:

protected final MatchCode matchColumn(ExtendedCell cell, long timestamp, byte typeByte) throws IOException {
    // 步骤1: 时间范围检查
    int tsCmp = tr.compare(timestamp);
    if (tsCmp > 0) return MatchCode.SKIP;  // 时间戳太大,跳过
    if (tsCmp < 0) return columns.getNextRowOrNextColumn(cell);  // 时间戳太小,跳到下一行/列
    
    // 步骤2: 检查是否为用户请求的列
    MatchCode matchCode = columns.checkColumn(cell, typeByte);
    if (matchCode != MatchCode.INCLUDE) {
        return matchCode;
    }
    
    // 步骤3: 版本数量检查
    matchCode = columns.checkVersions(cell, timestamp, typeByte, false);
    
    // 步骤4: 如果有过滤器,合并过滤器响应
    return filter == null ? matchCode : mergeFilterResponse(cell, matchCode, filter.filterCell(cell));
}
过滤器响应合并 (mergeFilterResponse方法)

这个方法实现了复杂的过滤器与列检查器的决策合并逻辑,通过一个完整的决策矩阵来处理各种组合情况:

private final MatchCode mergeFilterResponse(ExtendedCell cell, MatchCode matchCode, ReturnCode filterResponse) {
    switch (filterResponse) {
        case SKIP:
            if (matchCode == MatchCode.INCLUDE) return MatchCode.SKIP;
            if (matchCode == MatchCode.INCLUDE_AND_SEEK_NEXT_COL) return MatchCode.SEEK_NEXT_COL;
            if (matchCode == MatchCode.INCLUDE_AND_SEEK_NEXT_ROW) return MatchCode.SEEK_NEXT_ROW;
            break;
        case NEXT_COL:
            // 根据列跟踪器决定跳到下一列还是下一行
            if (matchCode == MatchCode.INCLUDE || matchCode == MatchCode.INCLUDE_AND_SEEK_NEXT_COL) {
                return columns.getNextRowOrNextColumn(cell);
            }
            break;
        case INCLUDE_AND_SEEK_NEXT_ROW:
            matchCode = MatchCode.INCLUDE_AND_SEEK_NEXT_ROW;
            break;
    }
    
    // 版本数控制:确保不超过扫描设置的最大版本数
    if (curColCell == null || !CellUtil.matchingRowColumn(cell, curColCell)) {
        count = 0;
        curColCell = cell;
    }
    count++;
    
    if (count > versionsAfterFilter) {
        // 超过版本限制,优化跳到下一行或列
        if (matchCode == MatchCode.INCLUDE_AND_SEEK_NEXT_ROW) {
            matchCode = MatchCode.SEEK_NEXT_ROW;
        } else {
            matchCode = MatchCode.SEEK_NEXT_COL;
        }
    }
    return matchCode;
}

StoreScanner与UserScanQueryMatcher的互动机制

StoreScanner和UserScanQueryMatcher形成了一个生产者-消费者模式:

  • StoreScanner:负责从多个数据源(MemStore、StoreFile)读取Cell数据
  • UserScanQueryMatcher:负责决定每个Cell是否应该被包含在最终结果中

在StoreScanner的next()方法中,交互流程如下:

// 1. StoreScanner从heap中获取下一个cell
cell = heap.peek();

// 2. 调用UserScanQueryMatcher进行匹配决策
ScanQueryMatcher.MatchCode qcode = matcher.match(cell);

// 3. 根据匹配结果决定后续操作
switch (qcode) {
    case INCLUDE:
    case INCLUDE_AND_SEEK_NEXT_ROW:
    case INCLUDE_AND_SEEK_NEXT_COL:
        // 包含该cell,添加到结果中
        outResult.add(cell);
        // 根据具体情况决定是否跳到下一行/列
        if (qcode == ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_ROW) {
            seekOrSkipToNextRow(cell);
        } else if (qcode == ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL) {
            seekOrSkipToNextColumn(cell);
        }
        break;
        
    case SEEK_NEXT_ROW:
        // 需要跳到下一行
        seekOrSkipToNextRow(cell);
        break;
        
    case SEEK_NEXT_COL:
        // 需要跳到下一列
        seekOrSkipToNextColumn(cell);
        break;
        
    case SKIP:
        // 跳过该cell,继续下一个
        this.heap.next();
        break;
        
    case DONE:
    case DONE_SCAN:
        // 扫描结束
        close(false);
        return scannerContext.setScannerState(NextState.NO_MORE_VALUES).hasMoreValues();
}

智能跳转优化

UserScanQueryMatcher不仅告诉StoreScanner是否包含某个Cell,还会提供跳转建议,让StoreScanner能够智能地跳过大量不必要的数据:

行级跳转 (SEEK_NEXT_ROW)

当UserScanQueryMatcher确定当前行已经处理完毕时,会建议StoreScanner直接跳到下一行,避免扫描当前行的剩余列族和列。

列级跳转 (SEEK_NEXT_COL)

当确定当前列已经处理完毕时,建议直接跳到下一列,避免扫描当前列的更多版本。

智能提示跳转 (SEEK_NEXT_USING_HINT)

当过滤器提供了下一个可能匹配的Key提示时,UserScanQueryMatcher会建议StoreScanner直接跳转到该位置,实现最大幅度的跳过优化。

性能优化协作

批量控制

StoreScanner会根据UserScanQueryMatcher的决策结果,动态调整批量大小,确保在达到批量限制时及时返回:

if (scannerContext.checkBatchLimit(LimitScope.BETWEEN_CELLS)) {
    break LOOP;  // 达到批量限制,提前返回
}
内存控制

通过UserScanQueryMatcher的包含决策,StoreScanner能够精确控制内存使用,避免加载过多不必要的数据:

if (matcher.isUserScan() && totalBytesRead > maxRowSize) {
    throw new RowTooBigException("Max row size allowed: " + maxRowSize);
}
版本控制

UserScanQueryMatcher通过内部的countversionsAfterFilter机制,确保StoreScanner不会返回超过用户指定版本数的Cell,实现精确的版本控制。

设计亮点总结

  1. 职责分离:StoreScanner专注数据获取,UserScanQueryMatcher专注决策判断
  2. 智能跳转:通过MatchCode系统实现多级跳转优化,大幅提升扫描效率
  3. 过滤器集成:完美支持用户自定义过滤器,提供灵活的过滤能力
  4. 版本精确控制:确保返回结果严格符合用户的版本数要求
  5. 内存友好:通过精确的包含/排除决策,最小化内存占用

trySkipToNextRow 方法

在 HBase 中,数据是按块存储的,每个块通常包含多个 KeyValue。每个块都有一个索引键,表示该块中的第一个 KeyValue。

当 trySkipToNextRow 方法发现下一个索引键(即下一个数据块的起始键)已经大于当前要查找的目标行时,说明:

  • 当前数据块中不可能有目标行的数据
  • 目标行的数据在下一个数据块或更后面的块中

例子:

  • 当前在 BLOCK 1 的 r1/c3
  • 下一个索引键是 BLOCK 2 的 r1/c4
  • 但我们要找的是 r2(下一行),r1/c4 < r2,所以继续找
  • 发现下一个索引键已经是 r2/c1 或更大 → 直接 SEEK 到 r2

当发现下一个索引键不大于目标行时,说明:

  • 目标行的数据可能在当前数据块中
  • 只需要逐条跳过当前块中的数据即可找到目标

例子:

|    BLOCK 1                       |     BLOCK 2                      |
|  r1/c1/t5, r1/c1/t4, r1/c1/t3    |    r1/c1/t2, r1/c1/T1, r1/c2/T3  |
  • 当前在 r1/c1/t5,要找下一个列(比如 c2)
  • 下一个索引键是 r1/c1/t4,还在同一个块
  • 所以逐条 SKIP 即可:r1/c1/t4 → r1/c1/t3 → r1/c1/t2 → r1/c2/T3

核心代码逻辑

protected boolean trySkipToNextRow(ExtendedCell cell) throws IOException {
    ExtendedCell nextIndexedKey = getNextIndexedKey();  // 获取下一个索引键
    if (nextIndexedKey != null && 
        matcher.compareKeyForNextRow(nextIndexedKey, cell) >= 0) {  // 关键判断!
        // 如果下一个索引键 >= 目标行键,说明目标在下一个块中
        // 继续在当前块中 SKIP
        this.heap.next();  // 逐条跳过
        ++kvsScanned;
    } else {
        // 下一个索引键 < 目标行键,目标可能在更后面的块中
        // 直接返回 false,让调用者执行 SEEK
        return false;
    }
}

为什么这样优化?

  • SEEK 操作代价高:需要重新定位文件指针,可能涉及磁盘 I/O
  • SKIP 操作代价低:只是在当前已加载的内存块中移动指针
  • 智能选择:根据索引键预测哪种方式更高效,避免不必要的昂贵操作

每次都需要 scan 所有文件吗?

不是。 HBase 在 StoreScanner 层面做了大量优化来避免扫描所有数据:

  • HFile 筛选 (selectScannersFrom): 这是最重要的优化。在扫描开始前,HBase 会利用 HFile 的元数据(Metadata)进行预筛选。

    • 时间范围(Time Range): 如果 HFile 的时间范围与查询的时间范围不重叠,则该文件被跳过。
    • 布隆过滤器(Bloom Filter): 对于 Get 请求或指定了具体行键的 Scan,HBase 会使用行布隆过滤器(ROW)或行列布隆过滤器(ROWCOL)来快速判断一个 HFile 是否可能包含目标行/单元格。如果布隆过滤器判断肯定不存在,则该文件被直接跳过。
    • 行键范围(Key Range): HFile 也存储了最小和最大行键。如果查询的行键范围与 HFile 的行键范围没有交集,文件也会被跳过。
  • Seek 操作: 对于通过筛选的 HFile,扫描器不会从头读到尾。它会执行 seek 操作,直接将文件指针定位到查询的 startRow,然后从那里开始顺序读取。

  • Lazy Seek: 对于多列查询,lazySeekEnabledGlobally 开启时,seek 操作可能被推迟,进一步减少不必要的 I/O。

因此,一个典型的 Scan 或 Get 请求,只会打开和扫描那些可能包含所需数据的 HFile 的相关部分

如果内存有,还要扫描出所有 key 对应的 value 吗?

这个问题的核心是 HBase 如何处理同一行、同一列在不同地方(MemStore、多个 HFile)存在多个版本的数据。

答案是:需要扫描,但会高效地合并和过滤。

StoreScanner 的工作机制正是为了解决这个问题:

  1. 统一视图KeyValueHeap 将来自 MemStoreScanner 和所有相关 StoreFileScanner 的 KeyValue 放入同一个最小堆中进行排序。这意味着无论数据来自内存还是磁盘,都会被统一处理。

  2. 版本合并与过滤: 当 StoreScanner 从 KeyValueHeap 中取出 KeyValue 时,ScanQueryMatcher 会发挥作用:

    • 版本数控制matcher 会跟踪当前行已经返回了多少个版本的 KeyValue。如果超过了 scan指定的最大版本数,后续出现的更旧的版本就会被跳过。
    • Delete Marker 处理: 如果 matcher 看到了一个删除标记(Delete Marker),它会“遮蔽”掉所有时间戳比该标记更旧的、位于同一行或同一列的 KeyValue。这些被遮蔽的 KeyValue 也会被跳过。
    • TTL 过期matcher 也会检查 KeyValue 的时间戳,如果它已经因为超出了 TTL 而过期,也会被跳过。

举个例子: 假设你要 get 一行 row1,列 cf:q1 的数据,最大版本数为 1。

  • MemStore 中有 {row1, cf:q1, ts=10, value='new'}
  • 一个 HFile 中有 {row1, cf:q1, ts=5, value='old'}

StoreScanner 的处理流程如下:

  1. MemStoreScanner 和 StoreFileScanner 都被创建并 seek 到 row1
  2. KeyValueHeap 会同时包含来自内存和文件的 KeyValue。由于 ts=10 的 KeyValue 更“小”(在 HBase 排序中,时间戳降序排列),它会先从堆顶弹出。
  3. matcher 接收到 ts=10 的 KeyValue,发现符合条件,版本数计数变为 1。
  4. 接下来,ts=5 的 KeyValue 从堆顶弹出。
  5. matcher 接收到 ts=5 的 KeyValue,但发现 row1, cf:q1 的版本数已经达到上限(1),于是它会跳过这个 KeyValue

最终,只有内存中的新版本数据会返回给上层。虽然磁盘上的数据也被 seek 到了,但它在合并阶段就被高效地过滤掉了,避免了不必要的数据处理和返回。

内存里的数据不一定是“最终”的

直觉上认为:“内存(MemStore)是最新写入的,磁盘(HFile)是过去写入的,所以内存有结果直接返回不就行了吗?”

在大多数情况下这个逻辑是对的,但 HBase 作为一个通用的分布式数据库,必须处理以下三种情况,导致它不敢只看内存:

1. 时间戳(Timestamp)的灵活性

HBase 允许客户端在写入数据时自定义时间戳

  • 场景:如果一个客户端在 10:00 写入了 RowA 到磁盘(HFile),然后另一个客户端在 10:05 写入了一个带有 9:00 时间戳的 RowA 到内存(MemStore)。
  • 结果:虽然内存里的数据是“后写”的,但由于它的时间戳更小,在 HBase 的逻辑里,磁盘上的数据才是“最新”的。HBase 必须比较所有 Scanner 的时间戳才能决定谁排在前面。
2. 删除标记(Tombstone)的覆盖范围

删除在 HBase 中也是一次写入(写入一个 Delete 类型的 Cell)。

  • 场景:如果你发起一个 Get 请求,内存中确实有一个 Value=A。但是,在某个尚未合并(Compaction)的磁盘 HFile 中,可能存在一个 Family DeleteRow Delete 标记,其时间戳大于内存中这条记录的时间戳。
  • 结果:只有把内存和所有 HFile 的 Scanner 结果放在一起“对质”(归并排序),HBase 才能确定这条记录是不是真的由于某种原因被标记删除了。
3. 多版本请求(Max Versions)
  • 场景:用户查询时可能指定了 VERSIONS => 3
  • 结果:即使内存里有最新的 1 个版本,剩下的 2 个版本极有可能散落在磁盘的不同 HFile 中。为了凑齐这 3 个版本,系统必须扫描磁盘。

总结:内存里的数据是“最近写入”的,但不一定是“逻辑上最新”或“逻辑上完整”的。只有通过 KeyValueHeap 将所有来源的数据按 RowKey -> Column -> Timestamp 排序,才能得到最终真相。


​HBase StoreScanner 初始化流程的性能瓶颈​

在 HBase 的 ​​StoreScanner 初始化流程​​ 里,执行顺序如下:

  1. ​预加载所有 Scanner 并 Seek 到起始位置​

    • 对每个 ​​StoreFile​​ 构造 ​​StoreFileScanner​​,并立即执行 seekToStartKV()→ 触发 HFileReaderImpl.seek→ 读取 ​​block index​​ → 可能读取 ​​data block​

    • 对 ​​MemStore​​ 构造 ​​MemStoreScanner​​,并立即执行 seekToFirstInRange()→ 跳转到 ​​SkipList​​ 的第一个合法 ​​Cell​

  2. ​构建最小堆结构​

    将上述所有 Scanner 塞进以 ​​KeyValueHeap​​ 为底层结构的“最小堆”。

  3. ​延迟过滤​

    最后通过 ​​matcher(ScanQueryMatcher)​​ 做行级/列级过滤。

​关键问题​​:此时“已经发生的磁盘 seek”成为沉没成本——即使后续 ​​matcher​​ 快速判定“这一行/这一列不需要”,也只能将该 Scanner 标记为“已耗尽”或“跳过”,无法撤销之前已付出的 I/O。


​关键路径拆解(HBase 2.x/3.x 主干)​

1. StoreScanner.<init>
   ├─ 对每个 StoreFile 构造 StoreFileScanner
   │   └─ 立即执行 seekToStartKV() 
   │       → HFileReaderImpl.seek 
   │       → 读 block index 
   │       → 可能读 data block
   ├─ 对 MemStore 构造 MemStoreScanner
   │   └─ 立即执行 seekToFirstInRange() 
   │       → 跳到 SkipList 第一个合法 Cell
   ├─ 把所有 scanner 塞进 KeyValueHeap(最小堆)
   └─ 之后每次 next() 时
       ├─ KeyValueHeap.peek 返回最小 KV
       └─ 用 ScanQueryMatcher.match(KV) 决定是否返回给上层
           —— 若 matcher 返回 SKIP、DELETE、NEXT_COL 等,就继续 heap.next() 再试

​核心矛盾​​:seek 到第一个合法 KV发生在 ​​matcher 之前​​。若表包含大量版本/列,但 Scan 仅取最新版本或指定列,则第一次 seek 读入的 block 中大部分 KV 会被 matcher 跳过,但磁盘 I/O 已不可逆。


​社区已有的缓解手段​

1. ​​延迟 Seek(Lazy-seek)​
  • ​机制​​:早在 0.96 版本引入,但仅针对文件级别。

    • ​StoreFileScanner​​ 的 seekToStartKV()不会真正读取 data block,仅将 block index 加载到内存,真正的 data block I/O 延迟到第一次 next()被调用时。

  • ​收益​​:若某个文件在 matcher 第一轮即被判定“整文件无需读取”(例如该文件最大时间戳 < scan 的 time range),则后续永不触发 data block 读,节省磁盘 I/O。

  • ​局限​​:

    • 文件内第一个 block 只要被 matcher 判为“可能有用”,仍需读取一次;

    • ​MemStore 的 seek​​ 是实打实的跳表定位,无 lazy 机制。

2. ​​列族级布隆/时间范围过滤​
  • ​机制​​:HFile 的行级布隆、行+列布隆、时间范围索引(time-range meta block)在文件打开时已加载。

    • ​StoreFileScanner​​ 在 seek 之前,会根据 scan 的 row/column/time 条件计算“是否可能命中”的掩码。若掩码为全不命中,整个文件直接跳过(连 index 都不读)。

  • ​收益​​:可过滤大量无关文件。

  • ​局限​​:只要掩码判断“有可能”,仍需走后续 lazy-seek 流程。

3. ​​Scanner-level Timeout + Row-level Timeout​
  • ​机制​​:若单次 next()耗时过长,RegionServer 会主动标记该 scanner 超时,客户端重开新 scanner。

  • ​作用​​:虽无法减少初始 I/O,但可避免因读取过多无用 KV 导致的长尾延迟。


​还能再优化吗?​

​方案 A:将 Matcher 的“粗过滤”再往前挪​
  • ​背景​​:当前 lazy-seek 仅实现“文件级”跳过,列/版本级过滤仍需等待第一次 next()

  • ​思路​​:在 ​​StoreFileScanner.lazySeek()​​ 中,先读取 trailer + index + bloom + time-range,结合 matcher 的“最小列集合/版本号/时间戳”倒推出“第一个可能命中的 data block 编号”。若不存在此类 block,则直接返回 EMPTY_SEEK,连第一个 data block 都不读。

  • ​实现​​:相当于将 matcher 的“粗逻辑”复制一份到 StoreFileScanner 内部(复杂度较高)。

  • ​收益​​:

    • 对“只 scan 最新版本”场景,可跳过所有老版本 block;

    • 对“只 scan 少量列”场景,可跳过不含这些列的 block。

​方案 B:MemStore 也做“延迟定位”​
  • ​背景​​:当前 MemStoreScanner 在构造时直接跳到 SkipList 第一个合法节点。

  • ​思路​​:若 MemStore 体积大且 scan 范围极小(如单行+单列),可仅记录“起始 key + 是否可能命中”的布尔值,第一次 next()时再定位。

  • ​收益​​:减少初期 CPU 开销与 cache-miss,但对热行场景收益有限。

​方案 C:Top-N 优化(HBASE-24808 方向)​
  • ​背景​​:若 SQL/Scan 仅需 limit N 条结果。

  • ​思路​​:在 StoreScanner 中维护一个最大堆(或计数器),一旦获取 N 条结果,提前关闭剩余未发生 lazy-seek 的 scanner,避免无效 I/O。

  • ​收益​​:对“分页/最新 N 条”场景极其有效,已在 Phoenix 层完成 POC,社区合并回内核的呼声高。


​结论​

  1. ​“先 seek 后过滤”确实是主要性能损耗点,但社区已通过 ​​lazy-seek + 布隆/时间索引​​ 基本消除了“整文件”级别的无用 I/O。

  2. ​剩余优化空间​​:

    • “文件内第一个 block” 的 I/O 仍难以避免,除非将 matcher 的过滤逻辑下沉到 StoreFileScanner 内部(方案 A),或查询语义本身为 Top-N(方案 C)。

    • 在多数“随机分布” workload 下,第一个 block 命中率较高,因此单纯“跳过剩余堆”收益有限;真正的优化方向是 ​​连第一个 block 都不读​​,这需要更精细的索引/布隆与时间范围联动,或通过 limit 语义提前终止 scanner。

StoreFileScanner seek延迟IO

StoreFileScanner.java中:

// 延迟seek的关键变量
private boolean realSeekDone;
private boolean delayedReseek;
private ExtendedCell delayedSeekKV;

requestSeek方法实现了延迟seek逻辑:

  • 首先通过布隆过滤器判断是否需要真正的seek
  • 如果需要seek,创建"假"的KeyValue作为当前值,不实际执行seek
  • 设置realSeekDone = false标记延迟seek状态

KeyValueHeap.java中的pollRealKV()方法:

while (kvScanner != null && !kvScanner.realSeekDone()) {
  if (kvScanner.peek() != null) {
    kvScanner.enforceSeek(); // 真正执行延迟的seek
  }
}

只有当scanner需要成为堆顶元素时,才会真正执行seek操作。

延迟IO时 怎么构建正确最小堆?

StoreScanner.javaseekScanners方法:

if (isLazy) {
  for (KeyValueScanner scanner : scanners) {
    scanner.requestSeek(seekKey, false, true); // 延迟seek
  }
}

构建最小堆时,所有scanner都执行延迟seek,不实际读取数据。

KeyValueHeap.java的构造函数:

for (KeyValueScanner scanner : scanners) {
  if (scanner.peek() != null) {
    this.heap.add(scanner); // 使用peek()获取的"假"key进行比较
  }
}

比较的是各个scanner通过peek()返回的"假"key,这些key是在requestSeek中创建的,代表了该scanner可能包含的最小key。

当调用next()或需要获取真实数据时:

  1. 通过pollRealKV()检测到realSeekDone() == false
  2. 调用enforceSeek()执行真正的seek操作
  3. 更新scanner的实际当前值
  4. 重新参与堆排序

假key的核心特性是"不超越真实数据":

  • createFirstOnRowColTS:创建的假key时间戳 ≤ 文件中的真实最大时间戳
  • createLastOnRowCol:创建的假key是行+列+qualifier的最大可能值

时间戳是降序排列的,时间戳越大,排序越靠前。

StoreFileScanner.requestSeek()方法中,当seekTimestamp > maxTimestampInFile时:

if (seekTimestamp > maxTimestampInFile) {
    // Create a fake key that is not greater than the real next key.
    // (Lower timestamps correspond to higher KVs.)
    setCurrentCell(PrivateCellUtil.createFirstOnRowColTS(kv, maxTimestampInFile));
}

这里的语义是:

  • 我们要找一个不小于目标key的位置
  • 但目标key的时间戳比文件中任何时间都大
  • 所以我们创建一个假key,它使用文件最大时间戳,但类型为Maximum

通过PrivateCellUtil.createFirstOnRowColTS(kv, maxTimestampInFile)创建的假key:

public static ExtendedCell createFirstOnRowColTS(Cell cell, long ts) {
    return new FirstOnRowColTSCell(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength(),
        cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength(),
        cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength(), ts);
}

关键特性:

  • 时间戳:使用文件最大时间戳
  • 类型字节:继承自FirstOnRowColCell,而FirstOnRowColCell继承自FirstOnRowCell
  • FirstOnRowCell的getTypeByte()返回KeyValue.Type.Maximum.getCode()

KeyValueHeap.java中的pollRealKV方法是保证正确性的核心:

protected KeyValueScanner pollRealKV() throws IOException {
    KeyValueScanner kvScanner = heap.poll();
    if (kvScanner == null) {
        return null;
    }

    while (kvScanner != null && !kvScanner.realSeekDone()) {
        if (kvScanner.peek() != null) {
            kvScanner.enforceSeek(); // 强制执行真实seek
            Cell curKV = kvScanner.peek();
            
            // 关键:与堆中的下一个scanner比较
            KeyValueScanner nextEarliestScanner = heap.peek();
            if (nextEarliestScanner == null) {
                return kvScanner; // 堆中只剩这一个scanner
            }

            Cell nextKV = nextEarliestScanner.peek();
            if (nextKV == null || comparator.compare(curKV, nextKV) < 0) {
                // 当前scanner已经有最小的key,直接返回
                return kvScanner;
            }
            
            // 当前scanner的key不是最小的,放回堆中重新竞争
            heap.add(kvScanner);
        }
        kvScanner = heap.poll();
    }
    return kvScanner;
}

假key的排序保证

  • 假key ≤ 真实key(时间戳、类型等维度)
  • 假key之间可以正确比较
  • 假key与真实key比较时,真实key总是"更大"或相等

延迟执行的正确性

  • 只有在scanner可能成为堆顶时才执行真实seek
  • 真实seek后,假key被真实key替换
  • 如果真实seek后发现key不是最小的,scanner被放回堆中继续竞争

对于普通 Get,默认是几个版本?

对于一个普通的 Get 操作,如果不显式调用 get.readVersions(int versions) 或 get.readAllVersions(),默认只获取最新的一个版本。

@InterfaceAudience.Public
public class Get extends Query implements Row {
  private static final Logger LOG = LoggerFactory.getLogger(Get.class);

  private byte[] row = null;
  private int maxVersions = 1;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值