<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          MySQL 跨庫分頁/ 分表分頁,為什么這么難?

          共 5671字,需瀏覽 12分鐘

           ·

          2022-01-10 14:13

          以下內(nèi)容來自公眾號逆鋒起筆,關(guān)注每日干貨及時送達

          來源:https://www.cnblogs.com/yjmyzz/p/12149737.html


          當業(yè)務(wù)數(shù)據(jù)達到一定量級(比如:mysql單表記錄量>1千萬)后,通常會考慮“分庫分表”將數(shù)據(jù)分散到不同的庫或表中,這樣可以大大提高讀/寫性能。但是問題來了,對于 select * from table limit offset , pagesize 這種分頁方式,原來一條語句就可以簡單搞定的事情會變得很復雜,本文將與大家一起探討分庫分表后"分頁"面臨的新問題。

          | 分表對分頁的影響

          比如有一張表,里面有8條記錄(為簡單起見,假設(shè)該表上只有1個自增ID),數(shù)學上可以抽象成1個(有序)數(shù)列(注:為方便討論,不加特殊說明的情況下,文本中數(shù)列的順序,均指升序)。
          (1,2,3,4,5,6,7,8)
          如果要取出上面紅色標識的2,3這二條記錄,limit 1,2 就行了。
          現(xiàn)在假如分成2張表(即:原來的數(shù)列,拆分成2個非空子數(shù)列),一般來講,有二種常用分法:
          分段法(比如:有時間屬性的數(shù)據(jù),類似訂單這種,可以按下單時間拆分,每個月1張表)
          (1,2,3,4)
          (5,6,7,8)
          沿用之前的limit x,y的思路,每個分表上 limit 1,2,會得到如下2個子數(shù)列:
          (2,3)
          (6,7)
          然后在內(nèi)存中合并排序,再取前2條 (2,3,6,7) => (2,3) ,貌似看上去也符合預期(這個思路也稱為歸并),但這只是假象。當要取的分頁數(shù)據(jù)落在不同的子數(shù)列上時,就能發(fā)現(xiàn)問題:
          (1,2,3,4,5,6,7,8)?
          比如,我們要從4個位置開始,連續(xù)取2個元素,即: limit 3,2
          (1,2,3,4) => limit 3,2 =>(4)
          (5,6,7,8) => limit 3,2 =>(8)
          最后合并出來的結(jié)果是(4,8) 與正確結(jié)果 (4,5)相比,顯然不對。
          模余均攤法(比如:字段值對2取模求余數(shù),根據(jù)余數(shù)決定分到哪個表,該方法也簡稱為取余法)
          (1,3,5,7)
          (2,4,6,8)
          歸并排序的思路在分段法上行不通,對于取模均攤同樣也不行,仍以 limit 1,2為例,原始序列取出來的結(jié)果是(2,3),如果用歸并的思路:
          (1,3,5,7)=> limit 1,2 =>(3 ,5)
          (2,4,6,8)=> limit 1,2 =>(4, 6)
          內(nèi)存合并排序后,取前2個,最終結(jié)果為(3 , 4)。
          結(jié)論:不管分庫分表采用什么分法,簡單歸并的思路,都無法正確解決分頁問題。

          | 全局法(limit x+y)

          反思一下剛才的歸并思路,本質(zhì)上我們在每個子數(shù)列(即:分表)上limit x,y 時,取出來的數(shù)據(jù)就有可能已經(jīng)產(chǎn)生缺失了。網(wǎng)上有一篇廣為流轉(zhuǎn)的文章"業(yè)界難題-跨庫分頁”,作者在文中提出了一個方案:把范圍擴大,分表sql上的limit x,y 變成 limit 0, x+y ,這樣改寫后,相當于分表中把"每頁最后一條數(shù)據(jù)"之前的所有數(shù)據(jù)全都取出來了(當然:這里面可能會有不需要的多余數(shù)據(jù)),然后內(nèi)存中合并在一起,再取x偏移量后的y條數(shù)據(jù)。
          用前面的例子驗證一下:
          原序列:(1,2,3,4,5,6,7,8),需要取出limit 1,2 ,即:(2,3)
          分段法拆成2段:
          (1 , 2 , 3 , 4) => limit 1,2 =>改寫成 limit 0, 1+2 => (1,2,3)
          (5 , 6 , 7 , 8) => limit 1,2 =>改寫成 limit 0, 1+2 => (5,6,7)
          將子數(shù)列合并排序=> { 1,2,3,5,6,7} => 按原始偏移量 limit 1,2 =>{2,3} 正確
          如果原數(shù)列中要取的數(shù)據(jù),正好落在2個子數(shù)列上(1,2,3,4,5,6,7,8),需要取出limit 3,2 ,即:(4,5)
          (1 , 2 , 3 , 4) => limit 3,2 =>改寫成 limit 0, 3+2 => (1,2,3,4)
          (5 , 6 , 7 , 8) => limit 3,2 =>改寫成 limit 0, 3+2 => (5,6,7,8)
          將子數(shù)列合并排序=> (1,2,3,4,5,6,7,8) => 按原始偏移量 limit 3,2 => (4,5) 也符合預期。
          取模均攤拆成2段
          (1,3,5,7) => limit 1,2 ->改寫成 limit 0, 1+2 => (1,3 ,5)
          (2,4,6,8) => limit 1,2 ->改寫成 limit 0, 1+2=> (2,4,6)
          將子序列合并=> (1,2,3,4,5,6) => 按原始偏移量 limit 1,2 =>(2,3) 正確
          該方法缺點也很明顯:取出的記錄太多了,比如 limit 10000000,10 -> 改寫后變成 limit 0, 10000010 遇到海量數(shù)據(jù),mysql中查詢有可能直接超時,這么多數(shù)據(jù)從db傳到應(yīng)用層,網(wǎng)絡(luò)開銷也很大,更不用說如果是java應(yīng)用,大量數(shù)據(jù)放到List或Map中,容易出現(xiàn)OOM。(注:一般情況下,需要用分庫分表的場景,數(shù)據(jù)量必然很大,所以這個方法,實際中基本上沒法用)。

          | 二次查詢法

          這也是"業(yè)界難題-跨庫分頁”一文中提到的一個方法,大致思路如下:在某1頁的數(shù)據(jù)均攤到各分表的前提下(注:這個前提很重要,也就是說不會有一個分表的數(shù)據(jù)特別多或特別少),換句話說:這個方案不適用分段法,按如下步驟操作:

          1)原sql中的limit offset,pagesize 改寫成 limit offset/n ,pagesize (注:n為分表個數(shù),如果offset/n除不盡,向下取整,避免最后的結(jié)果丟數(shù)據(jù))-- 這個的意思,其實就是假設(shè)原表這一頁的數(shù)據(jù),會均分到各個分表(所以,我一再強調(diào),前提是數(shù)據(jù)是均攤的,如果某個分表的記錄很少,極端情況下,甚至是空的,這個就不對了,最終結(jié)果會少數(shù)據(jù))。

          2)分表上,執(zhí)行改寫后的sql,得到一堆結(jié)果集,然后找出這堆結(jié)果中的最小id (假設(shè)id是關(guān)鍵的排序字段),記為min_id -- 這一步的目的,是為了找出最小的起始點,保證第1頁數(shù)據(jù)起點正確。

          3)各分表上的sql,where條件部分改寫成 id between min_id and origin_max_id (注:origin_max_id為上一步,每個分表查詢結(jié)果集中的最大值,顯然min_id=自身最小id的那張分表,不用再重復查詢) -- 這一步的目的在于,因為步驟1)查出來的結(jié)果,通常會比原表上該頁的數(shù)據(jù)少,所以這里重新將起始點設(shè)置到正確的位置,即:min_id,再查1次,相當于范圍擴大了,以保證數(shù)據(jù)不會丟。不過,這里有一個可優(yōu)化的地方,仔細想想,這1次查詢出來的結(jié)果,跟步驟1)中的查出來的結(jié)果,必然有一部分是重復的,因此改寫部分,只需要 id between min_id and origin_min_id就可以了(origin_min_id 即為原來分表結(jié)果上的最小id)。

          4)將上一步查詢出來的結(jié)果,在內(nèi)存中合并排序去重(注:如果上一步采用了優(yōu)化方案,就應(yīng)該是把1)與3)這二次查詢的結(jié)果全取出來合并排序去重),然后從開始連續(xù)取pagesize條數(shù)據(jù)即可(注:offset/n除不盡的話,向下取整了,也就是起始點可能向前多移了,所以有可能開始的第1條記錄,其實是上1頁的最后1條記錄,要追求精確的話,可以在應(yīng)用層記錄上一頁最后1條記錄的id,然后跟本次查詢結(jié)果前1條記錄對比,如果發(fā)現(xiàn)是一樣的,開始取數(shù)據(jù)的位置,就要向后移1位,如果考慮id有重復的話,就要根據(jù)情況多移幾位)

          驗證一下看看效果:
          場景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)
          最小值為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) ->取頭開始,取pageSize即2個元素 -> (3,4) 正確
          -----------------------------------------------------
          場景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) --注:因為1/2除不盡,這里向下取整了
          (2,4,6,8) -> limit 1,2 -> 改寫成 limit 0,2 -> (2,4)
          最小值為1
          第2次查詢
          (1,3,5,7) -> between 1 and 3 -> (1,3)
          (2,4,6,8) -> between 1 and 4 -> (2,4)
          將上面的結(jié)果合并:
          (1,2,3,4) -> (2,3) (注:起始點,第1次查詢改寫時,向下取整了,所以這里要向移1位,從第2個數(shù)字開始取pagesize條數(shù)據(jù))
          --------------------------------------------------------
          場景3(前提:分段法)
          為什么說分段法,這個方案不適用,可以看下面的分析:
          原序列:(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 -> {5,6} --注:這一段里根本就沒有這一頁的數(shù)據(jù)。
          最小值2
          第2次查詢
          (1,2,3,4) -> between 2 and 3 -> {2,3}
          (5,6,7,8) -> between 2 and 6 -> {5,6}
          (2,3,5,6) -> (2,3) 這個跟預期結(jié)果就對不上了。
          -------------------------------------------------------
          場景4(前提:取余法)
          取余法的前提下,如果某個分表的數(shù)據(jù),被清掉一部分,也就是某個分表數(shù)據(jù)偏少,會發(fā)生什么?
          比如下面這個,按奇數(shù)、偶數(shù)分成2個子序列,但是偶數(shù)故意刪除了幾個(相當于現(xiàn)實業(yè)務(wù)中,這個分表上的數(shù)據(jù)被干掉了一部分)。
          原序列:(1,3,5,6,7,8,9,11),需要取出limit 2,2 ,即:(5,6)
          第1次查詢
          (1,3,5,7,9,11) -> limit 2,2 -> 改寫成limit 1,2 -> (3,5)
          (6,8) -> limit 2,2 -> 改寫成limit 1,2 -> (8)
          第2次查詢
          (1,3,5,7,9,11) -> between 3 and 5 -> (3,5)
          (6,8) -> between 3 and 8 -> (6,8)
          合并后
          (3,5,6,8) -> (3,5) 這跟預期結(jié)果也對不上。

          | 禁止跳頁

          相當于只允許向上或向下翻頁,原理很簡單,比如:下一頁,先記錄上一頁的最大id,然后下一頁時,只需要 where id > 上一頁最大id limit pagesize, 每次只翻1頁。顯然,這是一個犧牲用戶體驗的做法。
          | 結(jié)論
          分表分頁不存在一個通用的解決方案,要么性能有問題(比如:全局法 limit x+y),要么必須具備一定的前提條件(比如:二次查詢),或者產(chǎn)品設(shè)計上犧牲用戶體驗,仍然是一個業(yè)內(nèi)難題。
          參考文章:

          https://juejin.im/post/5d1f52e46fb9a07eb3099bbf

          https://shardingsphere.apache.org/document/current/cn/features/sharding/use-norms/pagination/

          https://stackoverflow.com/questions/3927537/how-do-you-implement-sorting-and-paging-on-distributed-data

          http://kmiku7.github.io/2019/08/01/Do-Pagination-With-Table-Database-Sharding/

          https://segmentfault.com/a/1190000013225860

          https://mp.weixin.qq.com/s/h99sXP4mvVFsJw6Oh3aU5A

          本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權(quán)利。

          逆鋒起筆專注于程序員圈子,你不但可以學習到java、python等主流技術(shù)干貨,還可以第一時間獲悉最新技術(shù)動態(tài)、內(nèi)測資格、BAT大佬的經(jīng)驗、精品視頻教程、副業(yè)賺錢經(jīng)驗,微信搜索readdot關(guān)注!

          MySQL 8.0 可以操作 JSON 了!!

          MySQL 大批量插入,如何過濾掉重復數(shù)據(jù)?

          SQL 中如何給指定數(shù)據(jù)庫創(chuàng)建只讀用戶?

          MySQL 5.7 vs 8.0,哪個性能更牛?

          大廠 MySQL 規(guī)范,從入門到精通!

          點贊是最大的支持?
          瀏覽 61
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人人插视频 | 国产在线欧美日韩字幕 | 国产精品久久久久久久久毛毛 | 操久久久 | 成人黄色性视频 |