摘要前言 DT時(shí)代對(duì)平臺(tái)或商家來(lái)說(shuō)最有價(jià)值的就是數(shù)據(jù)了,在大數(shù)據(jù)時(shí)代數(shù)據(jù)呈現(xiàn)出數(shù)據(jù)量大,數(shù)據(jù)的維度多的特點(diǎn),用戶會(huì)使用多維度隨意組合條件快速召回?cái)?shù)據(jù)。數(shù)據(jù)處理業(yè)務(wù)場(chǎng)景需要實(shí)時(shí)性,需要能夠快速精準(zhǔn)的獲得到需要的數(shù)據(jù)。之前的通過(guò)數(shù)據(jù)庫(kù)的方式來(lái)處理數(shù)據(jù)的方式,由于數(shù)據(jù)庫(kù)的某些固有特性已經(jīng)很難滿足大數(shù)據(jù)時(shí)代對(duì)
DT時(shí)代對(duì)平臺(tái)或商家來(lái)說(shuō)最有價(jià)值的就是數(shù)據(jù)了,在大數(shù)據(jù)時(shí)代數(shù)據(jù)呈現(xiàn)出數(shù)據(jù)量大,數(shù)據(jù)的維度多的特點(diǎn),用戶會(huì)使用多維度隨意組合條件快速召回?cái)?shù)據(jù)。數(shù)據(jù)處理業(yè)務(wù)場(chǎng)景需要實(shí)時(shí)性,需要能夠快速精準(zhǔn)的獲得到需要的數(shù)據(jù)。之前的通過(guò)數(shù)據(jù)庫(kù)的方式來(lái)處理數(shù)據(jù)的方式,由于數(shù)據(jù)庫(kù)的某些固有特性已經(jīng)很難滿足大數(shù)據(jù)時(shí)代對(duì)數(shù)據(jù)處理的需求。
所以,在大數(shù)據(jù)時(shí)代使用hadoop,hive,spark,作為處理離線大數(shù)據(jù)的補(bǔ)充手段已經(jīng)大行其道。
以上提到的這些數(shù)據(jù)處理手段,只能離線數(shù)據(jù)處理方式,無(wú)法實(shí)現(xiàn)實(shí)時(shí)性。Solr作為補(bǔ)充,能夠很好地解決大數(shù)據(jù)的多維度查詢和數(shù)據(jù)召回實(shí)時(shí)性要求。
本文通過(guò)分析阿里淘寶聚石塔環(huán)境中遇到的一個(gè)具體需求是如何實(shí)現(xiàn)的,通過(guò)這個(gè)例子,拋磚引玉來(lái)體現(xiàn)SORL在數(shù)據(jù)處理上的優(yōu)勢(shì)。
阿里聚石塔是銜接淘寶大賣家,軟件開(kāi)發(fā)者和平臺(tái)提供者這三者的生態(tài)圈,阿里通過(guò)聚石塔平臺(tái),將阿里云底層的PAAS,IAAS環(huán)境提供給第三方開(kāi)發(fā)者,而第三方開(kāi)發(fā)者可以通過(guò)自己開(kāi)發(fā)的軟件產(chǎn)品,比如ERP,CRM系統(tǒng)販賣給淘寶上的大賣家,提高大賣家的工作效率。
賣家的交易數(shù)據(jù)是最有價(jià)值的數(shù)據(jù),通過(guò)交易數(shù)據(jù)可以衍生出很多產(chǎn)品,例如管理交易的ERP軟件,會(huì)員營(yíng)銷工具CRM,在聚石塔環(huán)境中通過(guò)大賣家授權(quán),這部分?jǐn)?shù)據(jù)可以授權(quán)給獨(dú)立軟件開(kāi)發(fā)者ISV。
在CRM系統(tǒng)中需要能夠通過(guò)設(shè)置買家的行為屬性快速過(guò)濾出有價(jià)值的買家記錄,進(jìn)行精準(zhǔn)會(huì)員營(yíng)銷。
以下是兩個(gè)具體需求,首先看兩個(gè)線框圖:
以上是賣家需要實(shí)時(shí)篩選一段時(shí)間內(nèi)購(gòu)買數(shù)量在一個(gè)區(qū)間之內(nèi)的買家。
再看一個(gè)線框圖:
賣家需要實(shí)時(shí)搜索一個(gè)時(shí)間段內(nèi),消費(fèi)金額在某個(gè)區(qū)間之內(nèi)的買家會(huì)員。這里的區(qū)間是以天為單位的,時(shí)間跨度可長(zhǎng)可短。
了解了線框圖之后,我們還要再看看對(duì)應(yīng)的數(shù)據(jù)庫(kù)ER圖:
表結(jié)構(gòu)相當(dāng)簡(jiǎn)單,只有兩張表,稍微有點(diǎn)經(jīng)驗(yàn)的開(kāi)發(fā)工程師就會(huì)寫出以下SQL:
select buyer.buyer_id,count(trade.trade_id) as pay_countFrom buyer inner join trade on(buyer.buyer_id = trade.buyer_id and buyer.seller_id = trade.seller_id)where trade.trade_time> ? and trade.trade_time < ? and buyer.seller_id=?group by buyer.buyer_idhaving pay_count > =1 AND pay_count <=5
第二個(gè)線框圖會(huì)用以下SQL語(yǔ)句來(lái)實(shí)現(xiàn):
select buyer.buyer_id , sum(trade.fee) as pay_sumFrom buyer inner join trade on( buyer.buyer_id = trade.buyer_id and buyer.seller_id = trade.seller_id)where trade.trade_time> ? AND trade.trade_time < ? and buyer.seller_id=?group by buyer.buyer_idhaving pay_sum > =20 and pay_sum <=100
以上,兩個(gè)SQL語(yǔ)句大同小異,having部分稍有不同, SQL語(yǔ)句并不算復(fù)雜,但是在大數(shù)據(jù)情況下,無(wú)法在毫秒級(jí)反饋給用戶。另外,假如where部分有其他查詢條件,比如,買家的性別,買家所屬的地區(qū)等,就需要數(shù)據(jù)庫(kù)上設(shè)置更多的聯(lián)合索引,所以這個(gè)需求使用SQL語(yǔ)句根本無(wú)法實(shí)現(xiàn)的。
問(wèn)題已經(jīng)明確,那么解決的辦法是什么呢?是使用數(shù)據(jù)的存儲(chǔ)過(guò)程?存儲(chǔ)過(guò)程底層還是依賴數(shù)據(jù)庫(kù)表的固有特性,無(wú)非是提供一些以時(shí)間換空間的策略來(lái)實(shí)現(xiàn)罷了,換湯不換藥,而且各個(gè)數(shù)據(jù)庫(kù)產(chǎn)品的存儲(chǔ)過(guò)程實(shí)現(xiàn)很很大差別,一旦選擇了某一個(gè)數(shù)據(jù)的存儲(chǔ)過(guò)程之后以后再要遷移數(shù)據(jù)到其他數(shù)據(jù)平臺(tái)上就非常困難了。
這里要向大家隆重介紹搜索引擎Solr。因?yàn)椋阉饕嬖诘讓邮褂玫古潘饕?,這和數(shù)據(jù)庫(kù)有本質(zhì)區(qū)別,倒排索引在數(shù)據(jù)查詢的性能上天生就比數(shù)據(jù)的Btree樹(shù)好上百倍,具體原因不在這里展開(kāi)了。雖然某些數(shù)據(jù)庫(kù)也支持了倒排索引例如PG,但畢竟不是通用的解決辦法。一旦添加了這類型的索引會(huì)影響數(shù)據(jù)的寫入吞吐量,因?yàn)橹亟ㄋ饕浅:臅r(shí)間。
開(kāi)源JAVA社區(qū)中使用最廣泛的應(yīng)該屬Solr了,筆者所在的團(tuán)隊(duì)就是長(zhǎng)期研究將Solr應(yīng)用到企業(yè)級(jí)應(yīng)用場(chǎng)景中,在原生Solr之上做了很多優(yōu)化和適配,方便企業(yè)級(jí)用戶使用。
言歸正傳,先講講大致思路,實(shí)現(xiàn)的架構(gòu)圖如下:
這里要說(shuō)明的一點(diǎn),發(fā)送到搜索引擎中的數(shù)據(jù)是一條寬表數(shù)據(jù),所謂寬表數(shù)據(jù)是將ER關(guān)系為1對(duì)N的實(shí)體,聚合成一條記錄。聚合方式有兩種,一種是向1的維度聚合,比如用戶實(shí)體和消費(fèi)記錄實(shí)體,寬表記錄如果是以用戶維度來(lái)聚合和話,就會(huì)將所有的消費(fèi)記錄以某個(gè)特殊字符作為分割符,聚合成一個(gè)字段,作為用戶記錄的一個(gè)冗余字段。也可以以消費(fèi)記錄為維度聚合,將關(guān)聯(lián)的用戶信息作為一個(gè)冗余字段,可想而知這樣的聚合方式用戶數(shù)據(jù)在索引數(shù)據(jù)中會(huì)有很多重復(fù)。
打?qū)挶磉@個(gè)環(huán)節(jié)看似和搜索不怎么相關(guān),但是合理的寬表數(shù)據(jù)結(jié)構(gòu)能大幅度地提高用戶數(shù)據(jù)查詢效率。
全量流程用Hive來(lái)實(shí)現(xiàn)的,如果是在阿里云公有云環(huán)境中可以用ODPS,因ODPS是PAAS服務(wù)。
增量通道,需要寫一個(gè)打?qū)挶聿僮?。因?yàn)樗阉饕嫣赜械慕Y(jié)構(gòu),增量同步更新持續(xù)一段時(shí)間之后會(huì)生成很多索引碎片,所以必須要隔一段時(shí)間從數(shù)據(jù)源重新導(dǎo)出并構(gòu)建一次全量索引數(shù)據(jù)。
這里介紹一下上面提交到用戶-消費(fèi)記錄的寬表結(jié)構(gòu)(簡(jiǎn)單起見(jiàn),去掉了表中和問(wèn)題域不相關(guān)的字段):
買家id | 賣家id |
---|---|
Buyer_id | Seller_id |
買家id | 賣家id | 交易id | 交易時(shí)間 | 單筆費(fèi)用 |
---|---|---|---|---|
Buyer_id | Seller_id | trade_id | trade_time | Fee |
買家id | 賣家id | dynamic_info(聚合字段) |
---|---|---|
Buyer_id | Seller_id | sellerId_date_buyerId_payment_payCount[;sellerId_date_buyerId_payment_payCount] |
這里需要對(duì)dynamic_info 聚合字段詳細(xì)說(shuō)明一下:
sellerId_date_buyerId_payment_payCount 這是一個(gè)聚合單元,從左向右依次的含義是:賣家ID,購(gòu)買的日期(精確到天),買家ID,購(gòu)買天之內(nèi)的費(fèi)用總和,購(gòu)買天之內(nèi)的購(gòu)買次數(shù)總和。
Dynamic_info字段可以有多個(gè)聚合單元組成,每個(gè)單元中的date是按天去重的,假如一個(gè)用戶在某一天在一家店中有多條購(gòu)買記錄最終也會(huì)聚合成一個(gè)單元。給一個(gè)聚合字段的實(shí)際示例:
Dynamic_info:9999_20151111_222_345.6_3;9999_20151212_222_627.5_1
這個(gè)字段的意義就是,一個(gè)id為222的用戶在2015年雙11當(dāng)天購(gòu)買了3筆價(jià)值345元的商品,在雙12當(dāng)天在這個(gè)商家處又購(gòu)買了一筆價(jià)值627.5元的商品。
之所以在Solr上進(jìn)行快速數(shù)據(jù)查詢的原因是,Solr的數(shù)據(jù)源是一個(gè)已經(jīng)聚合好的一份數(shù)據(jù),數(shù)據(jù)庫(kù)上執(zhí)行的join操作會(huì)耗費(fèi)大量IO,在Solr查詢省去了這部分時(shí)間。
寬表數(shù)據(jù)從多個(gè)分表聚合,數(shù)據(jù)的語(yǔ)義沒(méi)有變化,只是組織形式發(fā)生了變化,如果一個(gè)SAAS的服務(wù)提供上同時(shí)為十幾萬(wàn)個(gè)大賣家提供篩選服務(wù),而每個(gè)大賣家又積累的交易數(shù)據(jù)是非常大的,全部加在一起,要將數(shù)據(jù)進(jìn)行聚合化操作,有非常大的CPU和IO開(kāi)銷,好在在云服務(wù)時(shí)代有強(qiáng)大的離線計(jì)算工具如hadoop,ODPS可以將大數(shù)據(jù)如同肉面粉一般揉(處理)成任何你想要的結(jié)構(gòu),分分鐘不在話下。
準(zhǔn)備好全量源數(shù)據(jù),之后就是將其轉(zhuǎn)化為L(zhǎng)ucene的索引文件了,這個(gè)過(guò)程請(qǐng)查閱Solr Wiki便可,這里不進(jìn)行闡述。這里要重點(diǎn)描述的是Solr服務(wù)端如何響應(yīng)用戶的查詢請(qǐng)求,返回給用戶需要的查詢結(jié)果。
處理用戶在時(shí)間段內(nèi)購(gòu)買量或購(gòu)買額度進(jìn)行過(guò)濾,需要構(gòu)建一個(gè)QParser的插件,這個(gè)插件的作用是遍歷和查參數(shù)中匹配的條件項(xiàng)生成命中的DocSet命中結(jié)果集。
下面是QparserPlugin.java節(jié)選:
for (LeafReaderContext leaf : readerContext.leaves()) { docBase = leaf.docBase; reader = leaf.reader(); liveDocs = reader.getLiveDocs(); terms = reader.terms("dynamic_info"); termEnum = terms.iterator(); String prefixStart = sellerId + "_" + startTime; String prefixEnd = sellerId + "_" + endTime; String termStr = null; int docid = -1; if ((termEnum.seekCeil(new BytesRef(prefixStart))) != SeekStatus.END) { do { Matcher matcher = DYNAMIC_INFO .matcher(termStr = termEnum.term() .utf8ToString()); if (!matcher.matches()) { continue; } posting = termEnum.postings(posting); docid = posting.nextDoc();if (!(docid != PostingsEnum.NO_MORE_DOCS && (liveDocs == null || (liveDocs != null && liveDocs.get(docid))))) { continue; } if ((matcher.group(1) + "_" + matcher.group(2)) .compareTo(prefixEnd) > 0) { break; } addStatis(buyerStatis, docBase, docid, matcher); } while (termEnum.next() != null); } }
以上代碼的執(zhí)行邏輯是,截取prefixStart和prefixEnd之間的term序列,進(jìn)行分析如果符合過(guò)濾條件就將對(duì)應(yīng)docid插入buyerStatis收集器中。
等第一輪數(shù)據(jù)處理過(guò)程中就在對(duì)聚合結(jié)果進(jìn)行增量累加,代碼如下:
private static StaticReduce addStatis( Map<Integer, StaticReduce> buyerStatis, int docBase, int docid, Matcher matcher) { StaticReduce statis = buyerStatis.get(docBase + docid); if (statis == null) { statis = new StaticReduce(docBase + docid, Long.parseLong(matcher .group(3))/* buyerid */); buyerStatis.put(docBase + docid, statis); } if (statis.buyerId != Long.parseLong(matcher.group(3))) { return statis; } try { statis.addPayCount(Integer.parseInt(matcher.group(5))); } catch (Exception e) { } try { statis.addPayment(Float.parseFloat(matcher.group(4))); } catch (Exception e) { } return statis; }
同時(shí)對(duì)購(gòu)買數(shù)量,和購(gòu)買金額進(jìn)行累加。
最后對(duì)累加結(jié)果進(jìn)行過(guò)濾,符合過(guò)濾條件的,將docid插入到bitset中:
for (StaticReduce statis : buyerStatis.values()) {
// TODO 這里自己判斷是否要收集這條記錄 if (statis.payCount > Integer.MAX_VALUE
|| statis.paymentSum > 1) { System.out.println("count:" + statis.payCount + ",sum:"
+ statis.paymentSum);
bitSet.set(statis.luceneDocId);
}
}BitDocIdSet docIdSet = new BitDocIdSet(bitSet); DocIdSetIterator it = docIdSet.iterator();
final BitQuery bitquery = new BitQuery(it); return new QParser(qstr, localParams, params, req) {
@Override
public Query parse() throws SyntaxError { return bitquery;
}
};
最后將bitSet包裝成BitQuery作為Qparser的parse函數(shù)的返回值,返回有solr進(jìn)一步和其他結(jié)果集進(jìn)行過(guò)濾。
需要將以上的QparserPlugin插件注入到solr中,需要在solrconfig中寫以下配置:
<queryParser name="timesegstats" class ="com.xxx.qp.TimeSegStatsQParserPlugin" > <str name="buyerField">buyer_id</str> <str name="compoundField">dynamic_info </str> <str name="countField">emailSendCount</str> <str name="statsFields"></str> </queryParser>
Solr查詢語(yǔ)句Q參數(shù)設(shè)置:
q={!multiqp q.op=AND}seller_id:1441097932588 AND {!timesegstats sellerId=1441097932588 statsField=buyActivity startTime=20150901 endTime=20150924 startValue=1 endValue=200} AND {!timesegstats sellerId=1441097932588 statsField=paycount startTime=20140901 endTime=20150924 startValue=2 endValue=100}
以上是一個(gè)用Solr搜索引擎解決數(shù)據(jù)庫(kù)查詢瓶頸的實(shí)例,其實(shí)搜索引擎的使用場(chǎng)景非常廣泛,不僅可以用在像百度這樣的大規(guī)模非結(jié)構(gòu)化的數(shù)據(jù)查詢,可以定制比較復(fù)雜的排序規(guī)則。Solr更可以解決像本文講到的數(shù)據(jù)庫(kù)加速的場(chǎng)景,使得原本在數(shù)據(jù)庫(kù)上沒(méi)有無(wú)法實(shí)現(xiàn)的SQL查詢,可以通過(guò)Solr搜索引擎上輕松實(shí)現(xiàn)。
本文講到的需求,也可以使用像hive這樣的離線處理工具來(lái)實(shí)現(xiàn),每次處理完成后將結(jié)果再導(dǎo)入到mysql中,業(yè)務(wù)端通過(guò)讀取數(shù)據(jù)庫(kù)表中的數(shù)據(jù)來(lái)向用戶展示處理結(jié)果。這樣做雖然可行,但是,沒(méi)有辦法將處理結(jié)果的實(shí)時(shí)性沒(méi)有辦法保證,而且,離線處理結(jié)果的數(shù)據(jù)結(jié)構(gòu)是固化的,沒(méi)有辦法做到將處理結(jié)果靈活調(diào)整。而用Solr做到數(shù)據(jù)的查詢出口,可以很好地解決以上兩個(gè)問(wèn)題。