架構(gòu)師必備:多維度查詢的優(yōu)秀實(shí)踐
背景
有2種常見(jiàn)的多維度查詢場(chǎng)景,分別是:
- 帶多個(gè)篩選條件的列表查詢
- 不含分庫(kù)分表列的其他維度查詢
普通的數(shù)據(jù)庫(kù)查詢,很難實(shí)現(xiàn)上述需求場(chǎng)景,更不用提模糊查詢、全文檢索了。
下面結(jié)合樓主的經(jīng)驗(yàn)和知識(shí),介紹初級(jí)方案、進(jìn)階方案(上ElasticSearch),大部分情況下推薦使用ElasticSearch來(lái)實(shí)現(xiàn)多維度查詢,趕時(shí)間的讀者可以直接跳到“進(jìn)階方案:將ElasticSearch添加到現(xiàn)有系統(tǒng)中”。
初級(jí)方案
1、根據(jù)常見(jiàn)查詢場(chǎng)景,增加相應(yīng)字段的組合索引
這個(gè)是為了實(shí)現(xiàn)帶多個(gè)篩選條件的列表查詢的。
優(yōu)點(diǎn)
- 非常簡(jiǎn)單
- 讀寫(xiě)不一致時(shí)間較短:取決于數(shù)據(jù)庫(kù)主從同步延時(shí),一般為毫秒級(jí)別
缺點(diǎn)
- 非常局限:除非篩選條件比較固定,否則難以應(yīng)付后續(xù)新增或修改篩選條件
- 如果每次來(lái)新的篩選查詢字段的需求,就新增索引,最終導(dǎo)致索引過(guò)于龐大,影響性能
于是就出現(xiàn)了經(jīng)典的一幕:產(chǎn)品提需求說(shuō)要支持某個(gè)新字段的篩選查詢,開(kāi)發(fā)反饋說(shuō)做不了、或者成本很高,于是不了了之 :)
2、異構(gòu)出多份數(shù)據(jù)
更加優(yōu)雅的方式,是異構(gòu)出多份數(shù)據(jù)。
例如,C端按用戶維度查詢,B端按店鋪維度查詢,如果還有供應(yīng)商,按供應(yīng)商維度查詢。一個(gè)數(shù)據(jù)庫(kù)只能按一種維度來(lái)分庫(kù)。
(1)程序?qū)懭攵鄠€(gè)數(shù)據(jù)源
優(yōu)點(diǎn)是:非常簡(jiǎn)單。
缺點(diǎn)
- 跨庫(kù)寫(xiě)存在一致性問(wèn)題(除非不同維度的表使用公共的分庫(kù),事務(wù)寫(xiě)入),性能低
- 不能靈活支持更多其他維度的查詢
(2)借助Canal實(shí)現(xiàn)數(shù)據(jù)的自動(dòng)同步
通過(guò)Canal同步數(shù)據(jù),異構(gòu)出多個(gè)維度的數(shù)據(jù)源。詳見(jiàn)之前寫(xiě)的這篇文章: 架構(gòu)師必備:巧用Canal實(shí)現(xiàn)異步、解耦的架構(gòu)
優(yōu)點(diǎn)是:更加優(yōu)雅,無(wú)需改動(dòng)程序主流程。
缺點(diǎn)
- 仍然無(wú)法解決不斷變化的需求,不可能為了支持新維度就異構(gòu)出一份新數(shù)據(jù)
進(jìn)階方案:將ElasticSearch添加到現(xiàn)有系統(tǒng)中
應(yīng)用架構(gòu)
現(xiàn)有系統(tǒng)一般都會(huì)用到MySQL數(shù)據(jù)庫(kù),需要引入ES,為系統(tǒng)增強(qiáng)多維度查詢的功能。
MySQL繼續(xù)承擔(dān)業(yè)務(wù)的實(shí)時(shí)讀寫(xiě)請(qǐng)求、事務(wù)操作,ES承擔(dān)近實(shí)時(shí)的多維度查詢請(qǐng)求,ES可支撐十萬(wàn)級(jí)別qps(取決于節(jié)點(diǎn)數(shù)、分片數(shù)、副本數(shù))。
需要注意的是:同步數(shù)據(jù)至ES是秒級(jí)延遲(主要耗費(fèi)在索引refresh),而查詢已進(jìn)入索引的文檔,是在數(shù)毫秒到數(shù)百毫秒級(jí)別。
導(dǎo)入數(shù)據(jù)
需要同步機(jī)制,來(lái)把MySQL中的數(shù)據(jù)導(dǎo)入到ES中,主要流程如下:
- 預(yù)先定義ES索引的mapping配置,而不依賴ES自動(dòng)生成mapping
- 初始全量導(dǎo)入,后續(xù)增量導(dǎo)入:Canal+MQ數(shù)據(jù)管道同步,不需要或僅需少量代碼工作
- 數(shù)據(jù)過(guò)濾:不導(dǎo)入無(wú)需檢索的字段,減小索引大學(xué),提高性能
- 數(shù)據(jù)扁平化處理:如果數(shù)據(jù)庫(kù)中有json字段列,需要從中提取業(yè)務(wù)字段,避免嵌套類型的字段,提高性能
查詢數(shù)據(jù)
- 從ES 8.x版本開(kāi)始,建議使用Java api client,并且要Java 8及以上環(huán)境,因?yàn)榭墒褂酶鞣Nlambda函數(shù),來(lái)提高代碼可讀性
- 優(yōu)點(diǎn)是新客戶端與server代碼完全耦合(相比于原Java transport client,在8.x版本已廢棄),并且API風(fēng)格與http rest api很接近(相比于原Java rest client,在8.x版本已廢棄),只要熟練掌握http json請(qǐng)求體寫(xiě)法,即可快速上手。
- 底層使用的還是原來(lái)的low level rest client,實(shí)現(xiàn)了http長(zhǎng)連接、訪問(wèn)ES各節(jié)點(diǎn)的負(fù)載均衡、故障轉(zhuǎn)移,最底層依賴的是apache http async client。
- ES 7.x版本及以下,或使用Java 7及以下,建議升級(jí),否則就只能繼續(xù)用high level rest client。
代碼示例如下(含詳細(xì)注釋):
public class EsClientDemo {
// demo演示:創(chuàng)建client,然后搜索
public void createClientAndSearch() throws Exception {
// 創(chuàng)建底層的low level rest client,連接ES節(jié)點(diǎn)的9200端口
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200)).build();
// 創(chuàng)建transport類,傳入底層的low level rest client,和json解析器
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
// 創(chuàng)建核心client類,后續(xù)操作都圍繞此對(duì)象
ElasticsearchClient esClient = new ElasticsearchClient(transport);
// 多條件搜索
// fluent API風(fēng)格,并且使用lambda函數(shù)提高代碼可讀性,可以看出Java api client的語(yǔ)法,同http json請(qǐng)求體非常相似
String searchText = "bike";
String brand = "brandNew";
double maxPrice = 1000;
// 根據(jù)商品名稱,做match全文檢索查詢
Query byName = MatchQuery.of(m -> m
.field("name")
.query(searchText)
)._toQuery();
// 根據(jù)品牌,做term精確查詢
Query byBrand = new Query.Builder()
.term(t -> t
.field("brand")
.value(v -> v.stringValue(brand))
).build();
// 根據(jù)價(jià)格,做range范圍查詢
Query byMaxPrice = RangeQuery.of(r -> r
.field("price")
.lte(JsonData.of(maxPrice))
)._toQuery();
// 調(diào)用核心client,做查詢
SearchResponse<Product> response = esClient.search(s -> s
.index("products") // 指定ES索引
.query(q -> q // 指定查詢DSL
.bool(b -> b // 多條件must組合,必須同時(shí)滿足
.must(byName)
.must(byBrand)
.must(byMaxPrice)
)
),
Product.class
);
// 遍歷命中結(jié)果
List<Hit<Product>> hits = response.hits().hits();
for (Hit<Product> hit: hits) {
Product product = hit.source(); // 通過(guò)source獲取結(jié)果
logger.info("Found product " + product.getName() + ", score " + hit.score());
}
}
}
可參閱: ??https://www.elastic.co/guide/en/elasticsearch/client/index.html??
數(shù)據(jù)模型轉(zhuǎn)換
因?yàn)榧扔蠱ySQL,又有ES,所以有2種異構(gòu)的數(shù)據(jù)模型。需要在代碼中定義2種數(shù)據(jù)模型,并且實(shí)現(xiàn)類型互相轉(zhuǎn)換的工具類。
- MySQL數(shù)據(jù)VO
- ES數(shù)據(jù)VO
- MySQL數(shù)據(jù)VO、ES數(shù)據(jù)VO互相轉(zhuǎn)換工具
- 業(yè)務(wù)層BO
- 接口DTO
原理概要
ES之所以比MySQL,能勝任多維度查詢、全文檢索,是因?yàn)榈讓訑?shù)據(jù)結(jié)構(gòu)不同:
- ES倒排索引
- 如果是全文檢索字段:會(huì)先分詞,然后生成 term -> document 的倒排索引,查詢時(shí)也會(huì)把query分詞,然后檢索出相關(guān)的文檔。相關(guān)度算法如TF-IDF(term frequency–inverse document frequency),取決于:詞在該文檔中出現(xiàn)的頻率(TF,term frequency),越高代表越相關(guān);以及詞在所有文檔中出現(xiàn)的頻率(IDF,inverse document frequency),越高代表越不相關(guān),相當(dāng)于是一個(gè)通用的詞,對(duì)相關(guān)性影響較小。
- 如果是精確值字段:則無(wú)需分詞,直接把query作為一個(gè)整體的term,查詢對(duì)應(yīng)文檔。
- 因?yàn)槲臋n中的所有字段,都生成了倒排索引,所以能處理多維度組合查詢
- MySQL B+樹(shù)
- B+樹(shù)的非葉子節(jié)點(diǎn)記錄了孩子節(jié)點(diǎn)值的范圍,而葉子節(jié)點(diǎn)記錄了真正的一組值,并且在同一層,形成了一個(gè)有序鏈表
- 組合索引需要顯式創(chuàng)建:選擇需索引的字段、并且順序是重要的,所以如果待查詢的字段不在索引中,就無(wú)法高效查詢,可能演變?yōu)槿頀呙?/span> (對(duì)聚簇索引的葉子節(jié)點(diǎn)做一次遍歷)
另外簡(jiǎn)要回顧一下ES的架構(gòu)要點(diǎn):
- 節(jié)點(diǎn)分為主節(jié)點(diǎn)、數(shù)據(jù)節(jié)點(diǎn),一個(gè)節(jié)點(diǎn)上可以有多個(gè)分片,分片分為主分片、副本分片,1對(duì)多,主分片與副本分片分布在不同的節(jié)點(diǎn),來(lái)實(shí)現(xiàn)高可用
- 主分片數(shù)在創(chuàng)建時(shí),就需要指定,在創(chuàng)建后不能隨意更改(如果變化,路由就會(huì)出錯(cuò));而副本分片可以增加,來(lái)提高ES集群的查詢QPS
- 路由算法:id % 主分片數(shù),如果創(chuàng)建文檔時(shí)不指定id,則ES會(huì)自動(dòng)生成;一般會(huì)傳自定義業(yè)務(wù)id
優(yōu)點(diǎn)、缺點(diǎn)
優(yōu)點(diǎn)
- 支持各字段的多維度組合查詢,無(wú)懼未來(lái)新增字段(主要成本在于新增字段后、重建索引)
- 與現(xiàn)有系統(tǒng)完全解耦,適合架構(gòu)演進(jìn)
- 在數(shù)據(jù)量級(jí)上遠(yuǎn)勝M(fèi)ysql,最大支持PB級(jí)數(shù)據(jù)的存儲(chǔ)和查詢
缺點(diǎn)
- 讀寫(xiě)不一致時(shí)間在秒級(jí):因?yàn)橛?個(gè)耗時(shí)階段,一是同步階段將數(shù)據(jù)從MySQL數(shù)據(jù)庫(kù)寫(xiě)入ES,二是ES索引refresh階段,數(shù)據(jù)從buffer寫(xiě)入索引后才可查到
- 因此一個(gè)trick就是,在寫(xiě)入操作后,前端延遲調(diào)用后端的列表查詢接口,比如延遲1秒后再展示
- 超高并發(fā)下存在瓶頸,存在穩(wěn)定性問(wèn)題:目前原生版本支持大約 3-5 萬(wàn)分片,性能已經(jīng)到達(dá)極限,創(chuàng)建索引基本到達(dá) 30 秒+ 甚至分鐘級(jí)。節(jié)點(diǎn)數(shù)只能到 500 左右基本是極限了。但依然能滿足絕大部分場(chǎng)景。數(shù)據(jù)來(lái)源: ??https://elasticsearch.cn/slides/259#page=30??
ES最佳實(shí)踐
- 只把需要搜索的數(shù)據(jù)導(dǎo)入ES,避免索引過(guò)大
- 數(shù)據(jù)扁平化,不用嵌套結(jié)構(gòu),提高性能
- 合理設(shè)置字段類型,預(yù)先定義mapping配置,而不依賴ES自動(dòng)生成mapping
- 精確值的類型指定為keyword(mapping配置),并且使用term查詢
- 精確值是指無(wú)需進(jìn)行range范圍查詢的字段,既可以是字符串,比如書(shū)的作者名字,也可以是數(shù)值,比如商品id、訂單id、圖書(shū)ISBN編號(hào)、枚舉值。在使用中,大部分場(chǎng)景是以id類作為精確值
- 避免無(wú)路由查詢:無(wú)路由查詢會(huì)并發(fā)在多個(gè)索引上查詢、歸并排序結(jié)果,會(huì)使得集群cpu飆升,影響穩(wěn)定性
- 避免深度分頁(yè)查詢:如有大量數(shù)據(jù)查詢,推薦用scroll滾動(dòng)查詢
- 設(shè)置合理的文件系統(tǒng)緩存(filesytem cache)大小,提高性能:因?yàn)镋S查詢的熱數(shù)據(jù)在文件系統(tǒng)緩存中
- ES分片數(shù)在創(chuàng)建后不能隨意改動(dòng),但是副本數(shù)可以隨時(shí)增加,來(lái)提高最大QPS。如果單個(gè)分片壓力過(guò)大,需要擴(kuò)容。
更進(jìn)一步
前面提到ES超高并發(fā)下存在瓶頸,極端情況下可能遇到OOM,因此超高并發(fā)下需要C++實(shí)現(xiàn)的專用搜索引擎
例如:
- 百度:通用搜索引擎,根據(jù)文字、圖片搜索信息
- 電商垂類:電商專用搜索引擎,比如根據(jù)關(guān)鍵詞查找商品,或根據(jù)品牌、價(jià)格篩選商品,可總結(jié)為商品的搜索、廣告、推薦