完全指南——谓词下推、比较过滤器与 FilterList 实战)
文档教程数据库【免费下载链接】db-tutorial 后端程序员应该掌握的主流数据库知识项目地址https://gitcode.com/gh_mirrors/db/db-tutorial点击查看免费下载HBase 的get()与scan()虽然支持按 row key、列族、列、时间戳与版本做条件查询但无法进行细粒度的数据筛选。Filter过滤器机制正是为弥补这一不足而设计它允许开发者基于 row key、列或列值在服务端完成过滤谓词下推只有满足条件的数据才会被传回客户端。本文以 db-tutorial 仓库的codes/javadb/hbase模块基于 HBase 2.x 客户端为佐证系统讲解比较过滤器、专用过滤器、包装过滤器与FilterList的完整用法并通过仓库源码揭示分页过滤与滚动查询的真实实现。过滤器机制概览HBase 中两种主要的数据读取方法是get()和scan()它们都支持直接访问数据和通过指定起止 row key 访问数据。此外可以指定列族、列、时间戳和版本号来进行条件查询。它们的缺点是不支持细粒度的筛选功能。为了弥补这种不足Get和Scan支持通过过滤器Filter对 row key、列或列值进行过滤。HBase 提供了一些内置过滤器也允许用户通过继承Filter类来自定义过滤器。所有的过滤器都在服务端生效称为谓词下推Predicate Pushdown。这样可以保证被过滤掉的数据不会被传到客户端从而显著降低网络传输开销。HBase 过滤器层次结构的最底层是Filter接口和FilterBase抽象类。大部分过滤器都直接继承自FilterBase其整体可分为三大类比较过滤器基于CompareFilter需要比较运算符 比较器两个参数专用过滤器通常直接继承自FilterBase面向更特定场景如按列前缀、时间戳、行键前缀过滤包装过滤器通过包装其他过滤器实现拓展功能如SkipFilter、WhileMatchFilter。在仓库的封装代码中过滤器最终都通过Scan#setFilter(Filter)挂载到一次扫描上见 HbaseTemplate.java 中scan(String tableName, Scan scan)对table.getScanner(scan)的封装。比较过滤器所有比较过滤器均继承自CompareFilter。CompareFilter比FilterBase多了一个compare()方法它需要传入参数定义比较操作的过程比较运算符和比较器。创建一个比较过滤器需要两个参数分别是比较运算符和比较器实例public CompareFilter(final CompareOp compareOp, final ByteArrayComparable comparator) { this.compareOp compareOp; this.comparator comparator; }比较运算符CompareFilter支持以下比较运算符运算符含义说明LESS小于LESS_OR_EQUAL小于等于EQUAL等于NOT_EQUAL!不等于GREATER_OR_EQUAL大于等于GREATER大于NO_OP—排除所有符合条件的值无操作比较运算符均定义在枚举类CompareOperator中InterfaceAudience.Public public enum CompareOperator { LESS, LESS_OR_EQUAL, EQUAL, NOT_EQUAL, GREATER_OR_EQUAL, GREATER, NO_OP, }注意在 1.x 版本的 HBase 中比较运算符定义在CompareFilter.CompareOp枚举类中但在 2.0 之后这个类就被标识为deprecated并会在 3.0 移除。所以 2.0 之后版本的 HBase 需要使用CompareOperator这个枚举类。db-tutorial 仓库的 HBase 模块基于 2.x 客户端见 pom.xml 中的hbase-client依赖测试与示例代码中均使用CompareOperator。比较器所有比较器均继承自ByteArrayComparable抽象类常用的有以下几种BinaryComparator使用Bytes.compareTo(byte[], byte[])按字典序比较指定的字节数组。BinaryPrefixComparator按字典序与指定的字节数组进行比较但只比较到这个字节数组的长度。RegexStringComparator使用给定的正则表达式与指定的字节数组进行比较。仅支持EQUAL和NOT_EQUAL操作。SubStringComparator测试给定的子字符串是否出现在指定的字节数组中比较不区分大小写。仅支持EQUAL和NOT_EQUAL操作。NullComparator判断给定的值是否为空。BitComparator按位进行比较。BinaryPrefixComparator和BinaryComparator的区别不是很好理解这里举例说明一下在进行EQUAL的比较时如果比较器传入的是abcd的字节数组但是待比较数据是abcdefgh如果使用的是BinaryPrefixComparator比较器则比较以abcd字节数组的长度为准即efgh不会参与比较这时候认为abcd与abcdefgh是满足EQUAL条件的如果使用的是BinaryComparator比较器则认为其是不相等的。比较过滤器种类比较过滤器共有五个HBase 1.x 版本和 2.x 版本相同RowFilter基于行键来过滤数据FamilyFilter基于列族来过滤数据QualifierFilter基于列限定符列名来过滤数据ValueFilter基于单元格cell的值来过滤数据DependentColumnFilter指定一个参考列来过滤其他列的过滤器过滤的原则是基于参考列的时间戳来进行筛选。前四种过滤器的使用方法相同均只要传递比较运算符和比较器实例即可构建然后通过setFilter方法传递给scanFilter filter new RowFilter(CompareOperator.LESS_OR_EQUAL, new BinaryComparator(Bytes.toBytes(xxx))); scan.setFilter(filter);DependentColumnFilter的使用稍微复杂一点下面单独说明。DependentColumnFilter可以把DependentColumnFilter理解为一个 ValueFilter 和一个时间戳过滤器的组合。DependentColumnFilter有三个带参构造器这里选择一个参数最全的进行说明DependentColumnFilter(final byte[] family, final byte[] qualifier, final boolean dropDependentColumn, final CompareOperator op, final ByteArrayComparable valueComparator)各参数含义如下family列族qualifier列限定符列名dropDependentColumn决定参考列是否被包含在返回结果内为true时表示参考列被返回为false时表示被丢弃op比较运算符valueComparator比较器。这里举例进行说明DependentColumnFilter dependentColumnFilter new DependentColumnFilter( Bytes.toBytes(student), Bytes.toBytes(name), false, CompareOperator.EQUAL, new BinaryPrefixComparator(Bytes.toBytes(xiaolan)));其过滤流程分三步首先会去查找student:name中值以xiaolan开头的所有数据获得参考数据集这一步等同于 ValueFilter 过滤器其次再用参考数据集中所有数据的时间戳去检索其他列获得时间戳相同的其他列的数据作为结果数据集这一步等同于时间戳过滤器最后如果dropDependentColumn为true则返回参考数据集结果数据集若为false则抛弃参考数据集只返回结果数据集。专用过滤器专用过滤器通常直接继承自FilterBase用于更特定的场景。单列列值过滤器SingleColumnValueFilter基于某列参考列的值决定某行数据是否被过滤。其实例有以下方法setFilterIfMissing(boolean filterIfMissing)默认值为false即如果该行数据不包含参考列其依然被包含在最后的结果中设置为true时则不包含setLatestVersionOnly(boolean latestVersionOnly)默认为true即只检索参考列的最新版本数据设置为false则检索所有版本数据。SingleColumnValueFilter singleColumnValueFilter new SingleColumnValueFilter( student.getBytes(), name.getBytes(), CompareOperator.EQUAL, new SubstringComparator(xiaolan)); singleColumnValueFilter.setFilterIfMissing(true); scan.setFilter(singleColumnValueFilter);单列列值排除器SingleColumnValueExcludeFilterSingleColumnValueExcludeFilter继承自上面的SingleColumnValueFilter过滤行为与其相反它在参考列满足条件的基础上将参考列本身从返回结果中排除例如按某列的值筛选出行但又不希望该列出现在结果中时非常有用。行键前缀过滤器PrefixFilter基于 RowKey 值决定某行数据是否被过滤——RowKey 以指定前缀开头的数据才会被返回PrefixFilter prefixFilter new PrefixFilter(Bytes.toBytes(xxx)); scan.setFilter(prefixFilter);列名前缀过滤器ColumnPrefixFilter基于列限定符列名决定某行数据是否被过滤——列名以指定前缀开头的列才会被返回ColumnPrefixFilter columnPrefixFilter new ColumnPrefixFilter(Bytes.toBytes(xxx)); scan.setFilter(columnPrefixFilter);分页过滤器PageFilter可以使用这个过滤器实现对结果按行进行分页创建PageFilter实例的时候需要传入每页的行数public PageFilter(final long pageSize) { Preconditions.checkArgument(pageSize 0, must be positive %s, pageSize); this.pageSize pageSize; }下面的代码体现了客户端实现分页查询的主要逻辑这里对其进行一下解释说明客户端进行分页查询需要传递startRow起始 RowKey知道起始startRow后就可以返回对应的pageSize行数据。这里唯一的问题就是对于第一次查询显然startRow就是表格的第一行数据但是之后第二次、第三次查询我们并不知道startRow只能知道上一次查询的最后一条数据的 RowKey简单称之为lastRow。我们不能将lastRow作为新一次查询的startRow传入因为 scan 的查询区间是[startRow, endRow)即左闭右开区间这样startRow在新的查询也会被返回这条数据就重复了。同时在不使用第三方数据库存储 RowKey 的情况下我们是无法通过知道lastRow的下一个 RowKey 的因为 RowKey 的设计可能是连续的也有可能是不连续的。由于 HBase 的 RowKey 是按照字典序进行排序的。这种情况下就可以在lastRow后面加上0作为startRow传入因为按照字典序的规则某个值加上0后的新值在字典序上一定是这个值的下一个值对于 HBase 来说下一个 RowKey 在字典序上一定也是等于或者大于这个新值的。所以最后传入lastRow0如果等于这个值的 RowKey 存在就从这个值开始 scan否则从字典序的下一个 RowKey 开始 scan。25 个字母以及数字字符字典排序如下0 1 2 ... 9 a b ... z分页查询主要实现逻辑byte[] POSTFIX new byte[] { 0x00 }; Filter filter new PageFilter(15); int totalRows 0; byte[] lastRow null; while (true) { Scan scan new Scan(); scan.setFilter(filter); if (lastRow ! null) { // 如果不是首行 则 lastRow 0 byte[] startRow Bytes.add(lastRow, POSTFIX); System.out.println(start row: Bytes.toStringBinary(startRow)); scan.withStartRow(startRow); } ResultScanner scanner table.getScanner(scan); int localRows 0; Result result; while ((result scanner.next()) ! null) { System.out.println(localRows : result); totalRows; lastRow result.getRow(); } scanner.close(); // 最后一页查询结束 if (localRows 0) break; } System.out.println(total rows: totalRows);注意在多台 Region Server 上执行分页过滤的时候由于并行执行的过滤器不能共享它们的状态和边界所以有可能每个过滤器都会在完成扫描前获取了PageCount行的结果这种情况下会返回比分页条数更多的数据分页过滤器就有失效的可能。仓库源码印证db-tutorial 的 BaseScan.java 在toScan()中为每个 scan 自动追加PageFilter(size)并以FilterList组合所有过滤器而 HbaseTemplate.java 的getPageData方法在处理下一页时使用scan.withStartRow(lastRow, false)——false表示起始行不包含在结果中这正是lastRow0防重复思路在 HBase 2.x 客户端 API 中的等价实现比手动拼接0x00更简洁可靠。滚动查询scroll则通过 SingleFamilyScan.java 与 MultiFamilyScan.java 中的withStartRow(scrollRow, false)实现同样的排除上页末行语义。时间戳过滤器TimestampsFilterTimestampsFilter只返回具有指定时间戳的单元格数据ListLong list new ArrayList(); list.add(1554975573000L); TimestampsFilter timestampsFilter new TimestampsFilter(list); scan.setFilter(timestampsFilter);首次行键过滤器FirstKeyOnlyFilterFirstKeyOnlyFilter只扫描每行的第一列扫描完第一列后就结束对当前行的扫描并跳转到下一行。相比于全表扫描其性能更好通常用于行数统计的场景因为如果某一行存在则行中必然至少有一列FirstKeyOnlyFilter firstKeyOnlyFilter new FirstKeyOnlyFilter(); scan.setFilter(firstKeyOnlyFilter);说明原文档示例中的scan.set(firstKeyOnlyFilter)为笔误正确写法应为scan.setFilter(firstKeyOnlyFilter)。包装过滤器包装过滤器就是通过包装其他过滤器以实现某些拓展的功能。SkipFilter 过滤器SkipFilter包装一个过滤器当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时则拓展过滤整行数据。下面是一个使用示例// 定义 ValueFilter 过滤器 Filter filter1 new ValueFilter(CompareOperator.NOT_EQUAL, new BinaryComparator(Bytes.toBytes(xxx))); // 使用 SkipFilter 进行包装 Filter filter2 new SkipFilter(filter1);典型场景当需要整行中只要有一个 cell 不满足条件就丢弃整行时用SkipFilter包装ValueFilter即可实现普通ValueFilter只会过滤掉单个 cell行仍会返回。WhileMatchFilter 过滤器WhileMatchFilter包装一个过滤器当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时WhileMatchFilter则结束本次扫描返回已经扫描到的结果。下面是其使用示例Filter filter1 new RowFilter(CompareOperator.NOT_EQUAL, new BinaryComparator(Bytes.toBytes(rowKey4))); Scan scan new Scan(); scan.setFilter(filter1); ResultScanner scanner1 table.getScanner(scan); for (Result result : scanner1) { for (Cell cell : result.listCells()) { System.out.println(cell); } } scanner1.close(); System.out.println(--------------------); // 使用 WhileMatchFilter 进行包装 Filter filter2 new WhileMatchFilter(filter1); scan.setFilter(filter2); ResultScanner scanner2 table.getScanner(scan); for (Result result : scanner2) { // 原文档此处的 scanner1 为笔误应为 scanner2 for (Cell cell : result.listCells()) { System.out.println(cell); } } scanner2.close();运行结果省略部分输出如下rowKey0/student:name/1555035006994/Put/vlen8/seqid0 rowKey1/student:name/1555035007019/Put/vlen8/seqid0 rowKey2/student:name/1555035007025/Put/vlen8/seqid0 rowKey3/student:name/1555035007037/Put/vlen8/seqid0 rowKey5/student:name/1555035007051/Put/vlen8/seqid0 rowKey6/student:name/1555035007057/Put/vlen8/seqid0 rowKey7/student:name/1555035007062/Put/vlen8/seqid0 rowKey8/student:name/1555035007068/Put/vlen8/seqid0 rowKey9/student:name/1555035007073/Put/vlen8/seqid0 -------------------- rowKey0/student:name/1555035006994/Put/vlen8/seqid0 rowKey1/student:name/1555035007019/Put/vlen8/seqid0 rowKey2/student:name/1555035007025/Put/vlen8/seqid0 rowKey3/student:name/1555035007037/Put/vlen8/seqid0可以看到被包装后只返回了rowKey4之前的数据未包装时RowFilter(NOT_EQUAL, rowKey4)只是跳过rowKey4这一行继续扫描包装WhileMatchFilter后一旦遇到需要过滤的rowKey4扫描立即终止。FilterList多个过滤器的组合以上都是讲解单个过滤器的作用当需要多个过滤器共同作用于一次查询的时候就需要使用FilterList。FilterList支持通过构造器或者addFilter方法传入多个过滤器// 构造器传入 public FilterList(final Operator operator, final ListFilter filters) public FilterList(final ListFilter filters) public FilterList(final Filter... filters) // 方法传入 public void addFilter(ListFilter filters) public void addFilter(Filter filter)多个过滤器组合的结果由operator参数定义其可选参数定义在Operator枚举类中只有MUST_PASS_ALL和MUST_PASS_ONE两个可选的值MUST_PASS_ALL相当于 AND必须所有的过滤器都通过才认为通过MUST_PASS_ONE相当于 OR只要有一个过滤器通过则认为通过。InterfaceAudience.Public public enum Operator { /** !AND */ MUST_PASS_ALL, /** !OR */ MUST_PASS_ONE }使用示例如下ListFilter filters new ArrayListFilter(); Filter filter1 new RowFilter(CompareOperator.GREATER_OR_EQUAL, new BinaryComparator(Bytes.toBytes(XXX))); filters.add(filter1); Filter filter2 new RowFilter(CompareOperator.LESS_OR_EQUAL, new BinaryComparator(Bytes.toBytes(YYY))); filters.add(filter2); Filter filter3 new QualifierFilter(CompareOperator.EQUAL, new RegexStringComparator(ZZZ)); filters.add(filter3); FilterList filterList new FilterList(filters); Scan scan new Scan(); scan.setFilter(filterList);上述示例组合了RowKey ≥ XXX、RowKey ≤ YYY与列名匹配正则 ZZZ三个条件FilterList默认使用MUST_PASS_ALLAND语义。仓库源码印证db-tutorial 的 BaseScan.java 中维护了一个ListFilter filters字段addFilter(Filter)方法向其中追加过滤器toScan()会先为size非空的查询追加PageFilter随后把所有过滤器统一放入一个FilterList并scan.setFilter(filterList)。这意味着仓库的SingleFamilyScan/MultiFamilyScan查询封装天然支持多过滤器组合例如可在HbaseTemplateScanTest见 测试用例的分页/滚动查询之上叠加任意业务过滤器。在 db-tutorial 仓库中落地实战上述过滤器机制在 db-tutorial 仓库中有完整的工程化落地可作为读者直接参考的实践样本连接初始化HbaseFactory.java 通过HBaseConfiguration.create()构建Configuration配置了hbase.zookeeper.quorum、hbase.zookeeper.property.clientPort、hbase.rootdir、hbase.client.retries.number、hbase.rpc.timeout等关键参数然后HbaseTemplate.newInstance(configuration)创建客户端。扫描封装HbaseTemplate.java 提供scan(String tableName, Scan scan)直接透传Scan含过滤器page(...)/scroll(...)系列方法则基于SingleFamilyScan/MultiFamilyScan完成分页与滚动查询其中分页内部正是lastRow不重复策略的 2.x 实现。测试验证HbaseTemplateScanTest.java 覆盖了单列族/多列族分页查询、实体分页查询、滚动查询与滚动删除等场景是理解过滤器 分页组合行为的可运行范例。基础前置若想回顾get/scan/ResultScanner/ 缓存与批量等基础概念可阅读同系列文档 HBase Java API 基础特性其中给出了getScanner(String tableName, FilterList filterList)的完整示例。参考资料HBase Java API 基础特性get/scan/ResultScanner等基础 APIHBase 教程目录HBase 系列全部文档索引HBase 快速入门环境搭建与基础操作扩展阅读Lars George 所著《HBase 权威指南》中关于过滤器Filters的章节赞分享文档教程数据库【免费下载链接】db-tutorial 后端程序员应该掌握的主流数据库知识项目地址https://gitcode.com/gh_mirrors/db/db-tutorial点击查看免费下载相关推荐Apache Beam Java 元素级变换 Filter 完全指南谓词过滤与不等式过滤的源码级解析Apache Beam Java 元素级变换 Filter 完全指南谓词过滤与不等式过滤的源码级解析 Filter 是 Apache Beam Java SDApache Beam Java SDK Filter 转换实战用 Filter.by 谓词过滤 PCollection 元素Apache Beam Java SDK Filter 转换实战用 Filter.by 谓词过滤 PCollection 元素 Apache Beam 的 J批处理流处理大数据ExternalDNS 域名过滤器Domain Filter完全指南--domain-filter 与正则过滤的实战与原理ExternalDNS 域名过滤器Domain Filter完全指南 domain filter 与正则过滤的实战与原理 本文围绕 ExternalDNS云原生上一篇从源码到实践深入理解Equivalent-Exchange-3的能量值注册表设计下一篇OpenArk实战指南Windows系统安全分析的三大核心场景深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考