面試官:1億數(shù)據(jù)分庫(kù)分表,分頁(yè)查詢應(yīng)如何設(shè)計(jì)?
大家好,我是秀才。在后端技術(shù)面試,尤其是針對(duì)高級(jí)工程師或架構(gòu)師的崗位,分庫(kù)分表是一個(gè)核心考察點(diǎn)。資深面試官往往不會(huì)直接問(wèn)我們分庫(kù)分表策略是怎么設(shè)計(jì)的,而是會(huì)基于此提出一個(gè)極具挑戰(zhàn)性的場(chǎng)景問(wèn)題:“在億級(jí)別數(shù)據(jù)量且已實(shí)施分庫(kù)分表的背景下,分頁(yè)查詢應(yīng)如何設(shè)計(jì)?常見的性能瓶頸有哪些?”
這個(gè)問(wèn)題并不是單純考察分頁(yè)實(shí)現(xiàn),而是旨在評(píng)估工程師對(duì)于分布式數(shù)據(jù)處理的理解深度、多方案橫向?qū)Ρ鹊臋?quán)衡能力,以及在復(fù)雜約束下進(jìn)行架構(gòu)設(shè)計(jì)的綜合能力。它能夠精確地衡量出一位工程師在分布式系統(tǒng)領(lǐng)域的知識(shí)深度與廣度。
接下來(lái)秀才就對(duì)這個(gè)問(wèn)題進(jìn)行系統(tǒng)性解構(gòu),從根源探究其技術(shù)挑戰(zhàn),并梳理出一套完整的架構(gòu)解決方案,希望能對(duì)大家在工作實(shí)踐和面試中有所幫助。
1. 分庫(kù)分表的常見策略
在深入探討分頁(yè)查詢方案這一問(wèn)題之前,我們必須首先對(duì)分庫(kù)分表架構(gòu)下的基礎(chǔ)組件與核心概念建立一個(gè)清晰的共識(shí)。這里我們主要討論水平分表的解決方案。水平分表表結(jié)構(gòu)一般不會(huì)變更。要探索的是如何將海量數(shù)據(jù)分片存儲(chǔ)到不同的表中以實(shí)現(xiàn)高性能。
1.1 水平分片策略
通常,我們會(huì)采用以下幾種策略將數(shù)據(jù)打散到不同的庫(kù)表中:
- 哈希取模:這是最常見的方式,比如根據(jù)用戶ID進(jìn)行哈希計(jì)算后,對(duì)分庫(kù)(或分表)的數(shù)量取模,從而決定數(shù)據(jù)落在哪個(gè)節(jié)點(diǎn)。例如,將訂單數(shù)據(jù)按
user_id % 16分散到16個(gè)表中。
1
- 優(yōu)點(diǎn):數(shù)據(jù)分布相對(duì)均勻,不容易產(chǎn)生數(shù)據(jù)傾斜和熱點(diǎn)問(wèn)題。
- 缺點(diǎn):范圍查詢極不友好。比如,要查詢某一段時(shí)間內(nèi)創(chuàng)建的所有訂單,就必須掃描所有分片,因?yàn)闊o(wú)法從時(shí)間維度定位到具體的分片。
- 范圍劃分:按照某個(gè)字段的區(qū)間來(lái)切分?jǐn)?shù)據(jù)。最典型的就是按時(shí)間范圍,例如,每個(gè)月的訂單數(shù)據(jù)存放在一張獨(dú)立的表中(
orders_202401,orders_202402...)。
2
- 優(yōu)點(diǎn):范圍查詢非常高效,天然支持按時(shí)間等維度的冷熱數(shù)據(jù)分離。
- 缺點(diǎn):容易產(chǎn)生熱點(diǎn)問(wèn)題。例如,在訂單場(chǎng)景下,所有新的寫入請(qǐng)求都會(huì)集中在最新的那張表上,對(duì)數(shù)據(jù)庫(kù)造成瞬時(shí)高壓。
- 路由中間表:建立一個(gè)獨(dú)立的映射關(guān)系表,用于記錄某條數(shù)據(jù)(如主鍵)具體存儲(chǔ)在哪個(gè)物理庫(kù)表中。這種方式雖然靈活,但多了一次額外查詢,增加了架構(gòu)的復(fù)雜度。查詢時(shí)需要先訪問(wèn)路由表,再根據(jù)路由信息訪問(wèn)目標(biāo)分片。
3
這些策略并不是互斥,實(shí)際架構(gòu)中往往是組合使用,例如先按用戶ID哈希分庫(kù),庫(kù)內(nèi)再按時(shí)間范圍分表。
1.2 分庫(kù)分表的實(shí)現(xiàn)方式
實(shí)現(xiàn)數(shù)據(jù)分片的邏輯,通常由分庫(kù)分表中間件來(lái)完成,它們主要有三種架構(gòu)形態(tài):
- SDK 模式:以
jar包的形式被業(yè)務(wù)應(yīng)用直接引入。優(yōu)點(diǎn)是性能損耗最低,因?yàn)槁酚伞⒕酆系炔僮鞫荚跇I(yè)務(wù)應(yīng)用進(jìn)程內(nèi)完成,沒有額外的網(wǎng)絡(luò)調(diào)用。缺點(diǎn)是與特定編程語(yǔ)言強(qiáng)耦合(Java的SDK無(wú)法給Go用),且版本升級(jí)時(shí)需要協(xié)調(diào)所有依賴方,維護(hù)成本較高,容易形成“胖客戶端”
4
- Proxy 模式:以一個(gè)獨(dú)立的代理服務(wù)存在,對(duì)業(yè)務(wù)應(yīng)用偽裝成一個(gè)數(shù)據(jù)庫(kù)。應(yīng)用所有的SQL請(qǐng)求都先發(fā)給Proxy,由Proxy解析、路由、執(zhí)行,最后合并結(jié)果返回。優(yōu)點(diǎn)是與語(yǔ)言無(wú)關(guān),對(duì)業(yè)務(wù)透明。缺點(diǎn)是多了一層網(wǎng)絡(luò)開銷,且Proxy自身容易成為性能瓶頸和單點(diǎn)故障。為了保證Proxy的高可用,還需要額外部署如LVS、Nginx等負(fù)載均衡和Keepalived等高可用組件,增加了運(yùn)維的復(fù)雜度。
5
- Sidecar 模式:這是一種在服務(wù)網(wǎng)格(Service Mesh)架構(gòu)下興起的形態(tài),將分庫(kù)分表的能力下沉到與業(yè)務(wù)應(yīng)用一同部署的Sidecar中。它結(jié)合了SDK的低耦合和Proxy的語(yǔ)言無(wú)關(guān)性,但目前市面上尚未有非常成熟的開源產(chǎn)品。
6
在這三種形態(tài)中,SDK 模式的性能最好,但缺點(diǎn)是與具體編程語(yǔ)言綁定緊密。比如,Java 提供的 ShardingSphere jar 包無(wú)法直接被 Go 語(yǔ)言調(diào)用。 相比之下,Proxy 模式的性能最弱,因?yàn)樗胁樵冋?qǐng)求都會(huì)集中經(jīng)過(guò)它,極易成為系統(tǒng)瓶頸。如果只部署單節(jié)點(diǎn) Proxy,還存在單點(diǎn)故障的風(fēng)險(xiǎn)。不過(guò)它的優(yōu)勢(shì)也很明顯:與語(yǔ)言無(wú)關(guān),任何技術(shù)棧的業(yè)務(wù)都可以通過(guò)同一個(gè) Proxy 使用分庫(kù)分表能力。而且 Proxy 偽裝成普通數(shù)據(jù)庫(kù)實(shí)例后,業(yè)務(wù)只需替換數(shù)據(jù)源配置,就能在單庫(kù)單表與分庫(kù)分表之間平滑切換,幾乎無(wú)需額外改造
2. 全局查詢法
理解了這些背景,我們就能清晰地認(rèn)識(shí)到,為什么一個(gè)簡(jiǎn)單的LIMIT offset, size分頁(yè)查詢,在分庫(kù)分表環(huán)境下會(huì)面臨嚴(yán)峻的挑戰(zhàn)。其根本原因在于,數(shù)據(jù)被物理隔離在不同分片,單體數(shù)據(jù)庫(kù)的offset和size語(yǔ)義在分布式全局視角下已經(jīng)失效,必須引入跨節(jié)點(diǎn)的排序與數(shù)據(jù)聚合機(jī)制。
針對(duì)這個(gè)問(wèn)題,一種直接的思路是:將查詢請(qǐng)求廣播至所有相關(guān)分片,獲取各分片的數(shù)據(jù)后,在中間件層進(jìn)行全局排序,最后根據(jù)分頁(yè)參數(shù)截取目標(biāo)數(shù)據(jù)返回。這就是全局查詢法。
假設(shè)我們有一個(gè)分頁(yè)查詢需求:
SELECT * FROM order_tab ORDER BY id LIMIT 4 OFFSET 2;假設(shè)訂單數(shù)據(jù)按照user_id % 2被分到了order_tab_0和order_tab_1兩張表中。此查詢的復(fù)雜性根源于數(shù)據(jù)在分片中的不確定性分布。例如,全局偏移量為2、數(shù)量為4的這批數(shù)據(jù),可能存在以下幾種極端情況:
- 所有目標(biāo)數(shù)據(jù)(包括被
OFFSET跳過(guò)的數(shù)據(jù)和最終返回的數(shù)據(jù))全部位于order_tab_0中。
7
- 被
OFFSET跳過(guò)的2條數(shù)據(jù)位于order_tab_0,而最終需要的4條數(shù)據(jù)位于order_tab_1。
8
- 偏移量和目標(biāo)數(shù)據(jù)均勻或不均勻地散落在兩個(gè)分片中。
9
為應(yīng)對(duì)這種不確定性,并確保結(jié)果的完備性,中間件必須采用一種“寧可錯(cuò)撈,不可漏過(guò)”的策略。它會(huì)將原始SQL改寫為對(duì)所有分片都有效的查詢。
改寫的邏輯是:LIMIT size OFFSET offset -> LIMIT size + offset OFFSET 0。
-- 改寫后的SQL,下發(fā)到order_tab_0和order_tab_1
SELECT * FROM order_tab ORDER BY id LIMIT 6 OFFSET 0; -- (4 + 2 = 6)中間件會(huì)從兩個(gè)分片中各自取出前6條數(shù)據(jù),然后在內(nèi)存中進(jìn)行歸并排序,最后再?gòu)呐判蚝蟮慕Y(jié)果集中,跳過(guò)前8條,取出4條數(shù)據(jù)作為最終結(jié)果返回給客戶端。
應(yīng)對(duì)面試知道方法是遠(yuǎn)遠(yuǎn)不夠的,在介紹完這種方法之后,一定要分析這種方案的缺點(diǎn),為引出我們后續(xù)的優(yōu)化方案做準(zhǔn)備。你可以這樣來(lái)分析:“這種方案雖然能保證數(shù)據(jù)的絕對(duì)準(zhǔn)確,但其性能隱患是巨大的,尤其是在深度分頁(yè)(OFFSET值很大)的場(chǎng)景下主要有以下三大性能損耗?!?/span>
- 網(wǎng)絡(luò)開銷劇增:為了量化其網(wǎng)絡(luò)開銷,我們進(jìn)行如下分析。假設(shè)查詢
LIMIT 10 OFFSET 10000,在單庫(kù)中,數(shù)據(jù)庫(kù)只需傳輸10條記錄。但在分庫(kù)分表場(chǎng)景下,假設(shè)我們有10個(gè)分片,每個(gè)分片都需要傳輸10000 + 10條記錄到中間件。如果每條記錄是1KB,那么總傳輸量將是10 * 10010 * 1KB約等于 97.7MB。而真正有用的數(shù)據(jù)只有10 * 1KB = 10KB。為了獲取10KB的有效數(shù)據(jù),產(chǎn)生了近100MB的無(wú)效網(wǎng)絡(luò)傳輸,資源利用效率極低。
- 內(nèi)存消耗巨大:中間件需要將這近100MB的數(shù)據(jù)全部加載到內(nèi)存中進(jìn)行排序。隨著分頁(yè)越深,所需內(nèi)存越大,極易引發(fā)OOM(內(nèi)存溢出),尤其是在Proxy模式下,會(huì)導(dǎo)致整個(gè)代理服務(wù)集群響應(yīng)緩慢甚至崩潰。
- CPU 負(fù)載過(guò)高:排序本身是CPU密集型操作。當(dāng)數(shù)據(jù)量巨大時(shí),會(huì)導(dǎo)致中間件節(jié)點(diǎn)的CPU負(fù)載急劇升高,成為整個(gè)系統(tǒng)的性能瓶頸。
介紹完缺陷之后還可以補(bǔ)充一個(gè)優(yōu)化點(diǎn)以展示對(duì)問(wèn)題理解的深度。
“雖然該方案存在明顯瓶頸,但在工程實(shí)踐中,可以利用歸并排序的特性進(jìn)行優(yōu)化。由于從各分片獲取的數(shù)據(jù)本身已有序,無(wú)需將所有數(shù)據(jù)一次性加載到內(nèi)存。可采用多路歸并(Multiway Merge Sort)算法,維護(hù)一個(gè)大小為N(分片數(shù))的最小堆(Min-Heap),堆中存放每個(gè)分片當(dāng)前待處理的記錄。每次從堆頂取出全局最小的記錄,然后將該記錄所在分片的下一條記錄補(bǔ)充到堆中并調(diào)整堆。這個(gè)過(guò)程可以流式進(jìn)行,顯著降低內(nèi)存消耗,直到獲取到
size條目標(biāo)數(shù)據(jù)為止?!?/span>
10
這個(gè)補(bǔ)充回答,能體現(xiàn)出對(duì)問(wèn)題細(xì)節(jié)的深入思考。但這依然沒有從根本上解決深度分頁(yè)時(shí)數(shù)據(jù)傳輸量過(guò)大的問(wèn)題,因此我們需要探索更高效的架構(gòu)方案。
3. 面試實(shí)戰(zhàn)指南
既然全局聚合排序方案存在性能瓶頸,這就需要通過(guò)更優(yōu)化的設(shè)計(jì)來(lái)達(dá)成目標(biāo)。這也是面試的時(shí)候凸顯你個(gè)人能力的地方,這里主要有以下三種方案可以跟面試官討論
3.1 禁用跳頁(yè)查詢法
這是目前業(yè)界在移動(dòng)端無(wú)限滾動(dòng)(Infinite Scroll)等場(chǎng)景下最主流、最高效的方案。其核心思想是:在產(chǎn)品設(shè)計(jì)上約束分頁(yè)行為,放棄跳轉(zhuǎn)到任意頁(yè)的功能,只允許用戶逐頁(yè)向后加載。也就是通過(guò)犧牲一定的用戶體驗(yàn)來(lái)保證高性能
在這種交互模式下,我們不再需要OFFSET。取而代之的是,每次請(qǐng)求下一頁(yè)數(shù)據(jù)時(shí),客戶端都需要帶上上一頁(yè)最后一條記錄的排序鍵的值。
假設(shè)我們按ID升序分頁(yè),每頁(yè)50條:
- 第一頁(yè)查詢:
-- 首次加載,沒有max_id
SELECT * FROM order_tab ORDER BY id LIMIT 50;客戶端拿到這50條數(shù)據(jù)后,記錄下最后一條數(shù)據(jù)的ID,比如是1050。
- 第二頁(yè)查詢:
-- 加載下一頁(yè)時(shí),從上一頁(yè)的終點(diǎn)繼續(xù)
SELECT * FROM order_tab WHERE id > 1050 ORDER BY id LIMIT 50;這樣,無(wú)論翻到多少頁(yè),SQL語(yǔ)句中的LIMIT部分始終是固定的50,而OFFSET永遠(yuǎn)是0。查詢性能極其穩(wěn)定,且與分頁(yè)深度完全無(wú)關(guān)。如果排序鍵是降序的,比如按時(shí)間倒序,那么查詢條件就變?yōu)?nbsp;WHERE create_time < last_create_time。
處理復(fù)合排序鍵:如果排序規(guī)則是 ORDER BY create_time DESC, id DESC,WHERE條件就需要更嚴(yán)謹(jǐn)?shù)倪壿媮?lái)處理,以避免排序謬誤:
WHERE (create_time < last_create_time) OR (create_time = last_create_time AND id < last_id)這個(gè)細(xì)節(jié)非常關(guān)鍵,能體現(xiàn)出對(duì)問(wèn)題的深入思考和方案的完備性。還是老規(guī)矩,介紹完方案之后一定要分析其優(yōu)缺點(diǎn)
優(yōu)點(diǎn):性能極高且穩(wěn)定,實(shí)現(xiàn)簡(jiǎn)單,完美契合移動(dòng)端的交互習(xí)慣。
缺點(diǎn):犧牲了用戶自由跳轉(zhuǎn)頁(yè)碼的功能。
這個(gè)方案是解決這類場(chǎng)景的關(guān)鍵技術(shù)之一,雖然看似犧牲了一定的用戶體驗(yàn),但大多數(shù)情況下用戶卻還是可以接受的。這種思想在架構(gòu)設(shè)計(jì)中具有重要價(jià)值,它展現(xiàn)了如何通過(guò)優(yōu)化產(chǎn)品交互來(lái)規(guī)避復(fù)雜的技術(shù)難題,體現(xiàn)了技術(shù)方案與業(yè)務(wù)場(chǎng)景深度結(jié)合的架構(gòu)思維。
3.2 二次查詢法(亮點(diǎn)方案一)
這個(gè)時(shí)候面試官為了考察你的技術(shù)深度,可能會(huì)接著挑戰(zhàn)
“如果業(yè)務(wù)場(chǎng)景(例如后臺(tái)管理系統(tǒng))在產(chǎn)品設(shè)計(jì)上必須要求精確的跳頁(yè)功能,怎么辦?
這個(gè)時(shí)候就輪到我們的第一個(gè)亮點(diǎn)方案出場(chǎng)了——二次查詢法。這個(gè)方案的邏輯較為復(fù)雜,你如果能在面試中通過(guò)一些示例簡(jiǎn)潔的講清楚,將成為一個(gè)重要的技術(shù)亮點(diǎn)。
我們用一個(gè)例子來(lái)拆解其步驟,假設(shè)一個(gè)DB中保存了用戶年齡數(shù)據(jù),從1歲到30歲,共有30條記錄。如果需要查詢這些數(shù)據(jù),可能會(huì)執(zhí)行以下SQL語(yǔ)句:
select * from T order by age limit 5 offset 10那么會(huì)返回以下紅色標(biāo)識(shí)數(shù)據(jù),即[11,15],請(qǐng)記住此結(jié)果,下面會(huì)講解怎么分庫(kù)查詢以下結(jié)果
11
把以上所有數(shù)據(jù)分片存儲(chǔ)到3個(gè)分庫(kù)中,如下,注意下面數(shù)據(jù)只是用戶屬性年齡,不是分片鍵:
12
根據(jù)前面分析,在單一數(shù)據(jù)庫(kù)中執(zhí)行 LIMIT 5 OFFSET 10 查詢時(shí),返回的結(jié)果是[11-15]。那么,如果在這三個(gè)分庫(kù)中進(jìn)行全局查詢 LIMIT 5 OFFSET 10,該如何操作呢?
- 語(yǔ)句改寫
將以下SQL
select * from T order by age limit 5 offset 10改寫為:
select * from T order by age limit 5 offset 3在所有分庫(kù)執(zhí)行修改之后的sql,注意,這個(gè) offset 的 3,來(lái)自于全局offset的總偏移量 10,除以水平切分?jǐn)?shù)據(jù)庫(kù)個(gè)數(shù) 3。
執(zhí)行select * from T order by age limit 5 offset 3,結(jié)果如下(紅色標(biāo)識(shí)數(shù)據(jù)),為了便于理解用淺綠色標(biāo)識(shí)庫(kù)表前三條數(shù)據(jù):
13
- 找到返回?cái)?shù)據(jù)的最小值
第一個(gè)庫(kù),5 條數(shù)據(jù)的 age 最小值是10
第二個(gè)庫(kù),5 條數(shù)據(jù)的 age 最小值是 6
第三個(gè)庫(kù),5 條數(shù)據(jù)的 age 最小值是 12
14
三頁(yè)數(shù)據(jù)中,只需要比較各個(gè)分庫(kù)第一條數(shù)據(jù)[10,6,12],因此age最小值來(lái)自第二個(gè)庫(kù),age_min=6,時(shí)間復(fù)雜度很低
- 查詢二次改寫
第一次改寫的SQL語(yǔ)句是select * from T order by age limit 5 offset 3 第二次要改寫成一個(gè)between語(yǔ)句,between的起點(diǎn)是age_min,between的終點(diǎn)是原來(lái)每個(gè)分庫(kù)各自返回?cái)?shù)據(jù)的最大值:
- 第一個(gè)分庫(kù),第一次返回?cái)?shù)據(jù)的最大值是22 所以查詢改寫為
select * from T order by age where age between age_min and 22- 第二個(gè)分庫(kù),第一次返回?cái)?shù)據(jù)的最大值是20 所以查詢改寫為
select * from T order by age where age between age_min and 20- 第三個(gè)分庫(kù),第一次返回?cái)?shù)據(jù)的最大值是25 所以查詢改寫為
select * from T order by age where age between age_min and 25相對(duì)于第一次查詢,第二次查詢的條件放寬了,因此第二次查詢會(huì)返回比第一次查詢結(jié)果集更多的數(shù)據(jù)。假設(shè)這三個(gè)分庫(kù)返回的數(shù)據(jù)如下:

可以看到:
- 分庫(kù)一的結(jié)果集比第一次查詢多返回了一條數(shù)據(jù),即藍(lán)色記錄7。
- 由于
age_min是從原來(lái)的分庫(kù)二獲取的,因此分庫(kù)二的返回結(jié)果集與第一次查詢相同,實(shí)際上這次查詢可以省略。 - 分庫(kù)三的結(jié)果集比第一次查詢多返回了三條數(shù)據(jù),即深藍(lán)色記錄8、9、11。
- 找到age_min在全局的offset
注:offset表示所查詢的結(jié)果集中前面有多少條數(shù)據(jù)沒有被查詢。
在每個(gè)結(jié)果集中虛擬一個(gè)age_min記錄,找到age_min在各個(gè)分庫(kù)的offset,也就是找到age_main在各個(gè)分庫(kù)中前面有多少條數(shù)據(jù)。
16
在分庫(kù)1中age_min的offset為2,分庫(kù)2中age_min的offset為3,分庫(kù)3中age_min的offset為0。
所以age_min的全局offset為:2+3+0=5。
- 查找最終數(shù)據(jù)
既然已經(jīng)確定了全局的 age_min 為偏移量5,因此可以獲得全局的視角。根據(jù)第二次查詢的結(jié)果集。
各分庫(kù)二次查詢結(jié)果如下:
分庫(kù)1:7、10、14、16、21、22
分庫(kù)2:6、13、17、19、20
分庫(kù)3:8、9、11、12、15、18、23、25
統(tǒng)一放到list排序后:[6、7、8、9、10、11、12、13、14、15、16、17、18、19、20、21、22、23、25],得知最小值age_main當(dāng)前的全局offset為5,即6前面有5條數(shù)據(jù)了,最終結(jié)果要取offset 10 limit 5,即要保證前面有10條數(shù)據(jù)不會(huì)被取到,所以偏移量offset要往后再移動(dòng)5個(gè)單位到11,這樣前面就有10條數(shù)據(jù)不會(huì)被取到了。查詢的limit為5,然后再向后取5位,那就是[11、12、13、14、15],圖中黃色標(biāo)識(shí)數(shù)據(jù)就是我們要找的結(jié)果集。
17
3.2.1 二次查詢法的限制條件
雖然二次查詢法在一定程度上能提高跨庫(kù)分頁(yè)的性能,但是卻有著嚴(yán)格的限制條件,這個(gè)前提條件就是某1頁(yè)的數(shù)據(jù)要均攤到各分表,換句話說(shuō)就是二次查詢法不太適用于數(shù)據(jù)分布不均勻,比如數(shù)據(jù)大量集中在某一張表,而其他的分表數(shù)據(jù)量少,分段法的分表策略同樣不適用,下面就用實(shí)際例子來(lái)看一下:
- 場(chǎng)景1(分表策略:取模法)
原序列:(1,2,3,4,5,6,7,8),需要取出limit 2,2 ,即:(3,4)
第1次查詢
(1,3,5,7) -> limit 2,2 -> 改寫成 limit 1,2 -> (3,5)
(2,4,6,8) -> limit 2,2 -> 改寫成 limit 1,2 -> (4,6)
最小值min為3
第2次查詢
(1,3,5,7) -> between 3 and 5 -> (3,5)
(2,4,6,8) -> between 3 and 6 -> (4,6)
將第2次查詢的結(jié)果合并:
(3,4,5,6) ->根據(jù)規(guī)則,推算出最小值3的全局的offset為2,不需要向后移動(dòng),然后取limit即2個(gè)元素 -> (3,4) ,結(jié)果正確。
- 場(chǎng)景2(分表策略:取模法)
原序列:(1,2,3,4,5,6,7,8),需要取出limit 1,2 ,即:(2,3)
第1次查詢
(1,3,5,7) -> limit 1,2 -> 改寫成 limit 0,2 -> (1,3) --注:因?yàn)?/2除不盡,這里向下取整
(2,4,6,8) -> limit 1,2 -> 改寫成 limit 0,2 -> (2,4)
最小值min為1
第2次查詢
(1,3,5,7) -> between 1 and 3 -> (1,3)
(2,4,6,8) -> between 1 and 4 -> (2,4)
將第2次查詢的結(jié)果合并:
(1,2,3,4) -> 根據(jù)規(guī)則,推算出最小值1的全局的offset為0,向后移2位,然后取limit即2個(gè)元素 -> (3,4) ,結(jié)果正確。
- 場(chǎng)景3(分表策略:分段法)
原序列(1,2,3,4,5,6,7,8)->(1,2,3,4),(5,6,7,8)
原序列:(1,2,3,4,5,6,7,8),需要取出limit 2,2 ,即:(3,4)
第1次查詢
(1,2,3,4) -> limit 2,2 -> limit 1,2 -> {2,3} --注:這里就已經(jīng)把正確的數(shù)據(jù)給丟掉了。
(5,6,7,8) -> limit 2,2 -> limit 1,2 -> {6,7} --注:這一段里根本就沒有這一頁(yè)的數(shù)據(jù)。
最小值min為2
第2次查詢
(1,2,3,4) -> between 2 and 3 -> {2,3}
(5,6,7,8) -> between 2 and 7 -> {5,6,7}
(2,3,5,6,7) -> (3,5) 根據(jù)規(guī)則,推算出最小值2的全局的offset為1,向后移1為,然后取limit即2個(gè)元素 -> (3,5) ,結(jié)果正確這個(gè)跟預(yù)期結(jié)果就對(duì)不上了。
整體來(lái)看,二次查詢法還是相對(duì)復(fù)雜的,所以在跟面試官介紹的時(shí)候,能用示例來(lái)說(shuō)明很重要,如果有屏幕或者有白紙可以演示的話更好,這樣更容易講清楚。在介紹完方案之后,你可以再總結(jié)一下二次查詢的優(yōu)缺點(diǎn)。
總的來(lái)說(shuō),二次查詢法通過(guò)兩次查詢,犧牲了一定的響應(yīng)時(shí)間,換來(lái)了分頁(yè)的絕對(duì)精確性。但是其缺點(diǎn)也很明顯,需要進(jìn)行兩次數(shù)據(jù)庫(kù)查詢,并且有一定的限制,要求數(shù)據(jù)均勻分布,即某一頁(yè)的數(shù)據(jù)要均攤到各分表,否則查詢結(jié)果就不精確。它的優(yōu)點(diǎn)就是不會(huì)對(duì)業(yè)務(wù)造成局限性,每次返回的數(shù)據(jù)量都很有限,不會(huì)隨著翻頁(yè)增加數(shù)據(jù)的返回量。
3.3 引入中間表(亮點(diǎn)方案二)
中間表方案的核心是“空間換時(shí)間”思想。我們創(chuàng)建一個(gè)獨(dú)立的、未分片的“索引表”或“物化視圖”,該表僅存儲(chǔ)用于排序的字段和主鍵ID。
image
例如,要按更新時(shí)間排序,我們的索引表結(jié)構(gòu)可以是 (主鍵, 更新時(shí)間, 目標(biāo)庫(kù))。
當(dāng)需要分頁(yè)查詢時(shí):
- 先在這個(gè)單體的索引表上執(zhí)行分頁(yè)查詢,由于是單表,
LIMIT OFFSET性能很高。
-- 在索引表上輕松定位
SELECT primary_key, target_db_shard FROM index_table ORDER BY update_time DESC LIMIT 10 OFFSET 100;- 拿到目標(biāo)數(shù)據(jù)的
primary_key和它們所在的分片信息后,再通過(guò)IN查詢回到各個(gè)分片庫(kù)中,撈取完整的數(shù)據(jù)詳情。
這個(gè)方案有兩個(gè)顯著的挑戰(zhàn):數(shù)據(jù)一致性維護(hù)和查詢能力的限制。
- 數(shù)據(jù)一致性:這個(gè)方案最大的挑戰(zhàn)在于如何維護(hù)索引表與主數(shù)據(jù)表之間的數(shù)據(jù)一致性。
同步雙寫:在業(yè)務(wù)代碼中同時(shí)寫入主表和索引表。這會(huì)增加業(yè)務(wù)邏輯的復(fù)雜度和響應(yīng)時(shí)間,且存在分布式事務(wù)問(wèn)題。
異步復(fù)制:更優(yōu)化的方式是,通過(guò)訂閱數(shù)據(jù)庫(kù)的binlog(如使用Canal等工具),異步地將數(shù)據(jù)變更同步到索引表中。這能與業(yè)務(wù)邏輯解耦,但會(huì)存在一定的延遲,即數(shù)據(jù)一致性是最終一致性。

這里也是面試官最容易問(wèn)的地方,有經(jīng)驗(yàn)的面試官很可能會(huì)追問(wèn):“異步更新失敗了怎么辦?”,還是萬(wàn)變不離其宗,只要失敗,就可以補(bǔ)償。
你可以回答:“我們會(huì)引入具備重試與死信隊(duì)列(DLQ)的機(jī)制。如果多次重試后仍然失敗,則消息進(jìn)入死信隊(duì)列,并觸發(fā)告警,由人工介入進(jìn)行數(shù)據(jù)校準(zhǔn)?!?/span>
- 查詢能力限制:另一個(gè)重要的限制就是查詢能力有限。一個(gè)重要的操作限制是,任何過(guò)濾條件(
WHERE子句)都必須基于索引表中已存在的列。如果用戶需要根據(jù)一個(gè)未被物化的字段進(jìn)行篩選,此方案將失效,除非向索引表添加更多列,但這會(huì)增加存儲(chǔ)和維護(hù)的成本。
3.4 引入外部存儲(chǔ)
當(dāng)排序和篩選條件非常復(fù)雜時(shí),以上所有基于關(guān)系型數(shù)據(jù)庫(kù)的方案可能都難以應(yīng)對(duì)。此時(shí),一個(gè)更通用的架構(gòu)選擇是引入外部系統(tǒng)。
- 引入搜索引擎:將需要查詢的數(shù)據(jù)(全量或部分字段)通過(guò)雙寫或
binlog同步的方式,寫入到Elasticsearch這樣的專業(yè)搜索引擎中。分頁(yè)、排序、復(fù)雜篩選等查詢請(qǐng)求,全部交由Elasticsearch處理。Elasticsearch基于Lucene構(gòu)建,其倒排索引和分布式聚合能力使其在處理這類需求時(shí)具備天然優(yōu)勢(shì)。從ES獲取ID后,再回源數(shù)據(jù)庫(kù)查詢?cè)斍?。這是一種非常成熟的異構(gòu)索引方案。 - 引入分布式關(guān)系型數(shù)據(jù)庫(kù):另一種思路是采用NewSQL數(shù)據(jù)庫(kù),如TiDB。TiDB的底層存儲(chǔ)引擎TiKV是一個(gè)全局有序的分布式鍵值存儲(chǔ),這使得它能夠在分布式環(huán)境下高效地處理
ORDER BY和LIMIT操作,將分頁(yè)的復(fù)雜性下沉到了數(shù)據(jù)庫(kù)內(nèi)核層面,對(duì)應(yīng)用層透明。
4. 小結(jié)
從最初的全局查詢,到禁用跳頁(yè)、二次查詢、引入中間表,再到借助外部存儲(chǔ),可以看到分頁(yè)查詢?cè)诜謳?kù)分表架構(gòu)下并不是一個(gè)簡(jiǎn)單的 SQL 問(wèn)題,而是牽涉到性能、數(shù)據(jù)一致性、交互設(shè)計(jì)等多維度的系統(tǒng)性挑戰(zhàn)。沒有放之四海而皆準(zhǔn)的最佳方案,關(guān)鍵在于理解業(yè)務(wù)場(chǎng)景的需求邊界,然后在可接受的性能與復(fù)雜度之間做出取舍。面試中,能否系統(tǒng)性地剖析問(wèn)題、提出不同層次的解決思路,并清晰地權(quán)衡優(yōu)缺點(diǎn),往往比直接給出某個(gè)答案更能打動(dòng)面試官。



































