未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用范围与待维护事项
本文适用于已有 HBase 数据模型、Java API、并发编程及集群运维基础的读者,示例尚未在明确的 HBase/JDK 版本上验证。10x-100x 等收益数字仅为原稿未核实示例。自定义比较器与 A/B 过滤器缺少服务端序列化、部署实现;数值编码、缺列处理、业务时间与 Cell 时间戳混用,以及分批结果重复计行仍需修复。分页须客户端限量;修改列族缓存配置应先在测试集群评估,不能直接用于生产。
warning 关联材料边界
以下原引用目标缺失或无法唯一确认,已取消跳转并保留提及:01-HBase概述与核心概念;05-Java API开发实战。相关素材/延伸说明暂不可用,不能据此声称已经提供配套材料。
HBase 过滤器与扫描优化
HBase 提供了丰富的服务端过滤器体系,使查询条件在 RegionServer 侧执行,大幅减少网络传输量。与全表扫描或客户端过滤相比,合理使用服务端过滤器可能减少传输与处理开销;原稿所述“查询延迟降低一个数量级”为未核实的性能示例,实际收益需结合扫描范围、数据分布和集群配置测试。本文系统介绍过滤器继承体系、各类过滤器的使用场景与性能特征,以及 Scan 参数调优方法。
note 阅读前置知识
本文假设读者已了解 HBase 基本数据模型(RowKey、列族、列限定符、版本)以及 HBase Java API 的基本用法。如需补充,请先阅读 HBase概述与核心概念(原引用“01-HBase概述与核心概念”未能确定有效目标,暂不可跳转) 和 Java API开发实战(原引用“05-Java API开发实战”未能确定有效目标,暂不可跳转)。
核心概念:服务端过滤 vs 客户端过滤
HBase 过滤器的核心价值在于将计算下推到数据所在位置。理解这一点,才能理解为何过滤器的选择对性能至关重要。
| 维度 | 服务端过滤(Filter) | 客户端过滤(应用层逻辑) |
|---|---|---|
| 执行位置 | RegionServer,数据就近处理 | 客户端 JVM,数据已传输 |
| 网络开销 | 只传输符合条件的数据 | 传输全量数据再过滤 |
| CPU 消耗 | 分布在各 RegionServer | 集中在客户端 |
| 适用场景 | 过滤率高(大量数据被过滤掉) | 过滤率低(大多数数据需要返回) |
| 性能比较边界 | 原稿的 10x-100x 为未核实示例,不是通用收益;需进行同条件基准测试 | 数据量小时代码可能更简单,但仍需比较传输与处理开销 |
warning 服务端过滤并非万能
ValueFilter 和 SingleColumnValueFilter 需要访问 Cell 的值进行比较,是否解码取决于比较器,在宽表场景下可能有较大开销。能由 RowKey 表达条件时,应优先用 setStartRow/setStopRow 限定范围;PrefixFilter 可表达前缀条件,但并非所有查询都需要叠加,也不能保证性能最高。
FilterBase 继承体系
HBase 过滤器的抽象基类是 Filter,FilterBase 继承 Filter 并提供默认实现,常用于编写自定义过滤器。以下列出部分方法,其中 filterRowKey(byte[], int, int) 和 filterKeyValue(Cell) 属于旧版签名,目标版本可能推荐使用 Cell 参数的 filterRowKey 和 filterCell:
| 方法 | 作用 | 返回值含义 |
|---|---|---|
filterRowKey(byte[] buffer, int offset, int length) |
判断是否跳过整行 | true = 跳过该行 |
filterKeyValue(Cell v) |
判断是否跳过单个 Cell | ReturnCode 枚举 |
filterRow() |
行内所有 Cell 处理完后,判断是否丢弃整行 | true = 丢弃 |
hasFilterRow() |
声明是否使用 filterRow() |
影响执行路径 |
filterAllRemaining() |
是否终止整个 Scan | true = 提前终止 |
ReturnCode 枚举的各个值:
| ReturnCode | 含义 |
|---|---|
INCLUDE |
包含该 Cell |
INCLUDE_AND_NEXT_COL |
包含该 Cell,跳转到下一列 |
SKIP |
跳过该 Cell |
NEXT_COL |
跳过当前列的所有版本,转到下一列 |
NEXT_ROW |
跳过当前行剩余所有 Cell |
SEEK_NEXT_USING_HINT |
根据 getNextCellHint() 提示跳转 |
🎯 过滤器体系全景图
📊 HBase过滤器架构
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
A[HBase过滤器体系]
A --> B[🔑 行级过滤器]
A --> C[📁 列族过滤器]
A --> D[📄 列过滤器]
A --> E[💎 值过滤器]
A --> F[⚡ 特殊过滤器]
A --> G[🔗 复合过滤器]
B --> B1[PrefixFilter<br/>前缀匹配]
B --> B2[KeyOnlyFilter<br/>只返回键]
B --> B3[FirstKeyOnlyFilter<br/>每行首键]
B --> B4[InclusiveStopFilter<br/>包含停止键]
C --> C1[FamilyFilter<br/>列族筛选]
D --> C2[MultipleColumnPrefixFilter<br/>多列限定符前缀]
D --> D1[QualifierFilter<br/>列限定符筛选]
D --> D2[ColumnPrefixFilter<br/>列前缀匹配]
D --> D3[ColumnRangeFilter<br/>列范围筛选]
E --> E1[ValueFilter<br/>值匹配]
E --> E2[SingleColumnValueFilter<br/>单列值筛选]
E --> E3[BinaryComparator<br/>比较器,非过滤器]
E --> E4[RegexStringComparator<br/>比较器,非过滤器]
F --> F1[PageFilter<br/>分页过滤]
F --> F2[RandomRowFilter<br/>随机采样]
F --> F3[TimestampsFilter<br/>时间戳筛选]
F --> F4[ColumnCountGetFilter<br/>列数限制]
G --> G1[FilterList<br/>过滤器列表]
G --> G2[WhileMatchFilter<br/>匹配直到]
G --> G3[SkipFilter<br/>跳过匹配行]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#fff3e0
style D fill:#e8f5e8
style E fill:#fce4ec
style F fill:#f9fbe7
style G fill:#ede7f6
🎯 过滤器性能特征
| 过滤器类型 | 执行位置 | 性能影响 | 使用场景 | 推荐程度 |
|---|---|---|---|---|
| 行级过滤器 | RegionServer | ⭐⭐⭐⭐⭐ | 大范围筛选 | 优先使用 |
| 列族过滤器 | RegionServer | ⭐⭐⭐⭐ | 数据类型筛选 | 推荐使用 |
| 列过滤器 | RegionServer | ⭐⭐⭐ | 字段级筛选 | 合理使用 |
| 值过滤器 | RegionServer | ⭐⭐ | 内容筛选 | 谨慎使用 |
| 复合过滤器 | RegionServer | ⭐⭐⭐ | 复杂条件 | 按需使用 |
🔑 行级过滤器详解
🎯 RowKey前缀过滤器
📋 PrefixFilter应用
// 1. 基础前缀过滤
// 查询所有以"user_2024"开头的用户数据
Filter prefixFilter = new PrefixFilter(Bytes.toBytes("user_2024"));
Scan scan = new Scan();
scan.setFilter(prefixFilter);
// Java代码示例
public class PrefixFilterExample {
public void queryUsersByPrefix(Table table, String prefix) throws IOException {
// 创建前缀过滤器
Filter filter = new PrefixFilter(Bytes.toBytes(prefix));
Scan scan = new Scan();
scan.setFilter(filter);
scan.setCaching(100); // 设置缓存大小
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
System.out.println("找到用户: " + rowKey);
// 获取用户基本信息
Cell nameCell = result.getColumnLatestCell(
Bytes.toBytes("basic"),
Bytes.toBytes("name")
);
if (nameCell != null) {
String name = Bytes.toString(CellUtil.cloneValue(nameCell));
System.out.println(" 姓名: " + name);
}
}
}
}
// 高级前缀查询:按地区和时间前缀
public void queryOrdersByRegionAndDate(Table table, String region, String date)
throws IOException {
// RowKey格式:region_date_orderId
String prefix = region + "_" + date;
Filter filter = new PrefixFilter(Bytes.toBytes(prefix));
Scan scan = new Scan();
scan.setFilter(filter);
scan.addFamily(Bytes.toBytes("order_info")); // 只查询订单信息列族
int count = 0;
double totalAmount = 0.0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
count++;
// 统计订单金额
Cell amountCell = result.getColumnLatestCell(
Bytes.toBytes("order_info"),
Bytes.toBytes("amount")
);
if (amountCell != null) {
double amount = Bytes.toDouble(CellUtil.cloneValue(amountCell));
totalAmount += amount;
}
}
}
System.out.println(String.format(
"%s地区%s的订单统计:总数%d,总金额%.2f",
region, date, count, totalAmount
));
}
}
🎨 高级前缀组合技巧
// 多级前缀查询策略
public class AdvancedPrefixQueries {
// 1. 分层前缀查询
public void hierarchicalPrefixQuery(Table table) throws IOException {
// 查询结构:department_team_employee_
// 第一层:查询所有部门
List<String> departments = queryPrefixes(table, "", 1);
for (String dept : departments) {
System.out.println("部门: " + dept);
// 第二层:查询部门下的团队
List<String> teams = queryPrefixes(table, dept + "_", 2);
for (String team : teams) {
System.out.println(" 团队: " + team);
// 第三层:查询团队下的员工
List<String> employees = queryPrefixes(table, dept + "_" + team + "_", 3);
System.out.println(" 员工数: " + employees.size());
}
}
}
private List<String> queryPrefixes(Table table, String prefix, int level)
throws IOException {
Set<String> prefixes = new HashSet<>();
Filter filter = new PrefixFilter(Bytes.toBytes(prefix));
Scan scan = new Scan();
scan.setFilter(filter);
scan.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
String[] parts = rowKey.split("_");
if (parts.length > level) {
// 提取到指定层级的前缀
String levelPrefix = String.join("_",
Arrays.copyOfRange(parts, 0, level + 1));
prefixes.add(parts[level]);
}
}
}
return new ArrayList<>(prefixes);
}
// 2. 时间窗口前缀查询
public void timeWindowPrefixQuery(Table table, String startDate, String endDate)
throws IOException {
// RowKey格式:date_deviceId_timestamp
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
try {
Date start = sdf.parse(startDate);
Date end = sdf.parse(endDate);
Calendar cal = Calendar.getInstance();
cal.setTime(start);
while (!cal.getTime().after(end)) {
String dateStr = sdf.format(cal.getTime());
// 查询当天数据
Filter filter = new PrefixFilter(Bytes.toBytes(dateStr));
Scan scan = new Scan();
scan.setFilter(filter);
int dailyCount = 0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
dailyCount++;
}
}
System.out.println(dateStr + ": " + dailyCount + " 条记录");
cal.add(Calendar.DAY_OF_MONTH, 1);
}
} catch (ParseException e) {
throw new IOException("日期格式错误", e);
}
}
}
🔍 KeyOnly过滤器
// KeyOnlyFilter:只返回键,不返回值
// 适用场景:统计、索引构建、键存在性检查
public class KeyOnlyFilterExample {
// 1. 快速统计表中行数
public long countRows(Table table) throws IOException {
Filter filter = new KeyOnlyFilter();
Scan scan = new Scan();
scan.setFilter(filter);
scan.setCaching(1000); // 大缓存提升统计性能
long count = 0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
count++;
}
}
return count;
}
// 2. 构建RowKey索引
public Set<String> buildRowKeyIndex(Table table, String prefix)
throws IOException {
Set<String> rowKeys = new TreeSet<>();
// 组合前缀过滤器和KeyOnly过滤器
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
filterList.addFilter(new PrefixFilter(Bytes.toBytes(prefix)));
filterList.addFilter(new KeyOnlyFilter());
Scan scan = new Scan();
scan.setFilter(filterList);
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
rowKeys.add(Bytes.toString(result.getRow()));
}
}
return rowKeys;
}
// 3. 检查数据完整性
public void checkDataIntegrity(Table sourceTable, Table targetTable)
throws IOException {
System.out.println("检查数据完整性...");
// 获取源表所有RowKey
Set<String> sourceKeys = buildRowKeyIndex(sourceTable, "");
System.out.println("源表记录数: " + sourceKeys.size());
// 获取目标表所有RowKey
Set<String> targetKeys = buildRowKeyIndex(targetTable, "");
System.out.println("目标表记录数: " + targetKeys.size());
// 找出差异
Set<String> missingInTarget = new HashSet<>(sourceKeys);
missingInTarget.removeAll(targetKeys);
Set<String> extraInTarget = new HashSet<>(targetKeys);
extraInTarget.removeAll(sourceKeys);
if (!missingInTarget.isEmpty()) {
System.out.println("目标表缺失记录数: " + missingInTarget.size());
missingInTarget.stream().limit(10).forEach(key ->
System.out.println(" 缺失: " + key));
}
if (!extraInTarget.isEmpty()) {
System.out.println("目标表多余记录数: " + extraInTarget.size());
extraInTarget.stream().limit(10).forEach(key ->
System.out.println(" 多余: " + key));
}
if (missingInTarget.isEmpty() && extraInTarget.isEmpty()) {
System.out.println("✓ 数据完整性检查通过");
}
}
}
📄 列级过滤器
🏷️ 列族过滤器
// FamilyFilter:按列族筛选数据
public class FamilyFilterExample {
// 1. 只查询特定列族
public void querySpecificFamily(Table table, String familyName)
throws IOException {
// 创建列族过滤器
Filter filter = new FamilyFilter(
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes(familyName))
);
Scan scan = new Scan();
scan.setFilter(filter);
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
System.out.println("行键: " + rowKey);
// 遍历该行的所有列
for (Cell cell : result.rawCells()) {
String family = Bytes.toString(CellUtil.cloneFamily(cell));
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.println(String.format(" %s:%s = %s",
family, qualifier, value));
}
}
}
}
// 2. 排除特定列族
public void excludeFamily(Table table, String excludeFamilyName)
throws IOException {
Filter filter = new FamilyFilter(
CompareFilter.CompareOp.NOT_EQUAL,
new BinaryComparator(Bytes.toBytes(excludeFamilyName))
);
Scan scan = new Scan();
scan.setFilter(filter);
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
System.out.println("行键: " + Bytes.toString(result.getRow()));
// 统计各列族的列数
Map<String, Integer> familyColumnCount = new HashMap<>();
for (Cell cell : result.rawCells()) {
String family = Bytes.toString(CellUtil.cloneFamily(cell));
familyColumnCount.merge(family, 1, Integer::sum);
}
familyColumnCount.forEach((family, count) ->
System.out.println(" " + family + ": " + count + " 列"));
}
}
}
// 3. 多列限定符前缀过滤(不按列族名称匹配)
public void multipleColumnPrefixFilter(Table table, String... prefixes)
throws IOException {
byte[][] prefixBytes = Arrays.stream(prefixes)
.map(Bytes::toBytes)
.toArray(byte[][]::new);
Filter filter = new MultipleColumnPrefixFilter(prefixBytes);
Scan scan = new Scan();
scan.setFilter(filter);
System.out.println("查询列前缀: " + Arrays.toString(prefixes));
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
System.out.println("行键: " + rowKey);
for (Cell cell : result.rawCells()) {
String family = Bytes.toString(CellUtil.cloneFamily(cell));
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.println(String.format(" %s:%s = %s",
family, qualifier, value));
}
}
}
}
}
📊 列限定符过滤器
// QualifierFilter:按列限定符筛选
public class QualifierFilterExample {
// 1. 精确匹配列限定符
public void exactQualifierMatch(Table table, String qualifier)
throws IOException {
Filter filter = new QualifierFilter(
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes(qualifier))
);
Scan scan = new Scan();
scan.setFilter(filter);
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
System.out.println("行键: " + rowKey);
for (Cell cell : result.rawCells()) {
String family = Bytes.toString(CellUtil.cloneFamily(cell));
String qual = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.println(String.format(" %s:%s = %s",
family, qual, value));
}
}
}
}
// 2. 正则表达式匹配列限定符
public void regexQualifierMatch(Table table, String pattern)
throws IOException {
Filter filter = new QualifierFilter(
CompareFilter.CompareOp.EQUAL,
new RegexStringComparator(pattern)
);
Scan scan = new Scan();
scan.setFilter(filter);
System.out.println("列限定符正则模式: " + pattern);
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
System.out.println("行键: " + rowKey);
for (Cell cell : result.rawCells()) {
String family = Bytes.toString(CellUtil.cloneFamily(cell));
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
// 验证正则匹配
if (Pattern.matches(pattern, qualifier)) {
System.out.println(String.format(" ✓ %s:%s = %s",
family, qualifier, value));
}
}
}
}
}
// 3. 列范围过滤器
public void columnRangeFilter(Table table, String minColumn, String maxColumn)
throws IOException {
Filter filter = new ColumnRangeFilter(
Bytes.toBytes(minColumn), true, // 包含最小值
Bytes.toBytes(maxColumn), false // 不包含最大值
);
Scan scan = new Scan();
scan.setFilter(filter);
System.out.println(String.format("列范围: [%s, %s)", minColumn, maxColumn));
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
System.out.println("行键: " + rowKey);
for (Cell cell : result.rawCells()) {
String family = Bytes.toString(CellUtil.cloneFamily(cell));
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.println(String.format(" %s:%s = %s",
family, qualifier, value));
}
}
}
}
// 4. 动态列分析
public void analyzeColumnDistribution(Table table, String familyName)
throws IOException {
// 使用KeyOnly + Family过滤器分析列分布
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
filterList.addFilter(new KeyOnlyFilter());
filterList.addFilter(new FamilyFilter(
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes(familyName))
));
Scan scan = new Scan();
scan.setFilter(filterList);
Map<String, Integer> columnStats = new HashMap<>();
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
for (Cell cell : result.rawCells()) {
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
columnStats.merge(qualifier, 1, Integer::sum);
}
}
}
System.out.println("列族 " + familyName + " 的列分布统计:");
columnStats.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.forEach(entry ->
System.out.println(String.format(" %s: %d 次出现",
entry.getKey(), entry.getValue())));
}
}
💎 值过滤器
🎯 单列值过滤器
// SingleColumnValueFilter:基于特定列的值进行过滤
public class SingleColumnValueFilterExample {
// 1. 数值范围过滤
public void numericRangeFilter(Table table, double minValue, double maxValue)
throws IOException {
// 查询价格在指定范围内的商品
Filter minFilter = new SingleColumnValueFilter(
Bytes.toBytes("product_info"), // 列族
Bytes.toBytes("price"), // 列限定符
CompareFilter.CompareOp.GREATER_OR_EQUAL,
new BinaryComparator(Bytes.toBytes(minValue))
);
Filter maxFilter = new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("price"),
CompareFilter.CompareOp.LESS_OR_EQUAL,
new BinaryComparator(Bytes.toBytes(maxValue))
);
// 组合过滤器
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
filterList.addFilter(minFilter);
filterList.addFilter(maxFilter);
Scan scan = new Scan();
scan.setFilter(filterList);
System.out.println(String.format("价格范围: %.2f - %.2f", minValue, maxValue));
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
Cell priceCell = result.getColumnLatestCell(
Bytes.toBytes("product_info"),
Bytes.toBytes("price")
);
if (priceCell != null) {
double price = Bytes.toDouble(CellUtil.cloneValue(priceCell));
System.out.println(String.format("商品: %s, 价格: %.2f",
rowKey, price));
}
}
}
}
// 2. 字符串模式匹配
public void stringPatternFilter(Table table, String category)
throws IOException {
Filter filter = new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("category"),
CompareFilter.CompareOp.EQUAL,
new SubstringComparator(category) // 子串匹配
);
// 设置过滤器属性
((SingleColumnValueFilter) filter).setFilterIfMissing(true); // 过滤掉没有该列的行
((SingleColumnValueFilter) filter).setLatestVersionOnly(true); // 只检查最新版本
Scan scan = new Scan();
scan.setFilter(filter);
System.out.println("查询分类包含: " + category);
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
Cell categoryCell = result.getColumnLatestCell(
Bytes.toBytes("product_info"),
Bytes.toBytes("category")
);
if (categoryCell != null) {
String cat = Bytes.toString(CellUtil.cloneValue(categoryCell));
System.out.println(String.format("商品: %s, 分类: %s", rowKey, cat));
}
}
}
}
// 3. 时间戳过滤
public void timestampFilter(Table table, long startTime, long endTime)
throws IOException {
Filter filter = new SingleColumnValueFilter(
Bytes.toBytes("order_info"),
Bytes.toBytes("created_time"),
CompareFilter.CompareOp.GREATER_OR_EQUAL,
new LongComparator(startTime)
);
Scan scan = new Scan();
scan.setFilter(filter);
scan.setTimeRange(startTime, endTime); // 结合时间范围扫描
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(String.format("时间范围: %s - %s",
sdf.format(new Date(startTime)),
sdf.format(new Date(endTime))
));
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
Cell timeCell = result.getColumnLatestCell(
Bytes.toBytes("order_info"),
Bytes.toBytes("created_time")
);
if (timeCell != null) {
long timestamp = Bytes.toLong(CellUtil.cloneValue(timeCell));
String timeStr = sdf.format(new Date(timestamp));
System.out.println(String.format("订单: %s, 时间: %s",
rowKey, timeStr));
}
}
}
}
// 4. 复杂业务条件过滤
public void complexBusinessFilter(Table table) throws IOException {
// 查询VIP用户的高价值订单
Filter vipFilter = new SingleColumnValueFilter(
Bytes.toBytes("customer_info"),
Bytes.toBytes("level"),
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes("VIP"))
);
Filter highValueFilter = new SingleColumnValueFilter(
Bytes.toBytes("order_info"),
Bytes.toBytes("amount"),
CompareFilter.CompareOp.GREATER,
new BinaryComparator(Bytes.toBytes(1000.0))
);
Filter statusFilter = new SingleColumnValueFilter(
Bytes.toBytes("order_info"),
Bytes.toBytes("status"),
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes("completed"))
);
// 组合所有条件
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
filterList.addFilter(vipFilter);
filterList.addFilter(highValueFilter);
filterList.addFilter(statusFilter);
Scan scan = new Scan();
scan.setFilter(filterList);
double totalAmount = 0;
int orderCount = 0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
Cell amountCell = result.getColumnLatestCell(
Bytes.toBytes("order_info"),
Bytes.toBytes("amount")
);
Cell customerCell = result.getColumnLatestCell(
Bytes.toBytes("customer_info"),
Bytes.toBytes("name")
);
if (amountCell != null && customerCell != null) {
double amount = Bytes.toDouble(CellUtil.cloneValue(amountCell));
String customerName = Bytes.toString(CellUtil.cloneValue(customerCell));
totalAmount += amount;
orderCount++;
System.out.println(String.format(
"VIP订单: %s, 客户: %s, 金额: %.2f",
rowKey, customerName, amount));
}
}
}
System.out.println(String.format(
"VIP高价值订单统计: 总数=%d, 总金额=%.2f, 平均金额=%.2f",
orderCount, totalAmount,
orderCount > 0 ? totalAmount / orderCount : 0));
}
}
🎨 值比较器详解
// 各种比较器的应用场景
public class ComparatorExamples {
// 1. 二进制比较器 - 精确匹配
public void binaryComparatorExample(Table table) throws IOException {
Filter filter = new ValueFilter(
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes("active"))
);
Scan scan = new Scan();
scan.setFilter(filter);
System.out.println("精确匹配值为 'active' 的记录:");
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
System.out.println("行键: " + Bytes.toString(result.getRow()));
}
}
}
// 2. 正则表达式比较器 - 模式匹配
public void regexComparatorExample(Table table) throws IOException {
// 匹配邮箱格式
String emailPattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
Filter filter = new SingleColumnValueFilter(
Bytes.toBytes("contact_info"),
Bytes.toBytes("email"),
CompareFilter.CompareOp.EQUAL,
new RegexStringComparator(emailPattern)
);
Scan scan = new Scan();
scan.setFilter(filter);
System.out.println("有效邮箱地址的用户:");
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
Cell emailCell = result.getColumnLatestCell(
Bytes.toBytes("contact_info"),
Bytes.toBytes("email")
);
if (emailCell != null) {
String email = Bytes.toString(CellUtil.cloneValue(emailCell));
System.out.println(String.format("用户: %s, 邮箱: %s",
rowKey, email));
}
}
}
}
// 3. 子串比较器 - 包含匹配
public void substringComparatorExample(Table table) throws IOException {
Filter filter = new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("description"),
CompareFilter.CompareOp.EQUAL,
new SubstringComparator("智能") // 包含"智能"的商品
);
Scan scan = new Scan();
scan.setFilter(filter);
System.out.println("包含'智能'关键词的商品:");
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
Cell descCell = result.getColumnLatestCell(
Bytes.toBytes("product_info"),
Bytes.toBytes("description")
);
if (descCell != null) {
String description = Bytes.toString(CellUtil.cloneValue(descCell));
System.out.println(String.format("商品: %s", rowKey));
System.out.println(String.format(" 描述: %s", description));
}
}
}
}
// 4. 数值比较器 - 数值比较
public void numericComparatorExample(Table table) throws IOException {
// 查询库存少于10的商品
Filter filter = new SingleColumnValueFilter(
Bytes.toBytes("inventory"),
Bytes.toBytes("stock_quantity"),
CompareFilter.CompareOp.LESS,
new LongComparator(10L)
);
Scan scan = new Scan();
scan.setFilter(filter);
System.out.println("库存不足的商品 (< 10):");
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
Cell stockCell = result.getColumnLatestCell(
Bytes.toBytes("inventory"),
Bytes.toBytes("stock_quantity")
);
if (stockCell != null) {
long stock = Bytes.toLong(CellUtil.cloneValue(stockCell));
System.out.println(String.format("商品: %s, 库存: %d",
rowKey, stock));
}
}
}
}
// 5. 自定义比较器
public static class DateRangeComparator extends ByteArrayComparable {
private final long startDate;
private final long endDate;
public DateRangeComparator(long startDate, long endDate) {
super(Bytes.toBytes("date_range"));
this.startDate = startDate;
this.endDate = endDate;
}
@Override
public int compareTo(byte[] value, int offset, int length) {
try {
// 假设value是日期字符串格式 "yyyy-MM-dd"
String dateStr = Bytes.toString(value, offset, length);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
long date = sdf.parse(dateStr).getTime();
if (date >= startDate && date <= endDate) {
return 0; // 在范围内
} else if (date < startDate) {
return -1;
} else {
return 1;
}
} catch (Exception e) {
return -1;
}
}
}
public void customComparatorExample(Table table, String startDate, String endDate)
throws IOException {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
long start = sdf.parse(startDate).getTime();
long end = sdf.parse(endDate).getTime();
Filter filter = new SingleColumnValueFilter(
Bytes.toBytes("order_info"),
Bytes.toBytes("order_date"),
CompareFilter.CompareOp.EQUAL,
new DateRangeComparator(start, end)
);
Scan scan = new Scan();
scan.setFilter(filter);
System.out.println(String.format("日期范围: %s - %s", startDate, endDate));
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
Cell dateCell = result.getColumnLatestCell(
Bytes.toBytes("order_info"),
Bytes.toBytes("order_date")
);
if (dateCell != null) {
String orderDate = Bytes.toString(CellUtil.cloneValue(dateCell));
System.out.println(String.format("订单: %s, 日期: %s",
rowKey, orderDate));
}
}
}
} catch (ParseException e) {
throw new IOException("日期格式错误", e);
}
}
}
⚡ 特殊过滤器
📄 分页过滤器
// PageFilter:实现分页查询
public class PageFilterExample {
// 1. 基础分页实现
public void basicPagination(Table table, int pageSize, int pageNum)
throws IOException {
Filter filter = new PageFilter(pageSize);
Scan scan = new Scan();
scan.setFilter(filter);
scan.setCaching(pageSize);
// 如果不是第一页,需要设置起始行
if (pageNum > 1) {
// 这里需要记住上一页的最后一个RowKey
// 实际应用中通常存储在客户端或缓存中
String lastRowKey = getLastRowKeyFromPreviousPage(pageNum - 1);
if (lastRowKey != null) {
scan.setStartRow(Bytes.toBytes(lastRowKey + "\0")); // 排除上一页最后一行
}
}
System.out.println(String.format("第 %d 页 (每页 %d 条):", pageNum, pageSize));
int count = 0;
String lastRowKey = null;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
count++;
lastRowKey = Bytes.toString(result.getRow());
System.out.println(String.format(" %d. %s", count, lastRowKey));
// 显示部分列信息
Cell nameCell = result.getColumnLatestCell(
Bytes.toBytes("basic"),
Bytes.toBytes("name")
);
if (nameCell != null) {
String name = Bytes.toString(CellUtil.cloneValue(nameCell));
System.out.println(String.format(" 姓名: %s", name));
}
}
}
System.out.println(String.format("本页共 %d 条记录", count));
// 保存最后一个RowKey供下一页使用
saveLastRowKeyForPage(pageNum, lastRowKey);
}
// 2. 游标分页实现(推荐)
public PaginationResult cursorBasedPagination(Table table, String cursor, int pageSize)
throws IOException {
Filter filter = new PageFilter(pageSize + 1); // 多查一条判断是否有下一页
Scan scan = new Scan();
scan.setFilter(filter);
scan.setCaching(pageSize + 1);
if (cursor != null && !cursor.isEmpty()) {
scan.setStartRow(Bytes.toBytes(cursor + "\0"));
}
List<String> results = new ArrayList<>();
String nextCursor = null;
boolean hasNextPage = false;
try (ResultScanner scanner = table.getScanner(scan)) {
int count = 0;
for (Result result : scanner) {
count++;
String rowKey = Bytes.toString(result.getRow());
if (count <= pageSize) {
results.add(rowKey);
nextCursor = rowKey;
} else {
// 第pageSize+1条记录,说明有下一页
hasNextPage = true;
break;
}
}
}
return new PaginationResult(results, nextCursor, hasNextPage);
}
// 3. 分页结果类
public static class PaginationResult {
private final List<String> data;
private final String nextCursor;
private final boolean hasNextPage;
public PaginationResult(List<String> data, String nextCursor, boolean hasNextPage) {
this.data = data;
this.nextCursor = nextCursor;
this.hasNextPage = hasNextPage;
}
// getters...
public List<String> getData() { return data; }
public String getNextCursor() { return nextCursor; }
public boolean hasNextPage() { return hasNextPage; }
}
// 4. 复杂分页:带过滤条件的分页
public PaginationResult filteredPagination(Table table, String cursor, int pageSize,
String category) throws IOException {
// 创建复合过滤器
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
// 分页过滤器
filterList.addFilter(new PageFilter(pageSize + 1));
// 业务条件过滤器
if (category != null && !category.isEmpty()) {
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("category"),
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes(category))
));
}
Scan scan = new Scan();
scan.setFilter(filterList);
scan.setCaching(pageSize + 1);
if (cursor != null && !cursor.isEmpty()) {
scan.setStartRow(Bytes.toBytes(cursor + "\0"));
}
List<String> results = new ArrayList<>();
String nextCursor = null;
boolean hasNextPage = false;
try (ResultScanner scanner = table.getScanner(scan)) {
int count = 0;
for (Result result : scanner) {
count++;
String rowKey = Bytes.toString(result.getRow());
if (count <= pageSize) {
results.add(rowKey);
nextCursor = rowKey;
} else {
hasNextPage = true;
break;
}
}
}
return new PaginationResult(results, nextCursor, hasNextPage);
}
// 辅助方法(实际应用中应该使用持久化存储)
private static final Map<Integer, String> pageLastKeys = new HashMap<>();
private String getLastRowKeyFromPreviousPage(int pageNum) {
return pageLastKeys.get(pageNum);
}
private void saveLastRowKeyForPage(int pageNum, String lastRowKey) {
pageLastKeys.put(pageNum, lastRowKey);
}
}
🎲 随机采样过滤器
// RandomRowFilter:随机采样数据
public class RandomSamplingExample {
// 1. 随机采样分析
public void randomSampling(Table table, float sampleRate) throws IOException {
Filter filter = new RandomRowFilter(sampleRate);
Scan scan = new Scan();
scan.setFilter(filter);
scan.setCaching(1000);
System.out.println(String.format("随机采样率: %.2f%%", sampleRate * 100));
int totalSamples = 0;
Map<String, Integer> categoryStats = new HashMap<>();
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
totalSamples++;
Cell categoryCell = result.getColumnLatestCell(
Bytes.toBytes("product_info"),
Bytes.toBytes("category")
);
if (categoryCell != null) {
String category = Bytes.toString(CellUtil.cloneValue(categoryCell));
categoryStats.merge(category, 1, Integer::sum);
}
}
}
System.out.println(String.format("采样总数: %d", totalSamples));
final int sampleCount = totalSamples;
System.out.println("分类分布:");
categoryStats.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.forEach(entry -> {
double percentage = sampleCount > 0 ? (double) entry.getValue() / sampleCount * 100 : 0;
System.out.println(String.format(" %s: %d (%.1f%%)",
entry.getKey(), entry.getValue(), percentage));
});
}
// 2. 分层随机采样
public void stratifiedRandomSampling(Table table, Map<String, Float> stratumSampleRates)
throws IOException {
System.out.println("分层随机采样:");
for (Map.Entry<String, Float> entry : stratumSampleRates.entrySet()) {
String category = entry.getKey();
float sampleRate = entry.getValue();
System.out.println(String.format("\n分层: %s (采样率: %.2f%%)",
category, sampleRate * 100));
// 创建复合过滤器:分类过滤 + 随机采样
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("category"),
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes(category))
));
filterList.addFilter(new RandomRowFilter(sampleRate));
Scan scan = new Scan();
scan.setFilter(filterList);
scan.setCaching(500);
int count = 0;
double totalPrice = 0.0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
count++;
Cell priceCell = result.getColumnLatestCell(
Bytes.toBytes("product_info"),
Bytes.toBytes("price")
);
if (priceCell != null) {
double price = Bytes.toDouble(CellUtil.cloneValue(priceCell));
totalPrice += price;
}
}
}
System.out.println(String.format(" 采样数量: %d", count));
if (count > 0) {
System.out.println(String.format(" 平均价格: %.2f", totalPrice / count));
}
}
}
// 3. 时间窗口随机采样
public void timeWindowRandomSampling(Table table, long startTime, long endTime,
float sampleRate) throws IOException {
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
// 时间范围过滤
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("order_info"),
Bytes.toBytes("created_time"),
CompareFilter.CompareOp.GREATER_OR_EQUAL,
new LongComparator(startTime)
));
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("order_info"),
Bytes.toBytes("created_time"),
CompareFilter.CompareOp.LESS_OR_EQUAL,
new LongComparator(endTime)
));
// 随机采样
filterList.addFilter(new RandomRowFilter(sampleRate));
Scan scan = new Scan();
scan.setFilter(filterList);
scan.setTimeRange(startTime, endTime);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(String.format("时间窗口随机采样: %s - %s (采样率: %.2f%%)",
sdf.format(new Date(startTime)),
sdf.format(new Date(endTime)),
sampleRate * 100));
int orderCount = 0;
double totalAmount = 0.0;
Map<String, Integer> hourlyStats = new TreeMap<>();
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
orderCount++;
Cell timeCell = result.getColumnLatestCell(
Bytes.toBytes("order_info"),
Bytes.toBytes("created_time")
);
Cell amountCell = result.getColumnLatestCell(
Bytes.toBytes("order_info"),
Bytes.toBytes("amount")
);
if (timeCell != null && amountCell != null) {
long timestamp = Bytes.toLong(CellUtil.cloneValue(timeCell));
double amount = Bytes.toDouble(CellUtil.cloneValue(amountCell));
totalAmount += amount;
// 按小时统计
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(timestamp);
String hourKey = String.format("%02d:00", cal.get(Calendar.HOUR_OF_DAY));
hourlyStats.merge(hourKey, 1, Integer::sum);
}
}
}
System.out.println(String.format("采样订单数: %d", orderCount));
System.out.println(String.format("总金额: %.2f", totalAmount));
if (orderCount > 0) {
System.out.println(String.format("平均订单金额: %.2f", totalAmount / orderCount));
}
System.out.println("小时分布:");
hourlyStats.forEach((hour, count) ->
System.out.println(String.format(" %s: %d 订单", hour, count)));
}
// 4. A/B测试样本选择
public void abTestSampling(Table table, String testGroup, float sampleRate)
throws IOException {
// 使用RowKey的哈希值进行确定性采样
Filter filter = new RandomRowFilter(sampleRate) {
@Override
public ReturnCode filterKeyValue(Cell cell) throws IOException {
// 自定义采样逻辑:基于RowKey哈希
byte[] rowKey = CellUtil.cloneRow(cell);
int hash = Arrays.hashCode(rowKey);
// 根据testGroup决定采样逻辑
boolean inTestGroup;
if ("A".equals(testGroup)) {
inTestGroup = (hash % 100) < (sampleRate * 100);
} else {
inTestGroup = (hash % 100) >= (sampleRate * 100);
}
return inTestGroup ? ReturnCode.INCLUDE : ReturnCode.SKIP;
}
};
Scan scan = new Scan();
scan.setFilter(filter);
System.out.println(String.format("A/B测试组 %s 采样 (采样率: %.2f%%)",
testGroup, sampleRate * 100));
Set<String> userIds = new HashSet<>();
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
// 提取用户ID(假设RowKey格式包含用户ID)
String userId = extractUserIdFromRowKey(rowKey);
if (userId != null) {
userIds.add(userId);
}
}
}
System.out.println(String.format("测试组 %s 用户数: %d", testGroup, userIds.size()));
// 输出部分用户ID作为示例
userIds.stream().limit(10).forEach(userId ->
System.out.println(" 用户: " + userId));
}
private String extractUserIdFromRowKey(String rowKey) {
// 根据实际RowKey格式提取用户ID
// 这里假设RowKey格式为 "user_12345_..."
String[] parts = rowKey.split("_");
return parts.length > 1 ? parts[1] : null;
}
}
🔗 复合过滤器设计
📊 FilterList高级应用
// FilterList:组合多个过滤器
public class ComplexFilterExample {
// 1. AND逻辑组合 - 所有条件必须满足
public void andFilterCombination(Table table) throws IOException {
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
// 条件1:价格大于100
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("price"),
CompareFilter.CompareOp.GREATER,
new BinaryComparator(Bytes.toBytes(100.0))
));
// 条件2:评分大于4.0
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("rating"),
CompareFilter.CompareOp.GREATER,
new BinaryComparator(Bytes.toBytes(4.0))
));
// 条件3:库存大于0
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("inventory"),
Bytes.toBytes("stock"),
CompareFilter.CompareOp.GREATER,
new BinaryComparator(Bytes.toBytes(0))
));
// 条件4:分类子串匹配
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("category"),
CompareFilter.CompareOp.EQUAL,
new SubstringComparator("电子")
));
Scan scan = new Scan();
scan.setFilter(filterList);
System.out.println("查询条件 (AND逻辑):");
System.out.println("- 价格 > 100");
System.out.println("- 评分 > 4.0");
System.out.println("- 库存 > 0");
System.out.println("- 分类包含'电子'");
int count = 0;
double totalPrice = 0.0;
double totalRating = 0.0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
count++;
String rowKey = Bytes.toString(result.getRow());
// 获取商品信息
Map<String, String> productInfo = extractProductInfo(result);
System.out.println(String.format("商品 %d: %s", count, rowKey));
productInfo.forEach((key, value) ->
System.out.println(String.format(" %s: %s", key, value)));
// 统计数据
try {
double price = Double.parseDouble(productInfo.get("价格"));
double rating = Double.parseDouble(productInfo.get("评分"));
totalPrice += price;
totalRating += rating;
} catch (NumberFormatException e) {
// 忽略格式错误
}
}
}
if (count > 0) {
System.out.println(String.format("\n统计结果: 符合条件商品 %d 个", count));
System.out.println(String.format("平均价格: %.2f", totalPrice / count));
System.out.println(String.format("平均评分: %.2f", totalRating / count));
} else {
System.out.println("没有找到符合条件的商品");
}
}
// 2. OR逻辑组合 - 任一条件满足即可
public void orFilterCombination(Table table) throws IOException {
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ONE);
// 条件1:VIP用户
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("customer_info"),
Bytes.toBytes("level"),
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes("VIP"))
));
// 条件2:高价值订单 (>1000)
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("order_info"),
Bytes.toBytes("amount"),
CompareFilter.CompareOp.GREATER,
new BinaryComparator(Bytes.toBytes(1000.0))
));
// 条件3:新用户(注册时间在最近30天)
long thirtyDaysAgo = System.currentTimeMillis() - (30L * 24 * 60 * 60 * 1000);
filterList.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("customer_info"),
Bytes.toBytes("registration_time"),
CompareFilter.CompareOp.GREATER,
new LongComparator(thirtyDaysAgo)
));
Scan scan = new Scan();
scan.setFilter(filterList);
System.out.println("查询条件 (OR逻辑):");
System.out.println("- VIP用户 OR");
System.out.println("- 高价值订单(>1000) OR");
System.out.println("- 新用户(最近30天)");
Map<String, Integer> categoryStats = new HashMap<>();
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
// 分析满足的条件类型
String matchedCondition = analyzeMatchedCondition(result, thirtyDaysAgo);
categoryStats.merge(matchedCondition, 1, Integer::sum);
System.out.println(String.format("记录: %s, 匹配条件: %s",
rowKey, matchedCondition));
}
}
System.out.println("\n条件匹配统计:");
categoryStats.forEach((condition, count) ->
System.out.println(String.format(" %s: %d 条", condition, count)));
}
// 3. 嵌套复合过滤器
public void nestedFilterCombination(Table table) throws IOException {
// 外层OR: (高价值用户) OR (促销商品)
FilterList outerOr = new FilterList(FilterList.Operator.MUST_PASS_ONE);
// 内层AND1: 高价值用户 = VIP用户 AND 历史消费>5000
FilterList highValueUser = new FilterList(FilterList.Operator.MUST_PASS_ALL);
highValueUser.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("customer_info"),
Bytes.toBytes("level"),
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes("VIP"))
));
highValueUser.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("customer_info"),
Bytes.toBytes("total_spent"),
CompareFilter.CompareOp.GREATER,
new BinaryComparator(Bytes.toBytes(5000.0))
));
// 内层AND2: 促销商品 = 有折扣 AND 库存充足
FilterList promotionProduct = new FilterList(FilterList.Operator.MUST_PASS_ALL);
promotionProduct.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("promotion"),
Bytes.toBytes("discount_rate"),
CompareFilter.CompareOp.GREATER,
new BinaryComparator(Bytes.toBytes(0.0))
));
promotionProduct.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("inventory"),
Bytes.toBytes("stock"),
CompareFilter.CompareOp.GREATER,
new BinaryComparator(Bytes.toBytes(10))
));
// 组合嵌套过滤器
outerOr.addFilter(highValueUser);
outerOr.addFilter(promotionProduct);
Scan scan = new Scan();
scan.setFilter(outerOr);
System.out.println("嵌套复合过滤器查询:");
System.out.println("(VIP用户 AND 历史消费>5000) OR (有折扣 AND 库存>10)");
int highValueCount = 0;
int promotionCount = 0;
int bothCount = 0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
boolean isHighValue = checkHighValueUser(result);
boolean isPromotion = checkPromotionProduct(result);
if (isHighValue && isPromotion) {
bothCount++;
System.out.println(String.format("✓✓ %s: 高价值用户+促销商品", rowKey));
} else if (isHighValue) {
highValueCount++;
System.out.println(String.format("✓ %s: 高价值用户", rowKey));
} else if (isPromotion) {
promotionCount++;
System.out.println(String.format(" ✓ %s: 促销商品", rowKey));
}
}
}
System.out.println(String.format("\n匹配统计:"));
System.out.println(String.format(" 高价值用户: %d", highValueCount));
System.out.println(String.format(" 促销商品: %d", promotionCount));
System.out.println(String.format(" 两者兼备: %d", bothCount));
System.out.println(String.format(" 总计: %d", highValueCount + promotionCount + bothCount));
}
// 4. 动态过滤器构建
public void dynamicFilterBuilder(Table table, Map<String, Object> conditions)
throws IOException {
FilterList dynamicFilter = new FilterList(FilterList.Operator.MUST_PASS_ALL);
System.out.println("动态构建过滤器:");
// 价格范围
if (conditions.containsKey("minPrice")) {
double minPrice = (Double) conditions.get("minPrice");
dynamicFilter.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("price"),
CompareFilter.CompareOp.GREATER_OR_EQUAL,
new BinaryComparator(Bytes.toBytes(minPrice))
));
System.out.println(String.format("- 最低价格: %.2f", minPrice));
}
if (conditions.containsKey("maxPrice")) {
double maxPrice = (Double) conditions.get("maxPrice");
dynamicFilter.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("price"),
CompareFilter.CompareOp.LESS_OR_EQUAL,
new BinaryComparator(Bytes.toBytes(maxPrice))
));
System.out.println(String.format("- 最高价格: %.2f", maxPrice));
}
// 分类过滤
if (conditions.containsKey("category")) {
String category = (String) conditions.get("category");
dynamicFilter.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("category"),
CompareFilter.CompareOp.EQUAL,
new SubstringComparator(category)
));
System.out.println(String.format("- 分类包含: %s", category));
}
// 评分过滤
if (conditions.containsKey("minRating")) {
double minRating = (Double) conditions.get("minRating");
dynamicFilter.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("product_info"),
Bytes.toBytes("rating"),
CompareFilter.CompareOp.GREATER_OR_EQUAL,
new BinaryComparator(Bytes.toBytes(minRating))
));
System.out.println(String.format("- 最低评分: %.1f", minRating));
}
// 库存状态
if (conditions.containsKey("inStock") && (Boolean) conditions.get("inStock")) {
dynamicFilter.addFilter(new SingleColumnValueFilter(
Bytes.toBytes("inventory"),
Bytes.toBytes("stock"),
CompareFilter.CompareOp.GREATER,
new BinaryComparator(Bytes.toBytes(0))
));
System.out.println("- 有库存");
}
// 分页
if (conditions.containsKey("pageSize")) {
int pageSize = (Integer) conditions.get("pageSize");
dynamicFilter.addFilter(new PageFilter(pageSize));
System.out.println(String.format("- 分页大小: %d", pageSize));
}
Scan scan = new Scan();
if (dynamicFilter.getFilters().size() > 0) {
scan.setFilter(dynamicFilter);
}
System.out.println("\n查询结果:");
int count = 0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
count++;
String rowKey = Bytes.toString(result.getRow());
Map<String, String> productInfo = extractProductInfo(result);
System.out.println(String.format("%d. %s", count, rowKey));
productInfo.entrySet().stream().limit(3).forEach(entry ->
System.out.println(String.format(" %s: %s", entry.getKey(), entry.getValue())));
}
}
System.out.println(String.format("\n共找到 %d 个符合条件的商品", count));
}
// 辅助方法
private Map<String, String> extractProductInfo(Result result) {
Map<String, String> info = new HashMap<>();
// 提取商品基本信息
String[] fields = {"name", "price", "category", "rating"};
for (String field : fields) {
Cell cell = result.getColumnLatestCell(
Bytes.toBytes("product_info"),
Bytes.toBytes(field)
);
if (cell != null) {
String value = Bytes.toString(CellUtil.cloneValue(cell));
info.put(translateField(field), value);
}
}
// 提取库存信息
Cell stockCell = result.getColumnLatestCell(
Bytes.toBytes("inventory"),
Bytes.toBytes("stock")
);
if (stockCell != null) {
String stock = Bytes.toString(CellUtil.cloneValue(stockCell));
info.put("库存", stock);
}
return info;
}
private String translateField(String field) {
Map<String, String> translations = Map.of(
"name", "名称",
"price", "价格",
"category", "分类",
"rating", "评分"
);
return translations.getOrDefault(field, field);
}
private String analyzeMatchedCondition(Result result, long thirtyDaysAgo) {
List<String> conditions = new ArrayList<>();
// 检查VIP用户
Cell levelCell = result.getColumnLatestCell(
Bytes.toBytes("customer_info"),
Bytes.toBytes("level")
);
if (levelCell != null && "VIP".equals(Bytes.toString(CellUtil.cloneValue(levelCell)))) {
conditions.add("VIP用户");
}
// 检查高价值订单
Cell amountCell = result.getColumnLatestCell(
Bytes.toBytes("order_info"),
Bytes.toBytes("amount")
);
if (amountCell != null) {
try {
double amount = Bytes.toDouble(CellUtil.cloneValue(amountCell));
if (amount > 1000.0) {
conditions.add("高价值订单");
}
} catch (Exception e) {
// 忽略转换错误
}
}
// 检查新用户
Cell regTimeCell = result.getColumnLatestCell(
Bytes.toBytes("customer_info"),
Bytes.toBytes("registration_time")
);
if (regTimeCell != null) {
try {
long regTime = Bytes.toLong(CellUtil.cloneValue(regTimeCell));
if (regTime > thirtyDaysAgo) {
conditions.add("新用户");
}
} catch (Exception e) {
// 忽略转换错误
}
}
return conditions.isEmpty() ? "未知" : String.join("+", conditions);
}
private boolean checkHighValueUser(Result result) {
// 检查是否为高价值用户 (VIP + 历史消费>5000)
Cell levelCell = result.getColumnLatestCell(
Bytes.toBytes("customer_info"),
Bytes.toBytes("level")
);
Cell spentCell = result.getColumnLatestCell(
Bytes.toBytes("customer_info"),
Bytes.toBytes("total_spent")
);
boolean isVip = levelCell != null &&
"VIP".equals(Bytes.toString(CellUtil.cloneValue(levelCell)));
boolean highSpent = false;
if (spentCell != null) {
try {
double spent = Bytes.toDouble(CellUtil.cloneValue(spentCell));
highSpent = spent > 5000.0;
} catch (Exception e) {
// 忽略转换错误
}
}
return isVip && highSpent;
}
private boolean checkPromotionProduct(Result result) {
// 检查是否为促销商品 (有折扣 + 库存>10)
Cell discountCell = result.getColumnLatestCell(
Bytes.toBytes("promotion"),
Bytes.toBytes("discount_rate")
);
Cell stockCell = result.getColumnLatestCell(
Bytes.toBytes("inventory"),
Bytes.toBytes("stock")
);
boolean hasDiscount = false;
if (discountCell != null) {
try {
double discount = Bytes.toDouble(CellUtil.cloneValue(discountCell));
hasDiscount = discount > 0.0;
} catch (Exception e) {
// 忽略转换错误
}
}
boolean hasStock = false;
if (stockCell != null) {
try {
int stock = Bytes.toInt(CellUtil.cloneValue(stockCell));
hasStock = stock > 10;
} catch (Exception e) {
// 忽略转换错误
}
}
return hasDiscount && hasStock;
}
}
📈 扫描优化策略
⚡ Scan性能优化
// Scan参数优化
public class ScanOptimizationExample {
// 1. 基础Scan优化配置
public void optimizedBasicScan(Table table) throws IOException {
Scan scan = new Scan();
// 性能优化参数
scan.setCaching(1000); // 客户端缓存行数
scan.setBatch(100); // 每个 Result 最多返回的 Cell 数;可能拆分一行
scan.setMaxResultSize(10 * 1024 * 1024); // 最大结果大小10MB
scan.setAllowPartialResults(true); // 允许部分结果
scan.setLoadColumnFamiliesOnDemand(true); // 按需加载列族
// 只扫描需要的列族和列
scan.addFamily(Bytes.toBytes("basic_info"));
scan.addColumn(Bytes.toBytes("contact"), Bytes.toBytes("phone"));
scan.addColumn(Bytes.toBytes("contact"), Bytes.toBytes("email"));
// 设置时间范围(如果数据有时间特征)
long oneWeekAgo = System.currentTimeMillis() - (7L * 24 * 60 * 60 * 1000);
scan.setTimeRange(oneWeekAgo, System.currentTimeMillis());
System.out.println("优化的基础扫描配置:");
System.out.println("- 客户端缓存: 1000 行");
System.out.println("- 批量大小: 100");
System.out.println("- 最大结果: 10MB");
System.out.println("- 按需加载列族");
System.out.println("- 时间范围: 最近一周");
long startTime = System.currentTimeMillis();
int count = 0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
count++;
// 处理结果(这里只是简单计数)
if (count % 1000 == 0) {
System.out.println(String.format("已处理 %d 行", count));
}
}
}
long duration = System.currentTimeMillis() - startTime;
System.out.println(String.format("扫描完成: %d 行, 用时 %d ms", count, duration));
System.out.println(String.format("扫描速度: %.2f 行/秒", count * 1000.0 / duration));
}
// 2. 智能范围扫描
public void smartRangeScan(Table table, String startPrefix, String endPrefix)
throws IOException {
Scan scan = new Scan();
// 设置扫描范围
scan.setStartRow(Bytes.toBytes(startPrefix));
scan.setStopRow(Bytes.toBytes(endPrefix));
// 添加前缀过滤器作为双重保险
Filter prefixFilter = new PrefixFilter(Bytes.toBytes(startPrefix));
scan.setFilter(prefixFilter);
// 优化参数
scan.setCaching(500);
scan.setBatch(50);
scan.setReadType(Scan.ReadType.PREAD); // 随机读优化
System.out.println(String.format("智能范围扫描: %s - %s", startPrefix, endPrefix));
Map<String, Integer> prefixStats = new TreeMap<>();
long totalBytes = 0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
// 统计前缀分布
String prefix = extractPrefix(rowKey, 3); // 提取前3个字符作为前缀
prefixStats.merge(prefix, 1, Integer::sum);
// 统计数据大小
for (Cell cell : result.rawCells()) {
totalBytes += CellUtil.cloneValue(cell).length;
}
}
}
System.out.println("前缀分布统计:");
prefixStats.forEach((prefix, count) ->
System.out.println(String.format(" %s*: %d 行", prefix, count)));
System.out.println(String.format("总数据量: %.2f KB", totalBytes / 1024.0));
}
// 3. 并行扫描实现
public void parallelScan(Connection connection, Table table, int parallelism) throws IOException {
System.out.println(String.format("并行扫描 (并行度: %d)", parallelism));
// 获取表的Region信息
List<HRegionLocation> regions;
try (RegionLocator regionLocator = connection.getRegionLocator(table.getName())) {
regions = regionLocator.getAllRegionLocations();
}
System.out.println(String.format("表共有 %d 个Region", regions.size()));
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(parallelism);
CompletionService<ScanResult> completionService =
new ExecutorCompletionService<>(executor);
long overallStartTime = System.currentTimeMillis();
// 为每个Region创建扫描任务
int submittedTasks = 0;
for (HRegionLocation region : regions) {
HRegionInfo regionInfo = region.getRegionInfo();
completionService.submit(() -> {
try {
return scanRegion(table, regionInfo);
} catch (IOException e) {
throw new RuntimeException("Region扫描失败", e);
}
});
submittedTasks++;
}
// 收集结果
long totalRows = 0;
long totalBytes = 0;
for (int i = 0; i < submittedTasks; i++) {
try {
Future<ScanResult> future = completionService.take();
ScanResult result = future.get();
totalRows += result.getRowCount();
totalBytes += result.getByteCount();
System.out.println(String.format(
"Region完成: %d 行, %.2f KB, 用时 %d ms",
result.getRowCount(),
result.getByteCount() / 1024.0,
result.getDuration()
));
} catch (Exception e) {
System.err.println("获取扫描结果时出错: " + e.getMessage());
}
}
long totalDuration = System.currentTimeMillis() - overallStartTime;
executor.shutdown();
System.out.println("\n并行扫描汇总:");
System.out.println(String.format("总行数: %d", totalRows));
System.out.println(String.format("总数据量: %.2f MB", totalBytes / (1024.0 * 1024)));
System.out.println(String.format("总用时: %d ms", totalDuration));
System.out.println(String.format("扫描速度: %.2f 行/秒", totalRows * 1000.0 / totalDuration));
System.out.println(String.format("吞吐量: %.2f MB/秒",
totalBytes / (1024.0 * 1024) * 1000.0 / totalDuration));
}
private ScanResult scanRegion(Table table, HRegionInfo regionInfo) throws IOException {
long startTime = System.currentTimeMillis();
Scan scan = new Scan();
scan.setStartRow(regionInfo.getStartKey());
scan.setStopRow(regionInfo.getEndKey());
scan.setCaching(1000);
scan.setBatch(100);
int rowCount = 0;
long byteCount = 0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
rowCount++;
for (Cell cell : result.rawCells()) {
byteCount += CellUtil.cloneValue(cell).length;
}
}
}
long duration = System.currentTimeMillis() - startTime;
return new ScanResult(rowCount, byteCount, duration);
}
// 4. 自适应扫描优化
public void adaptiveScan(Table table, String rowKeyPrefix) throws IOException {
System.out.println("自适应扫描优化");
// 第一阶段:小范围采样,评估数据特征
ScanMetrics metrics = sampleScan(table, rowKeyPrefix, 100);
System.out.println("采样结果:");
System.out.println(String.format("- 平均行大小: %.2f KB", metrics.getAvgRowSize() / 1024.0));
System.out.println(String.format("- 平均列数: %.1f", metrics.getAvgColumnCount()));
System.out.println(String.format("- 扫描速度: %.0f 行/秒", metrics.getScanRate()));
// 根据采样结果调整扫描参数
Scan optimizedScan = createOptimizedScan(rowKeyPrefix, metrics);
System.out.println("\n优化后的扫描参数:");
System.out.println(String.format("- 缓存大小: %d", optimizedScan.getCaching()));
System.out.println(String.format("- 批量大小: %d", optimizedScan.getBatch()));
System.out.println(String.format("- 最大结果: %d KB",
optimizedScan.getMaxResultSize() / 1024));
// 执行优化后的全量扫描
long startTime = System.currentTimeMillis();
int totalRows = 0;
try (ResultScanner scanner = table.getScanner(optimizedScan)) {
for (Result result : scanner) {
totalRows++;
// 每10000行输出进度
if (totalRows % 10000 == 0) {
long elapsed = System.currentTimeMillis() - startTime;
double currentRate = totalRows * 1000.0 / elapsed;
System.out.println(String.format("进度: %d 行, 当前速度: %.0f 行/秒",
totalRows, currentRate));
}
}
}
long totalTime = System.currentTimeMillis() - startTime;
System.out.println(String.format("\n扫描完成: %d 行, 用时 %d ms", totalRows, totalTime));
System.out.println(String.format("最终速度: %.2f 行/秒", totalRows * 1000.0 / totalTime));
}
private ScanMetrics sampleScan(Table table, String prefix, int sampleSize)
throws IOException {
Scan scan = new Scan();
FilterList sampleFilters = new FilterList(FilterList.Operator.MUST_PASS_ALL);
sampleFilters.addFilter(new PrefixFilter(Bytes.toBytes(prefix)));
sampleFilters.addFilter(new PageFilter(sampleSize));
scan.setFilter(sampleFilters); // PageFilter 是 Region 级限制,严格采样数量还需客户端限量
scan.setCaching(sampleSize);
long startTime = System.currentTimeMillis();
int rowCount = 0;
long totalBytes = 0;
int totalColumns = 0;
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
rowCount++;
totalColumns += result.size();
for (Cell cell : result.rawCells()) {
totalBytes += CellUtil.cloneValue(cell).length;
}
}
}
long duration = System.currentTimeMillis() - startTime;
return new ScanMetrics(
rowCount > 0 ? (double) totalBytes / rowCount : 0,
rowCount > 0 ? (double) totalColumns / rowCount : 0,
rowCount * 1000.0 / duration
);
}
private Scan createOptimizedScan(String prefix, ScanMetrics metrics) {
Scan scan = new Scan();
scan.setFilter(new PrefixFilter(Bytes.toBytes(prefix)));
// 根据平均行大小调整缓存
if (metrics.getAvgRowSize() < 1024) { // 小行
scan.setCaching(2000);
scan.setBatch(200);
} else if (metrics.getAvgRowSize() < 10240) { // 中等行
scan.setCaching(500);
scan.setBatch(50);
} else { // 大行
scan.setCaching(100);
scan.setBatch(10);
}
// 根据列数调整批量大小
if (metrics.getAvgColumnCount() > 50) {
scan.setBatch(Math.min(scan.getBatch(), 20));
}
// 设置合理的最大结果大小
long maxResultSize = Math.max(1024 * 1024, // 最小1MB
(long) (metrics.getAvgRowSize() * scan.getCaching() * 0.8));
scan.setMaxResultSize(maxResultSize);
// 如果扫描速度很慢,检查瓶颈及列族压缩配置;Scan 的表名属性不能启用压缩
if (metrics.getScanRate() < 1000) {
System.out.println("扫描较慢:请检查 IO、RPC、缓存及列族压缩配置,调整后重新测试");
}
return scan;
}
// 辅助类
public static class ScanResult {
private final int rowCount;
private final long byteCount;
private final long duration;
public ScanResult(int rowCount, long byteCount, long duration) {
this.rowCount = rowCount;
this.byteCount = byteCount;
this.duration = duration;
}
public int getRowCount() { return rowCount; }
public long getByteCount() { return byteCount; }
public long getDuration() { return duration; }
}
public static class ScanMetrics {
private final double avgRowSize;
private final double avgColumnCount;
private final double scanRate;
public ScanMetrics(double avgRowSize, double avgColumnCount, double scanRate) {
this.avgRowSize = avgRowSize;
this.avgColumnCount = avgColumnCount;
this.scanRate = scanRate;
}
public double getAvgRowSize() { return avgRowSize; }
public double getAvgColumnCount() { return avgColumnCount; }
public double getScanRate() { return scanRate; }
}
// 辅助方法
private String extractPrefix(String str, int length) {
return str.length() >= length ? str.substring(0, length) : str;
}
}
🎯 缓存策略优化
// 缓存优化配置
public class CacheOptimizationExample {
// 1. BlockCache优化配置
public void blockCacheOptimization(Admin admin, String tableName) throws IOException {
System.out.println("BlockCache优化配置");
TableDescriptor tableDesc = admin.getDescriptor(TableName.valueOf(tableName));
// 为不同访问模式的列族配置不同的缓存策略
TableDescriptorBuilder tableBuilder = TableDescriptorBuilder.newBuilder(tableDesc);
for (ColumnFamilyDescriptor cfDesc : tableDesc.getColumnFamilies()) {
String familyName = cfDesc.getNameAsString();
ColumnFamilyDescriptorBuilder cfBuilder =
ColumnFamilyDescriptorBuilder.newBuilder(cfDesc);
// 根据列族特征配置缓存
if (familyName.equals("hot_data")) {
// 热数据:启用块缓存,优先级高
cfBuilder.setBlockCacheEnabled(true);
cfBuilder.setCacheDataOnWrite(true);
cfBuilder.setCacheDataInL1(true);
cfBuilder.setPrefetchBlocksOnOpen(true);
System.out.println(" " + familyName + ": 高优先级缓存");
} else if (familyName.equals("warm_data")) {
// 温数据:常规缓存配置
cfBuilder.setBlockCacheEnabled(true);
cfBuilder.setCacheDataOnWrite(false);
cfBuilder.setCacheDataInL1(false);
System.out.println(" " + familyName + ": 常规缓存");
} else if (familyName.equals("cold_data")) {
// 冷数据:禁用缓存,节省内存
cfBuilder.setBlockCacheEnabled(false);
cfBuilder.setCacheDataOnWrite(false);
System.out.println(" " + familyName + ": 禁用缓存");
}
tableBuilder.setColumnFamily(cfBuilder.build());
}
// 应用配置
admin.modifyTable(tableBuilder.build());
System.out.println("BlockCache配置已更新");
}
// 2. 客户端缓存优化
public void clientCacheOptimization(Table table) throws IOException {
System.out.println("客户端缓存优化示例");
// 场景1:频繁访问相同数据的优化
Map<String, Result> localCache = new ConcurrentHashMap<>();
String[] frequentKeys = {"user_001", "user_002", "user_003"};
// 预加载热点数据
List<Get> gets = Arrays.stream(frequentKeys)
.map(key -> new Get(Bytes.toBytes(key)))
.collect(Collectors.toList());
Result[] results = table.get(gets);
for (int i = 0; i < results.length; i++) {
if (results[i] != null && !results[i].isEmpty()) {
localCache.put(frequentKeys[i], results[i]);
System.out.println("预加载: " + frequentKeys[i]);
}
}
// 模拟频繁访问;命中率仅统计以下访问,不包含预加载请求
int cacheHits = 0;
for (int i = 0; i < 100; i++) {
String key = frequentKeys[i % frequentKeys.length];
// 先检查本地缓存
Result cached = localCache.get(key);
if (cached != null) {
cacheHits++;
// 使用缓存数据
processResult(cached, true);
} else {
// 从HBase获取
Get get = new Get(Bytes.toBytes(key));
Result result = table.get(get);
localCache.put(key, result);
processResult(result, false);
}
}
System.out.println(String.format("本地缓存命中率: %.1f%%",
cacheHits * 100.0 / 100));
}
// 3. 扫描缓存优化
public void scanCacheOptimization(Table table) throws IOException {
System.out.println("扫描缓存优化");
// 测试不同缓存配置的性能
int[] cachingSizes = {100, 500, 1000, 2000};
for (int caching : cachingSizes) {
long startTime = System.currentTimeMillis();
int count = 0;
Scan scan = new Scan();
scan.setCaching(caching);
scan.setBatch(50);
scan.setMaxResultSize(2 * 1024 * 1024); // 2MB
scan.setFilter(new PageFilter(5000)); // 限制总行数用于测试
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
count++;
}
}
long duration = System.currentTimeMillis() - startTime;
double rate = count * 1000.0 / duration;
System.out.println(String.format(
"缓存大小: %4d, 扫描 %d 行, 用时 %4d ms, 速度 %6.1f 行/秒",
caching, count, duration, rate));
}
}
// 4. 智能预取策略
public void intelligentPrefetch(Table table, String userIdPrefix) throws IOException {
System.out.println("智能预取策略");
// 第一步:分析访问模式
Map<String, Integer> accessPattern = analyzeAccessPattern(table, userIdPrefix);
// 第二步:根据访问频率预取数据
List<String> hotKeys = accessPattern.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(20) // 预取Top20热点数据
.map(Map.Entry::getKey)
.collect(Collectors.toList());
System.out.println("预取热点数据:");
hotKeys.forEach(key -> System.out.println(" " + key));
// 第三步:批量预取
List<Get> prefetchGets = hotKeys.stream()
.map(key -> {
Get get = new Get(Bytes.toBytes(key));
get.setCacheBlocks(true); // 确保数据进入缓存
return get;
})
.collect(Collectors.toList());
long startTime = System.currentTimeMillis();
Result[] prefetchResults = table.get(prefetchGets);
long prefetchTime = System.currentTimeMillis() - startTime;
int prefetchedCount = (int) Arrays.stream(prefetchResults)
.filter(result -> result != null && !result.isEmpty())
.count();
System.out.println(String.format(
"预取完成: %d/%d 成功, 用时 %d ms",
prefetchedCount, hotKeys.size(), prefetchTime));
// 第四步:验证缓存效果
testCacheEffectiveness(table, hotKeys);
}
private Map<String, Integer> analyzeAccessPattern(Table table, String prefix)
throws IOException {
Map<String, Integer> pattern = new HashMap<>();
// 模拟分析最近访问日志(实际应用中从日志系统获取)
Scan scan = new Scan();
FilterList analysisFilters = new FilterList(FilterList.Operator.MUST_PASS_ALL);
analysisFilters.addFilter(new PrefixFilter(Bytes.toBytes(prefix)));
analysisFilters.addFilter(new PageFilter(1000));
scan.setFilter(analysisFilters); // 保留前缀条件;严格限制总量还需客户端限量
scan.addColumn(Bytes.toBytes("access_log"), Bytes.toBytes("last_access"));
try (ResultScanner scanner = table.getScanner(scan)) {
for (Result result : scanner) {
String rowKey = Bytes.toString(result.getRow());
Cell accessCell = result.getColumnLatestCell(
Bytes.toBytes("access_log"),
Bytes.toBytes("last_access")
);
if (accessCell != null) {
// 根据最近访问时间计算访问权重
long lastAccess = Bytes.toLong(CellUtil.cloneValue(accessCell));
long daysSinceAccess = (System.currentTimeMillis() - lastAccess)
/ (24 * 60 * 60 * 1000);
// 访问越近,权重越高
int weight = Math.max(1, 10 - (int) daysSinceAccess);
pattern.put(rowKey, weight);
}
}
}
return pattern;
}
private void testCacheEffectiveness(Table table, List<String> keys) throws IOException {
System.out.println("测试缓存效果:");
long startTime = System.currentTimeMillis();
// 连续访问预取的数据
for (String key : keys) {
Get get = new Get(Bytes.toBytes(key));
Result result = table.get(get);
if (result != null && !result.isEmpty()) {
// 简单处理数据以模拟实际使用
@SuppressWarnings("unused")
int cellCount = result.size();
}
}
long duration = System.currentTimeMillis() - startTime;
double avgLatency = (double) duration / keys.size();
System.out.println(String.format(
"访问 %d 个预取数据, 总用时 %d ms, 平均延迟 %.2f ms",
keys.size(), duration, avgLatency));
// 对比:访问未预取的随机数据
testRandomAccess(table, keys.size());
}
private void testRandomAccess(Table table, int count) throws IOException {
System.out.println("对比测试 - 随机访问:");
long startTime = System.currentTimeMillis();
// 生成随机键访问
Random random = new Random();
for (int i = 0; i < count; i++) {
String randomKey = "random_" + random.nextInt(100000);
Get get = new Get(Bytes.toBytes(randomKey));
try {
Result result = table.get(get);
// 处理结果(可能为空)
@SuppressWarnings("unused")
boolean hasData = result != null && !result.isEmpty();
} catch (Exception e) {
// 忽略不存在的键
}
}
long duration = System.currentTimeMillis() - startTime;
double avgLatency = (double) duration / count;
System.out.println(String.format(
"访问 %d 个随机数据, 总用时 %d ms, 平均延迟 %.2f ms",
count, duration, avgLatency));
}
private void processResult(Result result, boolean fromCache) {
// 模拟数据处理
if (result != null && !result.isEmpty()) {
String source = fromCache ? "缓存" : "HBase";
// System.out.println("处理数据来源: " + source);
}
}
}
🎉 过滤器优化总结
success 🌟 **恭喜!你已经掌握了HBase过滤器与扫描优化的精髓!**
你现在掌握的核心技能:
- ✅ 过滤器体系:理解各类过滤器的特点和适用场景
- ✅ 性能优化:掌握过滤器的性能影响和优化策略
- ✅ 复合查询:能够设计复杂的多条件过滤逻辑
- ✅ 扫描优化:掌握Scan参数调优和并行扫描技术
- ✅ 缓存策略:理解缓存机制和预取优化方法
- ✅ 实战应用:能够解决实际业务中的查询性能问题
完成上述内容后,还需通过掌握检查、练习、代码修复及目标环境测试,才能评估是否具备 HBase 高级查询优化的实际能力。 🚀
🎯 过滤器选择指南
// 过滤器选择决策树
if (查询条件 == "RowKey模式") {
if (精确前缀匹配) {
使用 PrefixFilter;
} else if (正则表达式匹配) {
使用 RowFilter + RegexStringComparator;
} else if (范围查询) {
使用 Scan.setStartRow/setStopRow;
}
} else if (查询条件 == "列值条件") {
if (单列简单条件) {
使用 SingleColumnValueFilter;
} else if (多列复杂条件) {
使用 FilterList组合;
} else if (值模式匹配) {
使用 ValueFilter + 合适的Comparator;
}
} else if (查询需求 == "性能优化") {
if (需要分页) {
使用 PageFilter;
} else if (需要采样) {
使用 RandomRowFilter;
} else if (只需要统计) {
使用 KeyOnlyFilter;
}
}
过滤器性能对比
以下按常见访问阶段列出定性开销参考,不是经验证的固定效率排名;即使数据量与过滤比例相同,投影、比较器、缓存及扫描范围仍会改变结果:
| 排名 | 过滤器 | 过滤粒度 | 过滤发生点 | 典型耗时(百万行) | 说明 |
|---|---|---|---|---|---|
| 1 | setStartRow / setStopRow |
行级 | HFile Block 预取阶段 | 极低(Block 级跳过) | 直接跳过不在范围内的 Block,不需反序列化 |
| 2 | PrefixFilter |
行级 | RowKey 比较阶段 | 低 | 只比较 RowKey 字节前缀,无需读列 |
| 3 | RowFilter |
行级 | RowKey 比较阶段 | 低~中 | 支持正则、子串等,取决于比较器复杂度 |
| 4 | FamilyFilter |
列族级 | Column family 解码后 | 中 | 需读取到列族字节,不需解码列值 |
| 5 | QualifierFilter |
列限定符级 | Qualifier 解码后 | 中 | 需读取 Qualifier 字节,不需解码列值 |
| 6 | SingleColumnValueFilter |
列值级 | 指定列的 Cell 值比较阶段 | 中高 | 需访问指定列的值;是否解码及其开销取决于比较器 |
| 7 | ValueFilter |
列值级 | 扫描到的 Cell 值比较阶段 | 视扫描范围及比较器而定 | 对扫描到的 Cell 值进行比较,不一定反序列化,也不能断言开销总是最大 |
tip 性能优化原则
过滤器应尽可能作用在数据访问路径的最早阶段。理想顺序:RowKey 范围 → PrefixFilter → FamilyFilter → QualifierFilter → SingleColumnValueFilter。ValueFilter 应作为最后手段。
Scan 关键参数参考
| 参数 | 方法 | 默认值 | 调优建议 |
|---|---|---|---|
| 行缓存 | setCaching(n) |
依版本及客户端配置而定 | 增大通常可减少 RPC 次数;请求行数还受结果大小、部分结果及服务端限制影响,需评估内存与处理时间 |
| Cell 批量 | setBatch(n) |
-1(不限) | 控制每个 Result 的最大 Cell 数,可能拆分一行;不是单次 RPC 的最大列数,计行与行级处理需考虑拆分结果 |
| 最大结果大小 | setMaxResultSize(bytes) |
依版本及客户端配置而定 | 与 setCaching 配合限制响应规模,还需考虑宽行、部分结果及服务端行为;不能将 2MB 当作通用默认值 |
| 列族投影 | addFamily(family) |
全列族 | 只添加需要的列族,减少 IO |
| 列投影 | addColumn(family, qualifier) |
全列 | 精确指定需要的列,减少网络传输 |
| 起始行 | setStartRow(row) |
表头 | 配合 RowKey 设计实现范围扫描;包含边界 |
| 终止行 | setStopRow(row) |
表尾 | 不包含边界;setStartRow + setStopRow 是最高效的范围限定 |
| 版本数 | setMaxVersions(n) |
1 | 多版本场景按需设置;默认只返回最新版本 |
| 时间范围 | setTimeRange(min, max) |
无限制 | 按 Cell 版本时间戳筛选,范围为 [min, max);时间戳可由客户端指定,不等同于业务时间列,也不能返回已被 TTL 清理的数据 |
| 读类型 | setReadType(type) |
DEFAULT | STREAM 适合全表扫描;PREAD 适合随机小范围扫描 |
Scan + Filter 组合示例
以下示例演示 FilterList + SingleColumnValueFilter + PageFilter 的完整组合,用于带条件的游标分页:
// 查询 status=active 的用户,每页20条,游标分页
public PaginationResult queryActiveUsers(Table table, String cursor, int pageSize)
throws IOException {
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
// 1. 业务条件:status 列值等于 "active"
SingleColumnValueFilter statusFilter = new SingleColumnValueFilter(
Bytes.toBytes("info"),
Bytes.toBytes("status"),
CompareFilter.CompareOp.EQUAL,
new BinaryComparator(Bytes.toBytes("active"))
);
// filterIfMissing=true:没有 status 列的行视为不匹配,直接过滤掉
statusFilter.setFilterIfMissing(true);
filterList.addFilter(statusFilter);
// 2. 分页控制:多取1条用于判断是否有下一页
filterList.addFilter(new PageFilter(pageSize + 1));
Scan scan = new Scan();
scan.setFilter(filterList);
scan.setCaching(pageSize + 1);
// 列投影:只取需要的字段,减少传输量
scan.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
scan.addColumn(Bytes.toBytes("info"), Bytes.toBytes("status"));
// 游标:从上一页最后一个 RowKey 的下一行开始
if (cursor != null && !cursor.isEmpty()) {
// "\0" 追加确保从 cursor 的后一行开始(不包含 cursor 本身)
scan.setStartRow(Bytes.toBytes(cursor + "\0"));
}
List<String> rowKeys = new ArrayList<>();
String nextCursor = null;
boolean hasNextPage = false;
try (ResultScanner scanner = table.getScanner(scan)) {
int count = 0;
for (Result result : scanner) {
count++;
String rowKey = Bytes.toString(result.getRow());
if (count <= pageSize) {
rowKeys.add(rowKey);
nextCursor = rowKey;
} else {
hasNextPage = true;
break;
}
}
}
return new PaginationResult(rowKeys, nextCursor, hasNextPage);
}
note PageFilter 的真实分页限制
PageFilter 在每个 Region 的扫描中分别计数,而不是按 RegionServer 全局计数。跨多个 Region 的扫描可能返回多于 pageSize 行。严格分页须在客户端最多收集 pageSize 条,并多取一条判断下一页,再用最后一条已返回记录的 RowKey 续扫;PageFilter 仅作辅助限制。本文字符串追加 "\0" 的游标示例只适用于能无损往返转换的字符串 RowKey,任意二进制 RowKey 应保留原始字节并使用目标版本支持的排他起始边界。
掌握检查
完成本文学习后,请检验以下能力:
- [ ] 能够说明服务端过滤和客户端过滤的本质区别,并举出一个适合各自场景的例子
- [ ] 能够说明 RowFilter、ValueFilter、PrefixFilter、SingleColumnValueFilter、FamilyFilter 的访问阶段与开销来源,并解释为何不能脱离数据分布、投影和比较器给出固定效率排名
- [ ] 理解
filterIfMissing=true的实际含义,以及不设置时默认行为会导致什么结果 - [ ] 能够解释
setCaching和setBatch的区别,并说明各自在什么场景下需要调整 - [ ] 能够用
FilterList(MUST_PASS_ALL)和FilterList(MUST_PASS_ONE)组合出 AND 和 OR 逻辑
练习题
题目一:过滤器选型
有一张 HBase 表,RowKey 格式为 userId_timestamp,有两个列族 profile(用户基本信息)和 order(订单信息)。现在需要查询以下数据:满足"用户 ID 以 VIP_ 开头"且"订单金额大于 1000 元"的所有记录,每页返回 50 条。请设计过滤器组合,并说明每个过滤器的作用及加入顺序的理由。
题目二:性能问题排查
某团队对一个每行有 200 列的宽表执行全表 Scan,只需要其中 3 列的数据,但查询速度比预期慢 10 倍。请列举 3 个可能导致性能低下的原因,并给出对应的 API 修复方案。
#HBase #过滤器 #扫描优化 #性能调优 #查询优化 #缓存策略