前言
这次讲的是基于HBase的模糊查询和分页。肯定有人问为什么要用HBase,其实我也知道基于大数据量的检索和模糊查询和分页 用ES比HBase强多了。因为HBase是面向列存储的K-V型nosql数据库,先天的特性使它不擅长于业务复杂的查询 甚至是模糊查询。但是实际情况是客户落地的机器数量有限制,只能从已有的数据库中去想办法。还好不是业务比较复杂的查询,只是简单的基于日期和标题的模糊查询。那么就使用HBase就能满足。话不多说,我们开始介绍。
1、构思
现有的元数据是存放在HBase中的,rowkey为了防止热点现象,采取了id倒序的设计。绝大部分场景是用户直接访问rowkey获取对应的内容。现在要求加上搜索功能,原来的rowkey不能改,只能构建二级索引去指向元数据的rowkey,通过二次查找来获取元数据。这里二级索引rowkey设计规则可以是
id倒序 + 日期 + 标题
其中id倒序用来防止热点现象,日期和标题也作为rowkey的组成部分方便后续用RowFilter进行模糊查询,HBase对rowkey的操作要比对column的操作性能要好上很多。
分页功能可用PageFilter实现。
2、实现
这里我实现了一个通用方法。
/**
* 获取指定Rowkey正则的资讯列表(分页)
*
* @param pageSize 页大小
* @param lastRowKey 上一页最后的rowkey
* @param rowkeyReg Rowkey正则
* @return 资讯列表
*/
public List<GeneralNewsInfo> getData(int pageSize, String lastRowKey, String rowkeyReg) {
List<GeneralNewsInfo> dataList = new ArrayList<>();
getConnection();
try {
// 二级索引表查询索引数据
Table table = conn.getTable(TableName.valueOf(RcmdHbaseConstants.RCMD_NEWS_TITLE_INDEX));
Scan scan = new Scan();
// 构建模糊查询的Filter和分页的Filter
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
if (rowkeyReg != null) {
RegexStringComparator regex = new RegexStringComparator(rowkeyReg);
Filter filter = new RowFilter(CompareFilter.CompareOp.EQUAL, regex);
filterList.addFilter(filter);
}
Filter pageFilter = new PageFilter(pageSize);
Filter rowFilter = new RowFilter(CompareFilter.CompareOp.GREATER,
new BinaryComparator(Bytes.toBytes(lastRowKey)));
filterList.addFilter(pageFilter);
filterList.addFilter(rowFilter);
scan.setFilter(filterList);
ResultScanner rs = table.getScanner(scan);
Result result;
int rowNum = 0;
while ((result = rs.next()) != null) {
if (rowNum >= pageSize) {
break;
}
List<Cell> listCells = result.listCells();
for (Cell cell : listCells) {
String rowkey = Bytes.toString(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength());
// 索引拿到rowkey 去元数据表中获取元数据
Get get = new Get(Bytes.toBytes(rowkey));
GeneralNewsInfo generalNewsInfo = getDataByRowKey(RcmdHbaseConstants.RCMD_NEWS_REPOSITORY, get);
if (!StringUtils.isEmpty(generalNewsInfo.getRowkey())) {
dataList.add(generalNewsInfo);
rowNum++;
} else {
LOGGER.error("##### rowkey not found {}", rowkey);
}
}
}
table.close();
} catch (IOException e) {
LOGGER.error("##### Hbase获取资讯列表失败");
e.printStackTrace();
}
return dataList;
}
/**
* 根据rowkey获取hbase元数据
* @param tableName 表名
* @param get HbaseGet
*/
public GeneralNewsInfo getDataByRowKey(String tableName, Get get) {
GeneralNewsInfo generalNewsInfo = new GeneralNewsInfo();
try {
getConnection();
Table table = conn.getTable(TableName.valueOf(tableName));
Result result = table.get(get);
Cell[] cells = result.rawCells();
String rowkey = "";
if (cells.length >= 1) {
for (Cell cell : cells) {
rowkey = Bytes.toString(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength());
String column = Bytes.toString(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength());
String value = Bytes.toString(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength());
setGeneralNewsInfo(generalNewsInfo, column, value);
}
}
generalNewsInfo.setRowkey(rowkey);
table.close();
} catch (Exception e) {
LOGGER.error("####获取hbase数据出错:" + e);
}
return generalNewsInfo;
}
对于 rowkeyReg 的生成,要根据实际情况来确定,我这边需要根据上述设计好的二级索引rowkey规则来构建 rowkeyReg ,代码如下:
/**
* 获取Hbase 查询Reg
*
* @param timeStart 开始时间
* @param timeEnd 结束时间
* @param titleStr 标题关键词
* @return 正则
*/
private String getTitleRegex(String timeStart, String timeEnd, String titleStr) {
titleStr = titleStr.replace("*", "\\*").replace("(", "\\(").replace(")", "\\)")
.replace("}", "\\}").replace("{", "\\{").replace("|", "\\|");
if (StringUtils.isEmpty(timeStart) && StringUtils.isEmpty(timeEnd) && StringUtils.isEmpty(titleStr)) {
return null;
}
String regex = "";
if (!StringUtils.isEmpty(timeStart) && !StringUtils.isEmpty(timeEnd)) {
regex += "(";
LocalDate ss = LocalDate.parse(timeStart, DateTimeFormatter.ofPattern("yyyyMMdd"));
LocalDate ee = LocalDate.parse(timeEnd, DateTimeFormatter.ofPattern("yyyyMMdd"));
LocalDateTime startTime = LocalDateTime.of(ss.getYear(), ss.getMonth(), ss.getDayOfMonth(), 0, 0, 0);
LocalDateTime endTime = LocalDateTime.of(ee.getYear(), ee.getMonth(), ee.getDayOfMonth(), 0, 0, 0);
while (startTime.isBefore(endTime) || startTime.isEqual(endTime)) {
String ymd = startTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
regex += ymd + "|";
startTime = startTime.plusDays(1);
}
regex = regex.substring(0, regex.lastIndexOf("|"));
regex += ")";
}
if (StringUtils.isEmpty(regex)) {
regex += "[0-9]{4}";
}
if (StringUtils.isEmpty(titleStr)) {
regex += ".*";
} else {
regex += ".*" + titleStr + ".*";
}
return "\\S{" + (paramConfig.getIdFixNum() + 2) + "}" + regex;
}
根据此方法获取rowkey正则,pageSize 和 lastRowKey 根据实际情况进行传入,就可以实现模糊分页查询啦!如图所示功能已经可以使用(涉及公司隐私做了稍许处理哈_)。
直接一页一页按顺序查询不跳转,速度还是很快的。实测600万多条记录,每次查询在100ms以内。
真正实现所有功能。还需要对应用端对查询进行二次封装,对每页的 lastRowKey进行缓存,前端分页控件最好自己实现逻辑,要禁止大范围的跳转,不然后台递归获取 lastRowKey 性能会很差。
对于性能有要求的,可以用异步方法去维护一个从当前页数到后续一段范围内的页数对应的rowkey池。这样用户查询的时候就不用递归获取rowkey了。这些与本次主题无关就暂不赘述啦。
其实真正要实现多维度查询,资源够用的情况下用ES是好的选择。hbase只用来存元数据,es来构建索引存储索引数据和提供查询支持。
————————————————
版权声明:本文为CSDN博主「Yano阳」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/longyangaaoo/article/details/81119436
网友评论