架構(gòu)師必備:多維度查詢的最佳實踐

背景

有2種常見的多維度查詢場景,分別是:

  • 帶多個篩選條件的列表查詢
  • 不含分庫分表列的其他維度查詢

普通的數(shù)據(jù)庫查詢,很難實現(xiàn)上述需求場景民镜,更不用提模糊查詢弄唧、全文檢索了适肠。

下面結(jié)合樓主的經(jīng)驗和知識,介紹初級方案候引、進階方案(上ElasticSearch)侯养,大部分情況下推薦使用ElasticSearch來實現(xiàn)多維度查詢,趕時間的讀者可以直接跳到“進階方案:將ElasticSearch添加到現(xiàn)有系統(tǒng)中”澄干。

初級方案

1逛揩、根據(jù)常見查詢場景,增加相應(yīng)字段的組合索引

這個是為了實現(xiàn)帶多個篩選條件的列表查詢的麸俘。

優(yōu)點

  • 非常簡單
  • 讀寫不一致時間較短:取決于數(shù)據(jù)庫主從同步延時息尺,一般為毫秒級別

缺點

  • 非常局限:除非篩選條件比較固定,否則難以應(yīng)付后續(xù)新增或修改篩選條件
  • 如果每次來新的篩選查詢字段的需求疾掰,就新增索引搂誉,最終導(dǎo)致索引過于龐大,影響性能

于是就出現(xiàn)了經(jīng)典的一幕:產(chǎn)品提需求說要支持某個新字段的篩選查詢静檬,開發(fā)反饋說做不了炭懊、或者成本很高,于是不了了之 :)

2拂檩、異構(gòu)出多份數(shù)據(jù)

更加優(yōu)雅的方式侮腹,是異構(gòu)出多份數(shù)據(jù)。
例如稻励,C端按用戶維度查詢父阻,B端按店鋪維度查詢,如果還有供應(yīng)商望抽,按供應(yīng)商維度查詢加矛。一個數(shù)據(jù)庫只能按一種維度來分庫。

(1)程序?qū)懭攵鄠€數(shù)據(jù)源

優(yōu)點是:非常簡單煤篙。

缺點

  • 跨庫寫存在一致性問題(除非不同維度的表使用公共的分庫斟览,事務(wù)寫入),性能低
  • 不能靈活支持更多其他維度的查詢

(2)借助Canal實現(xiàn)數(shù)據(jù)的自動同步

通過Canal同步數(shù)據(jù)辑奈,異構(gòu)出多個維度的數(shù)據(jù)源苛茂。詳見之前寫的這篇文章:架構(gòu)師必備:巧用Canal實現(xiàn)異步绑嘹、解耦的架構(gòu)

image.png

優(yōu)點是:更加優(yōu)雅上鞠,無需改動程序主流程中鼠。

缺點

  • 仍然無法解決不斷變化的需求色难,不可能為了支持新維度就異構(gòu)出一份新數(shù)據(jù)

進階方案:將ElasticSearch添加到現(xiàn)有系統(tǒng)中

應(yīng)用架構(gòu)

現(xiàn)有系統(tǒng)一般都會用到MySQL數(shù)據(jù)庫,需要引入ES躁绸,為系統(tǒng)增強多維度查詢的功能裕循。
MySQL繼續(xù)承擔(dān)業(yè)務(wù)的實時讀寫請求、事務(wù)操作涨颜,ES承擔(dān)近實時的多維度查詢請求费韭,ES可支撐十萬級別qps(取決于節(jié)點數(shù)茧球、分片數(shù)庭瑰、副本數(shù))。
需要注意的是:同步數(shù)據(jù)至ES是秒級延遲(主要耗費在索引refresh)抢埋,而查詢已進入索引的文檔弹灭,是在數(shù)毫秒到數(shù)百毫秒級別。

image.png

導(dǎo)入數(shù)據(jù)

需要同步機制揪垄,來把MySQL中的數(shù)據(jù)導(dǎo)入到ES中穷吮,主要流程如下:

  • 預(yù)先定義ES索引的mapping配置,而不依賴ES自動生成mapping
  • 初始全量導(dǎo)入饥努,后續(xù)增量導(dǎo)入:Canal+MQ數(shù)據(jù)管道同步捡鱼,不需要或僅需少量代碼工作
  • 數(shù)據(jù)過濾:不導(dǎo)入無需檢索的字段,減小索引大學(xué)酷愧,提高性能
  • 數(shù)據(jù)扁平化處理:如果數(shù)據(jù)庫中有json字段列驾诈,需要從中提取業(yè)務(wù)字段,避免嵌套類型的字段溶浴,提高性能
image.png

查詢數(shù)據(jù)

  • 從ES 8.x版本開始乍迄,建議使用Java api client,并且要Java 8及以上環(huán)境士败,因為可使用各種lambda函數(shù)闯两,來提高代碼可讀性

    • 優(yōu)點是新客戶端與server代碼完全耦合(相比于原Java transport client,在8.x版本已廢棄)谅将,并且API風(fēng)格與http rest api很接近(相比于原Java rest client漾狼,在8.x版本已廢棄),只要熟練掌握http json請求體寫法饥臂,即可快速上手邦投。
    • 底層使用的還是原來的low level rest client,實現(xiàn)了http長連接擅笔、訪問ES各節(jié)點的負(fù)載均衡志衣、故障轉(zhuǎn)移屯援,最底層依賴的是apache http async client。
  • ES 7.x版本及以下念脯,或使用Java 7及以下狞洋,建議升級,否則就只能繼續(xù)用high level rest client绿店。

代碼示例如下(含詳細注釋):

public class EsClientDemo {

    // demo演示:創(chuàng)建client吉懊,然后搜索
    public void createClientAndSearch() throws Exception {
        // 創(chuàng)建底層的low level rest client,連接ES節(jié)點的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ù)操作都圍繞此對象
        ElasticsearchClient esClient = new ElasticsearchClient(transport);


        // 多條件搜索
        // fluent API風(fēng)格转培,并且使用lambda函數(shù)提高代碼可讀性恶导,可以看出Java api client的語法,同http json請求體非常相似
        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ù)價格,做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組合裂垦,必須同時滿足
                    .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();  // 通過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)換

因為既有MySQL,又有ES肌索,所以有2種異構(gòu)的數(shù)據(jù)模型蕉拢。需要在代碼中定義2種數(shù)據(jù)模型,并且實現(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,能勝任多維度查詢亡电、全文檢索届巩,是因為底層數(shù)據(jù)結(jié)構(gòu)不同:

  • ES倒排索引
    • 如果是全文檢索字段:會先分詞,然后生成 term -> document 的倒排索引份乒,查詢時也會把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)于是一個通用的詞湿蛔,對相關(guān)性影響較小膀曾。
    • 如果是精確值字段:則無需分詞,直接把query作為一個整體的term阳啥,查詢對應(yīng)文檔添谊。
    • 因為文檔中的所有字段,都生成了倒排索引察迟,所以能處理多維度組合查詢
  • MySQL B+樹
    • B+樹的非葉子節(jié)點記錄了孩子節(jié)點值的范圍斩狱,而葉子節(jié)點記錄了真正的一組值,并且在同一層扎瓶,形成了一個有序鏈表
    • 組合索引需要顯式創(chuàng)建:選擇需索引的字段所踊、并且順序是重要的,所以如果待查詢的字段不在索引中概荷,就無法高效查詢秕岛,可能演變?yōu)槿頀呙?/strong>(對聚簇索引的葉子節(jié)點做一次遍歷)

另外簡要回顧一下ES的架構(gòu)要點:

  • 節(jié)點分為主節(jié)點、數(shù)據(jù)節(jié)點乍赫,一個節(jié)點上可以有多個分片瓣蛀,分片分為主分片陆蟆、副本分片雷厂,1對多,主分片與副本分片分布在不同的節(jié)點叠殷,來實現(xiàn)高可用
  • 主分片數(shù)在創(chuàng)建時改鲫,就需要指定,在創(chuàng)建后不能隨意更改(如果變化林束,路由就會出錯)像棘;而副本分片可以增加,來提高ES集群的查詢QPS
  • 路由算法:id % 主分片數(shù)壶冒,如果創(chuàng)建文檔時不指定id缕题,則ES會自動生成;一般會傳自定義業(yè)務(wù)id

優(yōu)點胖腾、缺點

優(yōu)點

  • 支持各字段的多維度組合查詢烟零,無懼未來新增字段(主要成本在于新增字段后、重建索引)
  • 與現(xiàn)有系統(tǒng)完全解耦咸作,適合架構(gòu)演進
  • 在數(shù)據(jù)量級上遠勝Mysql锨阿,最大支持PB級數(shù)據(jù)的存儲和查詢

缺點

  • 讀寫不一致時間在秒級:因為有2個耗時階段,一是同步階段將數(shù)據(jù)從MySQL數(shù)據(jù)庫寫入ES记罚,二是ES索引refresh階段墅诡,數(shù)據(jù)從buffer寫入索引后才可查到
    • 因此一個trick就是,在寫入操作后桐智,前端延遲調(diào)用后端的列表查詢接口末早,比如延遲1秒后再展示
  • 超高并發(fā)下存在瓶頸烟馅,存在穩(wěn)定性問題:目前原生版本支持大約 3-5 萬分片,性能已經(jīng)到達極限然磷,創(chuàng)建索引基本到達 30 秒+ 甚至分鐘級焙糟。節(jié)點數(shù)只能到 500 左右基本是極限了。但依然能滿足絕大部分場景样屠。數(shù)據(jù)來源:https://elasticsearch.cn/slides/259#page=30

ES最佳實踐

  • 只把需要搜索的數(shù)據(jù)導(dǎo)入ES穿撮,避免索引過大
  • 數(shù)據(jù)扁平化,不用嵌套結(jié)構(gòu)痪欲,提高性能
  • 合理設(shè)置字段類型悦穿,預(yù)先定義mapping配置,而不依賴ES自動生成mapping
  • 精確值的類型指定為keyword(mapping配置)业踢,并且使用term查詢
    • 精確值是指無需進行range范圍查詢的字段栗柒,既可以是字符串,比如書的作者名字知举,也可以是數(shù)值瞬沦,比如商品id、訂單id雇锡、圖書ISBN編號逛钻、枚舉值。在使用中锰提,大部分場景是以id類作為精確值
  • 避免無路由查詢:無路由查詢會并發(fā)在多個索引上查詢曙痘、歸并排序結(jié)果,會使得集群cpu飆升立肘,影響穩(wěn)定性
  • 避免深度分頁查詢:如有大量數(shù)據(jù)查詢边坤,推薦用scroll滾動查詢
  • 設(shè)置合理的文件系統(tǒng)緩存(filesytem cache)大小,提高性能:因為ES查詢的熱數(shù)據(jù)在文件系統(tǒng)緩存中
  • ES分片數(shù)在創(chuàng)建后不能隨意改動谅年,但是副本數(shù)可以隨時增加茧痒,來提高最大QPS。如果單個分片壓力過大融蹂,需要擴容旺订。

更進一步

前面提到ES超高并發(fā)下存在瓶頸,極端情況下可能遇到OOM殿较,因此超高并發(fā)下需要C++實現(xiàn)的專用搜索引擎
例如:

  • 百度:通用搜索引擎耸峭,根據(jù)文字、圖片搜索信息
  • 電商垂類:電商專用搜索引擎淋纲,比如根據(jù)關(guān)鍵詞查找商品劳闹,或根據(jù)品牌、價格篩選商品,可總結(jié)為商品的搜索本涕、廣告业汰、推薦
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市菩颖,隨后出現(xiàn)的幾起案子样漆,更是在濱河造成了極大的恐慌,老刑警劉巖晦闰,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件放祟,死亡現(xiàn)場離奇詭異,居然都是意外死亡呻右,警方通過查閱死者的電腦和手機跪妥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來声滥,“玉大人眉撵,你說我怎么就攤上這事÷渌埽” “怎么了纽疟?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長憾赁。 經(jīng)常有香客問我污朽,道長,這世上最難降的妖魔是什么缠沈? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任膘壶,我火速辦了婚禮错蝴,結(jié)果婚禮上洲愤,老公的妹妹穿的比我還像新娘。我一直安慰自己顷锰,他們只是感情好柬赐,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著官紫,像睡著了一般肛宋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上束世,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天酝陈,我揣著相機與錄音,去河邊找鬼毁涉。 笑死沉帮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播穆壕,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼待牵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了喇勋?” 一聲冷哼從身側(cè)響起缨该,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎川背,沒想到半個月后贰拿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡熄云,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年壮不,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片皱碘。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡询一,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出癌椿,到底是詐尸還是另有隱情健蕊,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布踢俄,位于F島的核電站缩功,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏都办。R本人自食惡果不足惜嫡锌,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望琳钉。 院中可真熱鬧势木,春花似錦、人聲如沸歌懒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽及皂。三九已至甫男,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間验烧,已是汗流浹背板驳。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碍拆,地道東北人若治。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓效扫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親直砂。 傳聞我的和親對象是個殘疾皇子菌仁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容