基於Solr的HBase多條件查詢測試
背景:
某電信專案中採用HBase來儲存使用者終端明細資料,供前臺頁面即時查詢。HBase無可置疑擁有其優勢,但其本身只對rowkey支援毫秒級的快 速檢索,對於多欄位的組合查詢卻無能為力。針對HBase的多條件查詢也有多種方案,但是這些方案要麼太複雜,要麼效率太低,本文只對基於Solr的HBase多條件查詢方案進行測試和驗證。
原理:
基於Solr的HBase多條件查詢原理很簡單,將HBase表中涉及條件過濾的欄位和rowkey在Solr中建立索引,透過Solr的多條件查詢快速獲得符合過濾條件的rowkey值,拿到這些rowkey之後在HBASE中透過指定rowkey進行查詢。
測試環境:
solr 4.0.0版本,使用其自帶的jetty服務端容器,單節點;
hbase-0.94.2-cdh4.2.1,10臺Lunux伺服器組成的HBase叢集。
HBase中2512萬條資料172個欄位;
Solr索引HBase中的100萬條資料;
測試結果:
1、100萬條資料在Solr中對8個欄位建立索引。在Solr中最多8個過濾條件獲取51316條資料的rowkey值,基本在57-80毫秒。根據Solr返回的rowkey值在HBase表中獲取所有51316條資料12個欄位值,耗時基本在15秒;
2、資料量同上,過濾條件同上,採用Solr分頁查詢,每次獲取20條資料,Solr獲得20個rowkey值耗時4-10毫秒,拿到Solr傳入的rowkey值在HBase中獲取對應20條12個欄位的資料,耗時6毫秒。
以下列出測試環境的搭建、以及相關程式碼實現過程。
一、Solr環境的搭建
因為初衷只是測試Solr的使用,Solr的執行環境也只是用了其自帶的jetty,而非大多人用的Tomcat;沒有搭建Solr叢集,只是一個單一的Solr服務端,也沒有任何引數調優。
1)在Apache網站上下載Solr 4:,我們這裡下載的是“apache-solr-4.0.0.tgz”;
2)在當前目錄解壓Solr壓縮包:
cd /opt tar -xvzf apache-solr-4.0.0.tgz
3)修改Solr的配置檔案schema.xml,新增我們需要索引的多個欄位(配置檔案位於“/opt/apache-solr-4.0.0/example/solr/collection1/conf/”)
"rowkey" type="string" indexed="true" stored="true" required="true" multiValued="false" /> "time" type="string" indexed="true" stored="true" required="false" multiValued="false" /> "tebid" type="string" indexed="true" stored="true" required="false" multiValued="false" /> "tetid" type="string" indexed="true" stored="true" required="false" multiValued="false" /> "puid" type="string" indexed="true" stored="true" required="false" multiValued="false" /> "mgcvid" type="string" indexed="true" stored="true" required="false" multiValued="false" /> "mtcvid" type="string" indexed="true" stored="true" required="false" multiValued="false" /> "smaid" type="string" indexed="true" stored="true" required="false" multiValued="false" /> "mtlkid" type="string" indexed="true" stored="true" required="false" multiValued="false" />
另外關鍵的一點是修改原有的uniqueKey,本文設定HBase表的rowkey欄位為Solr索引的uniqueKey:
rowkey
type 引數代表索引資料型別,我這裡將type全部設定為string是為了避免異常型別的資料導致索引建立失敗,正常情況下應該根據實際欄位型別設定,比如整型欄位設定為int,更加有利於索引的建立和檢索;
indexed 引數代表此欄位是否建立索引,根據實際情況設定,建議不參與條件過濾的欄位一律設定為false;
stored 引數代表是否儲存此欄位的值,建議根據實際需求只將需要獲取值的欄位設定為true,以免浪費儲存,比如我們的場景只需要獲取rowkey,那麼只需把rowkey欄位設定為true即可,其他欄位全部設定flase;
required 引數代表此欄位是否必需,如果資料來源某個欄位可能存在空值,那麼此屬性必需設定為false,不然Solr會丟擲異常;
multiValued 引數代表此欄位是否允許有多個值,通常都設定為false,根據實際需求可設定為true。
4)我們使用Solr自帶的example來作為執行環境,定位到example目錄,啟動服務監聽:
cd /opt/apache-solr-4.0.0/example java -jar ./start.jar
如果啟動成功,可以透過瀏覽器開啟此頁面:http://192.168.1.10:8983/solr/
二、讀取HBase源表的資料,在Solr中建立索引
一種方案是透過HBase的普通API獲取資料建立索引,此方案的缺點是效率較低每秒只能處理100多條資料(或許可以透過多執行緒提高效率):
package com.ultrapower.hbase.solrhbase; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.KeyValue; import org.apache.hadoop.hbase.client.HTable; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.ResultScanner; import org.apache.hadoop.hbase.client.Scan; import org.apache.hadoop.hbase.util.Bytes; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.HttpSolrServer; import org.apache.solr.common.SolrInputDocument; public class SolrIndexer { /** * @param args * @throws IOException * @throws SolrServerException */ public static void main(String[] args) throws IOException, SolrServerException { final Configuration conf; HttpSolrServer solrServer = new HttpSolrServer( "http://192.168.1.10:8983/solr"); // 因為服務端是用的Solr自帶的jetty容器,預設埠號是8983 conf = HBaseConfiguration.create(); HTable table = new HTable(conf, "hb_app_xxxxxx"); // 這裡指定HBase表名稱 Scan scan = new Scan(); scan.addFamily(Bytes.toBytes("d")); // 這裡指定HBase表的列族 scan.setCaching(500); scan.setCacheBlocks(false); ResultScanner ss = table.getScanner(scan); System.out.println("start ..."); int i = 0; try { for (Result r : ss) { SolrInputDocument solrDoc = new SolrInputDocument(); solrDoc.addField("rowkey", new String(r.getRow())); for (KeyValue kv : r.raw()) { String fieldName = new String(kv.getQualifier()); String fieldValue = new String(kv.getValue()); if (fieldName.equalsIgnoreCase("time") || fieldName.equalsIgnoreCase("tebid") || fieldName.equalsIgnoreCase("tetid") || fieldName.equalsIgnoreCase("puid") || fieldName.equalsIgnoreCase("mgcvid") || fieldName.equalsIgnoreCase("mtcvid") || fieldName.equalsIgnoreCase("smaid") || fieldName.equalsIgnoreCase("mtlkid")) { solrDoc.addField(fieldName, fieldValue); } } solrServer.add(solrDoc); solrServer.commit(true, true, true); i = i + 1; System.out.println("已經成功處理 " + i + " 條資料"); } ss.close(); table.close(); System.out.println("done !"); } catch (IOException e) { } finally { ss.close(); table.close(); System.out.println("erro !"); } } }
另外一種方案是用到HBase的Mapreduce框架,分散式並行執行效率特別高,處理1000萬條資料僅需5分鐘,但是這種高併發需要對Solr伺服器進行配置調優,不然會丟擲伺服器無法響應的異常:
Error: org.apache.solr.common.SolrException: Server at http://192.168.1.10:8983/solr returned non ok status:503, message:Service Unavailable
MapReduce入口程式:
package com.ultrapower.hbase.solrhbase; import java.io.IOException; import java.net.URISyntaxException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.client.Scan; import org.apache.hadoop.hbase.mapreduce.TableMapReduceUtil; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.output.NullOutputFormat; public class SolrHBaseIndexer { private static void usage() { System.err.println("輸入引數: "); System.exit(1); } private static Configuration conf; public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException, URISyntaxException { if (args.length == 0 || args.length > 3) { usage(); } createHBaseConfiguration(args[0]); ConfigProperties tutorialProperties = new ConfigProperties(args[0]); String tbName = tutorialProperties.getHBTbName(); String tbFamily = tutorialProperties.getHBFamily(); Job job = new Job(conf, "SolrHBaseIndexer"); job.setJarByClass(SolrHBaseIndexer.class); Scan scan = new Scan(); if (args.length == 3) { scan.setStartRow(Bytes.toBytes(args[1])); scan.setStopRow(Bytes.toBytes(args[2])); } scan.addFamily(Bytes.toBytes(tbFamily)); scan.setCaching(500); // 設定快取資料量來提高效率 scan.setCacheBlocks(false); // 建立Map任務 TableMapReduceUtil.initTableMapperJob(tbName, scan, SolrHBaseIndexerMapper.class, null, null, job); // 不需要輸出 job.setOutputFormatClass(NullOutputFormat.class); // job.setNumReduceTasks(0); System.exit(job.waitForCompletion(true) ? 0 : 1); } /** * 從配置檔案讀取並設定HBase配置資訊 * * @param propsLocation * @return */ private static void createHBaseConfiguration(String propsLocation) { ConfigProperties tutorialProperties = new ConfigProperties( propsLocation); conf = HBaseConfiguration.create(); conf.set("hbase.zookeeper.quorum", tutorialProperties.getZKQuorum()); conf.set("hbase.zookeeper.property.clientPort", tutorialProperties.getZKPort()); conf.set("hbase.master", tutorialProperties.getHBMaster()); conf.set("hbase.rootdir", tutorialProperties.getHBrootDir()); conf.set("solr.server", tutorialProperties.getSolrServer()); } }
對應的Mapper:
package com.ultrapower.hbase.solrhbase; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.KeyValue; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.io.ImmutableBytesWritable; import org.apache.hadoop.hbase.mapreduce.TableMapper; import org.apache.hadoop.io.Text; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.HttpSolrServer; import org.apache.solr.common.SolrInputDocument; public class SolrHBaseIndexerMapper extends TableMapper{ public void map(ImmutableBytesWritable key, Result hbaseResult, Context context) throws InterruptedException, IOException { Configuration conf = context.getConfiguration(); HttpSolrServer solrServer = new HttpSolrServer(conf.get("solr.server")); solrServer.setDefaultMaxConnectionsPerHost(100); solrServer.setMaxTotalConnections(1000); solrServer.setSoTimeout(20000); solrServer.setConnectionTimeout(20000); SolrInputDocument solrDoc = new SolrInputDocument(); try { solrDoc.addField("rowkey", new String(hbaseResult.getRow())); for (KeyValue rowQualifierAndValue : hbaseResult.list()) { String fieldName = new String( rowQualifierAndValue.getQualifier()); String fieldValue = new String(rowQualifierAndValue.getValue()); if (fieldName.equalsIgnoreCase("time") || fieldName.equalsIgnoreCase("tebid") || fieldName.equalsIgnoreCase("tetid") || fieldName.equalsIgnoreCase("puid") || fieldName.equalsIgnoreCase("mgcvid") || fieldName.equalsIgnoreCase("mtcvid") || fieldName.equalsIgnoreCase("smaid") || fieldName.equalsIgnoreCase("mtlkid")) { solrDoc.addField(fieldName, fieldValue); } } solrServer.add(solrDoc); solrServer.commit(true, true, true); } catch (SolrServerException e) { System.err.println("更新Solr索引異常:" + new String(hbaseResult.getRow())); } } }
讀取引數配置檔案的輔助類:
package com.ultrapower.hbase.solrhbase; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.Properties; public class ConfigProperties { private static Properties props; private String HBASE_ZOOKEEPER_QUORUM; private String HBASE_ZOOKEEPER_PROPERTY_CLIENT_PORT; private String HBASE_MASTER; private String HBASE_ROOTDIR; private String DFS_NAME_DIR; private String DFS_DATA_DIR; private String FS_DEFAULT_NAME; private String SOLR_SERVER; // Solr伺服器地址 private String HBASE_TABLE_NAME; // 需要建立Solr索引的HBase表名稱 private String HBASE_TABLE_FAMILY; // HBase表的列族 public ConfigProperties(String propLocation) { props = new Properties(); try { File file = new File(propLocation); System.out.println("從以下位置載入配置檔案: " + file.getAbsolutePath()); FileReader is = new FileReader(file); props.load(is); HBASE_ZOOKEEPER_QUORUM = props.getProperty("HBASE_ZOOKEEPER_QUORUM"); HBASE_ZOOKEEPER_PROPERTY_CLIENT_PORT = props.getProperty("HBASE_ZOOKEEPER_PROPERTY_CLIENT_PORT"); HBASE_MASTER = props.getProperty("HBASE_MASTER"); HBASE_ROOTDIR = props.getProperty("HBASE_ROOTDIR"); DFS_NAME_DIR = props.getProperty("DFS_NAME_DIR"); DFS_DATA_DIR = props.getProperty("DFS_DATA_DIR"); FS_DEFAULT_NAME = props.getProperty("FS_DEFAULT_NAME"); SOLR_SERVER = props.getProperty("SOLR_SERVER"); HBASE_TABLE_NAME = props.getProperty("HBASE_TABLE_NAME"); HBASE_TABLE_FAMILY = props.getProperty("HBASE_TABLE_FAMILY"); } catch (IOException e) { throw new RuntimeException("載入配置檔案出錯"); } catch (NullPointerException e) { throw new RuntimeException("檔案不存在"); } } public String getZKQuorum() { return HBASE_ZOOKEEPER_QUORUM; } public String getZKPort() { return HBASE_ZOOKEEPER_PROPERTY_CLIENT_PORT; } public String getHBMaster() { return HBASE_MASTER; } public String getHBrootDir() { return HBASE_ROOTDIR; } public String getDFSnameDir() { return DFS_NAME_DIR; } public String getDFSdataDir() { return DFS_DATA_DIR; } public String getFSdefaultName() { return FS_DEFAULT_NAME; } public String getSolrServer() { return SOLR_SERVER; } public String getHBTbName() { return HBASE_TABLE_NAME; } public String getHBFamily() { return HBASE_TABLE_FAMILY; } }
引數配置檔案“config.properties”:
HBASE_ZOOKEEPER_QUORUM=slave-1,slave-2,slave-3,slave-4,slave-5 HBASE_ZOOKEEPER_PROPERTY_CLIENT_PORT=2181 HBASE_MASTER=master-1:60000 HBASE_ROOTDIR=hdfs:///hbase DFS_NAME_DIR=/opt/data/dfs/name DFS_DATA_DIR=/opt/data/d0/dfs2/data FS_DEFAULT_NAME=hdfs://192.168.1.10:9000 SOLR_SERVER=http://192.168.1.10:8983/solr HBASE_TABLE_NAME=hb_app_m_user_te HBASE_TABLE_FAMILY=d
三、結合Solr進行HBase資料的多條件查詢:
可以透過web頁面操作Solr索引,
查詢:
http://192.168.1.10:8983/solr/select?(time:201307 AND tetid:1 AND mgcvid:101 AND smaid:101 AND puid:102)
刪除所有索引:
http://192.168.1.10:8983/solr/update/?stream.body=&stream.contentType=text/xml;charset=utf-8&commit=true *:*
透過java客戶端結合Solr查詢HBase資料:
package com.ultrapower.hbase.solrhbase; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.client.Get; import org.apache.hadoop.hbase.client.HTable; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.util.Bytes; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServer; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.HttpSolrServer; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; public class QueryData { /** * @param args * @throws SolrServerException * @throws IOException */ public static void main(String[] args) throws SolrServerException, IOException { final Configuration conf; conf = HBaseConfiguration.create(); HTable table = new HTable(conf, "hb_app_m_user_te"); Get get = null; Listlist = new ArrayList (); String url = "http://192.168.1.10:8983/solr"; SolrServer server = new HttpSolrServer(url); SolrQuery query = new SolrQuery("time:201307 AND tetid:1 AND mgcvid:101 AND smaid:101 AND puid:102"); query.setStart(0); //資料起始行,分頁用 query.setRows(10); //返回記錄數,分頁用 QueryResponse response = server.query(query); SolrDocumentList docs = response.getResults(); System.out.println("文件個數:" + docs.getNumFound()); //資料總條數也可輕易獲取 System.out.println("查詢時間:" + response.getQTime()); for (SolrDocument doc : docs) { get = new Get(Bytes.toBytes((String) doc.getFieldValue("rowkey"))); list.add(get); } Result[] res = table.get(list); byte[] bt1 = null; byte[] bt2 = null; byte[] bt3 = null; byte[] bt4 = null; String str1 = null; String str2 = null; String str3 = null; String str4 = null; for (Result rs : res) { bt1 = rs.getValue("d".getBytes(), "3mpon".getBytes()); bt2 = rs.getValue("d".getBytes(), "3mponid".getBytes()); bt3 = rs.getValue("d".getBytes(), "amarpu".getBytes()); bt4 = rs.getValue("d".getBytes(), "amarpuid".getBytes()); if (bt1 != null && bt1.length>0) {str1 = new String(bt1);} else {str1 = "無資料";} //對空值進行new String的話會丟擲異常 if (bt2 != null && bt2.length>0) {str2 = new String(bt2);} else {str2 = "無資料";} if (bt3 != null && bt3.length>0) {str3 = new String(bt3);} else {str3 = "無資料";} if (bt4 != null && bt4.length>0) {str4 = new String(bt4);} else {str4 = "無資料";} System.out.print(new String(rs.getRow()) + " "); System.out.print(str1 + "|"); System.out.print(str2 + "|"); System.out.print(str3 + "|"); System.out.println(str4 + "|"); } table.close(); } }
小結:
透過測試發現,結合Solr索引可以很好的實現HBase的多條件查詢,同時還能解決其兩個難點:分頁查詢、資料總量統計。
實際場景中大多都是分頁查詢,分頁查詢返回的資料量很少,採用此種方案完全可以達到前端頁面毫秒級的實時響應;若有大批次的資料互動,比如涉及到資料匯出,實際上效率也是很高,十萬資料僅耗時10秒。
另外,如果真的將Solr納入使用,Solr以及HBase端都可以不斷進行最佳化,比如可以搭建Solr叢集,甚至可以採用SolrCloud基於hadoop的分散式索引服務。
總之,HBase不能多條件過濾查詢的先天性缺陷,在Solr的配合之下可以得到較好的彌補,難怪諸如新蛋科技、國美電商、蘇寧電商等網際網路公司以及眾多遊戲公司,都使用Solr來支援快速查詢。
----end
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29754888/viewspace-1299209/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- RANK函式基於條件的查詢函式
- Laravel 多條件查詢Laravel
- SQL多條件查詢SQL
- 多條件查詢---ssh版本
- [20190502]查詢條件不等於測試.txt
- mongodb條件查詢不等於MongoDB
- SpringBoot Jpa多條件查詢Spring Boot
- mysql like查詢 - 根據多個條件的模糊匹配查詢MySql
- mysql帶AND關鍵字的多條件查詢MySql
- mybatis多條件的模糊查詢解決方案MyBatis
- linq to sql的多條件動態查詢SQL
- SQL中多條件查詢括號的用途SQL
- mysql多條件過濾查詢之mysq高階查詢MySql
- MongoDB查詢條件MongoDB
- MongoDB條件查詢MongoDB
- mysql條件查詢MySql
- 等於NULL的查詢條件導致查詢結果不正確Null
- jsp+servlet+mysql多條件模糊查詢JSServletMySql
- 條件查詢JSPJS
- Spring data jpa 多表查詢(三:多對多關係動態條件查詢)Spring
- 寫一個“特殊”的查詢構造器 – (四、條件查詢:複雜條件)
- Azure 基礎:自定義 Table storage 查詢條件
- 查詢作為條件的SQLSQL
- Laravel 多條件查詢時粗心導致的一個 BUGLaravel
- golang beego orm 查詢條件 or andGolangORM
- 【mybatis-plus】條件查詢MyBatis
- sql 查詢條件問題SQL
- 查詢條件封裝物件封裝物件
- Javaweb-DQL-條件查詢JavaWeb
- 對於專案中簡單的多條件查詢的一些心得體會
- where語句中多條件查詢欄位NULL與NOT NULL不確定性查詢Null
- Mybatis學習筆記 3:Mybatis 多種條件查詢MyBatis筆記
- Laravel Eloquent ORM 多條件查詢,你會怎麼寫?LaravelORM
- jQuery製作淘寶商城商品列表多條件查詢功能jQuery
- EntityFramework動態多條件查詢與Lambda表示式樹Framework
- 複合條件查詢的重構
- oracle date資料的條件查詢Oracle
- SQL SERVER 條件語句的查詢SQLServer