分庫分表會帶來讀擴散問題?怎麼解決?

大家好,我是華仔。

今天這篇文章,其實也是我曾經面試中遇到過的真題。

分庫分表大家可能聽得多了,但讀擴散問題大家瞭解嗎?

這裏涉及到幾個問題。

分庫分表是什麼?

讀擴散問題是什麼?

分庫分表爲什麼會引發讀擴散問題?

怎麼解決讀擴散問題?

能不能不要在評論區叫我刁毛?

不好意思,失態了。

這些問題還是比較有意思的。

相信兄弟們也一定有機會遇到哈哈哈。

我們先從分庫分表的話題聊起吧。

分庫分表

我們平時做項目開發。一開始,通常都先用一張數據表,而一般來說數據表寫到 2kw 條數據之後,底層 B + 樹的層級結構就可能會變高,不同層級的數據頁一般都放在磁盤裏不同的地方,換言之,磁盤 IO 就會增多,帶來的便是查詢性能變差。如果對上面這句話有疑惑的話,可以去看下我之前寫的文章。

於是,當我們單表需要管理的數據變得越來越多,就不得不考慮數據庫分表。而這裏的分表,分爲水平分表和垂直分表

垂直分表的原理比較簡單,一般就是把某幾列拆成一個新表,這樣單行數據就會變小,B + 樹裏的單個數據頁(固定 16kb)內能放入的行數就會變多,從而使單表能放入更多的數據。

垂直分表沒有太多可以說的點。下面,我們重點說說最常見的水平分表

水平分表有好幾種做法,但不管是哪種,本質上都是將原來的 user 表,變成 user_0, user1, user2 .... uerN這樣的 N 多張小表。

從讀寫一張 user 大表,變成讀寫 user_1 … userN 這樣的 N 張小表

分表

每一張小表裏,只保存一部分數據,但具體保存多少,這個自己定,一般就訂個 500w~2kw

那分表具體怎麼做?

根據 id 範圍分表

我認爲最好用的,是根據 id 範圍進行分表。

我們假設每張分表能放2kw行數據。那 user0 就放主鍵 id 爲1~2kw的數據。user1 就放 id 爲2kw+1 ~ 4kw,user2 就放 id 爲4kw+1 ~ 6kw, userN 就放 2N kw+1 ~ 2(N+1)kw

根據 id 範圍分表

假設現在有條數據,id=3kw,將這個3kw除2kw = 1.5,向下取整得到1,那就可以得到這條數據屬於user1表。於是去讀寫 user1 表就行了。這就完成了數據的路由邏輯,我們把這部分邏輯封裝起來,放在數據庫和業務代碼之間。

這樣。對於業務代碼來說,它只知道自己在讀寫一張 user 表,根本不知道底下還分了那麼多張小表。

對於數據庫來說,它並不知道自己被分表了,它只知道有那麼幾張表,正好名字長得比較像而已。

這還只是在一個數據庫裏做分表,如果範圍再搞大點,還能在多個數據庫裏做分表,這就是所謂的分庫分表

不管是單庫分表還是分庫分表,都可以通過這樣一箇中間層邏輯做路由。

還真的就應了那句話,沒有什麼是加中間層不能解決的。

如果有,就多加一層。

至於這個中間層的實現方式就更靈活了,它既可以像第三方 orm 庫那樣加在業務代碼中。

通過 orm 讀寫分表

也可以在 mysql 和業務代碼之間加個 proxy 服務

如果是通過第三方 orm 庫的方式來做的話,那需要根據不同語言實現不同的代碼庫,所以不少廠都選擇後者加個 proxy 的方式,這樣就不需要關心上游服務用的是什麼語言。

通過 proxy 管理分表

根據 id 取模分表

這時候就有兄弟要提出問題了," 我看很多方案都對 id 取模,你這個方案是不是不完整?"。

取模的方案也是很常見的。

比如一個 id=31 進來,我們一共分了 5 張表,分別是 user0 到 user4。對31%5=1,取模得1,於是就能知道應該讀寫user1表。

根據 id 取模分表

優點當然是比較簡單。而且讀寫數據都可以很均勻的分攤到每個分表上。

缺點也比較明顯,如果想要擴展表的個數,比如從 5 張表變成 8 張表。那同樣還是 id=31 的數據,31%8 = 7,就需要讀寫 user7 這張表。跟原來就對不上了。

這就需要考慮數據遷移的問題。很頭禿。

爲了避免後續擴展的問題,我見過一些業務一開始就將數據預估得很大,然後心一橫,分成 100 張表,一張表如果存個 2kw 條,那也能存 20 億數據了。

也不是說這樣不行吧,就是這個業務直到最後放棄的時候,也就存了百萬條數據,每次打開數據庫表能看到茫茫多的 user_xx,就是不太舒服,專業點,叫增加了程序員的心智負擔

而上面一種方式,根據 id 範圍去分表,就能很好的解決這些問題,數據少的時候,表也少,隨着數據增多,表會慢慢變多。而且這樣表還可以無限擴展。

那是不是說取模的做法就用不上了呢?

也不是。

將上面兩種方式結合起來

id 取模的做法,最大的好處是,新寫入的數據都是實實在在的分散到了多張表上。

而根據 id 範圍去做分表,因爲 id 是遞增的,那新寫入的數據一般都會落到某一張表上,如果你的業務場景寫數據特別頻繁,那這張表就會出現寫熱點的問題。

這時候就可以將 id 取模和 id 範圍分表的方式結合起來。

我們可以在某個 id 範圍裏,引入取模的功能。比如 以前 2kw~4kw是 user1 表,現在可以在這個範圍再分成 5 個表,也就是引入 user1-0, user1-2 到 user1-4,在這 5 個表裏取模。

舉個例子,id=3kw,根據範圍,會分到 user1 表,然後再進行取模 3kw % 5 = 0,也就是讀寫 user1-0 表。

這樣就可以將寫單表分攤爲寫多表。

這在分庫的場景下優勢會更明顯,不同的庫,可以把服務部署到不同的機器上,這樣各個機器的性能都能被用起來。

根據 id 範圍分表後再取模

讀擴散問題

我們上面提到的好幾種分表方式,都用了 id 這一列作爲分表的依據,這其實就是所謂的分片鍵

實際上我們一般也是用的數據庫主鍵作爲分片鍵

這樣,理想情況下我們已知一個 id,不管是根據哪種規則,我們都能很快定位到該讀哪個分表。

但很多情況下,我們的查詢又不是隻查主鍵,如果我的數據庫表有一列 name,並且加了個普通索引。

這樣我執行下面的 sql

select * from user where name = "小白";

由於 name 並不是分片鍵,我們沒法定位到具體要到哪個分表上去執行 sql。

於是就會對所有分表都執行上面的 sql,當然不會是串行執行 sql,一般都是併發執行 sql 的。

如果我有 100 張表,就執行 100 次 sql。

如果我有 200 張表,就執行 200 次 sql。

隨着我的表越來越多,次數會越來越多,這就是所謂的讀擴散問題

讀擴散問題

這是個比較有趣的問題,它確實是個問題,但大部分的業務不會去處理它,讀 100 次怎麼了,數據增長之後讀的次數會不斷增加又怎麼了?但架不住我的業務不賺錢啊,也根本長不了那麼多數據啊。

話是這麼說沒錯,但面試官問你的時候,你得知道怎麼處理啊。

引入新表來做分表

問題的核心在於,主鍵是分片鍵,而普通索引列並不分片。

那好辦,我們單獨建個新的分片表,這個新表裏的列就只有舊錶的主鍵 id 和普通索引列,而這次換普通索引列來做分片鍵。

通過新索引表解決讀擴散問題

這樣當我們要查詢普通索引列時,先到這個新的分片表裏做一次查詢,就能迅速定位到對應的主鍵 id,然後再拿主鍵 id 去舊的分片表裏查一次數據。這樣就從原來漫無目的的全表擴散查詢,縮減爲只查固定幾個表了。

舉個例子。比如我的表原本長下面這樣,其中 id 列是主鍵,同時也是分片鍵,name 列是非主鍵索引。爲了簡化,假設三條數據一張表。

此時分表裏 id=1,4,6 的都有 的數據。

當我們執行 select * from user where name = "小白";  則需要併發查 3 張表,隨着表變多,查詢次數會變得更多。

舉例說明讀擴散問題

但如果我們爲 name 列建個新表 (nameX),以 name 爲新的分片鍵

這樣我們可以先執行  select id from nameX where name = "小白";  

再拿着結果裏的 ids 去查詢  select * from user where id in (ids);  這樣就算表變多了,也可以迅速定位到某幾張具體的表,減少了查詢次數。

舉例說明通過新索引表解決讀擴散問題

但這個做法的缺點也比較明顯,你需要維護兩套表,並且普通索引列更新時,要兩張表同時進行更改。

有一定的開發量

有沒有更簡單的方案?

使用其他更合適的存儲

我們常規的查詢是通過 id 主鍵去查詢對應的 name 列。而像上面的方案,則通過引入一個新表,倒過來,先用 name 查到對應的 id,再拿 id 去獲取具體的數據。這其實就像是建立了一個新的索引一樣,像這種,通過 name 列反查原數據的思想,其實就很類似於倒排索引

相當於我們是利用了倒排索引的思路去解決分表下的數據查詢問題。

回想下,其實我們的原始需求無非就是在大量數據的場景下依然能提供普通索引列或其他更多維度的查詢。

這種場合,更適合使用 es,es 天然分片,而且內部利用倒排索引的形式來加速數據查詢。

哦?兄弟萌,又是它,倒排索引,又是個極小的細節,做好筆記。

舉個例子,我同樣是一行數據 id,name,age。在 mysql 裏,你得根據 id 分片,如果要支持 name 和 age 的查詢,爲了防止讀擴散,你得分別再建一個 name 的分片表和一個 age 的分片表。

而如果你用 es,它會在它內部以 id 分片鍵進行分片,同時還能建一個 name 到 id,和一個 age 到 id 的倒排索引。這是不是就跟上面做的事情沒啥區別。

而且將 mysql 接入 es 也非常簡單,我們可以通過開源工具 canal 監聽 mysql 的binlog日誌變更,再將數據解析後寫入 es,這樣 es 就能提供近實時的查詢能力。

mysql 同步 es

覺得 es+mysql 還是繁瑣?有沒有其他更簡潔的方案?

有。

別用 mysql 了,改用 tidb 吧,相信大家多少也聽說過這個名稱,這是個分佈式數據庫

它通過引入 Range 的概念進行數據表分片,比如第一個分片表的 id 在 0~2kw,第二個分片表的 id 在 2kw~4kw。

哦?有沒有很熟悉,這不就是文章開頭提到的根據 id 範圍進行數據庫分表嗎?

它支持普通索引,並且普通索引也是分片的,這是不是又跟上面提到的倒排索引方案很類似。

又是個極小的細節。

並且 tidb 跟 mysql 的語法幾乎一致,現在也有非常多現成的工具可以幫你把數據從 mysql 遷移到 tidb。所以開發成本並不高。

用 tidb 替換 mysql

總結

參考資料

《圖解分庫分表》 

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

最後

當年我還在某個遊戲項目組裏做開發的時候,從企鵝那邊挖來的策劃信誓旦旦的說,我們要做的這款遊戲老少皆宜,肯定是爆款。要做成全球同服。上線至少過億註冊十萬人同時在線。要好好規劃和設計。

我們算了下,信他能有個 1 億註冊。用了 id 範圍的方式進行分片,分了 4 張表

搞得我熱血沸騰。

那天晚上下班,夏蟬鳴泣,從赤道吹來的熱風陣陣拂過我的手臂,我聽着澤野弘之的歌,就算是開電瓶車,我都感覺自己像是在開高達。

一年後。

遊戲上線前一天通知運維加機器,怕頂不住,要整夜關注。

後來上線了,全球最高在線人數58人。其中有7個是項目組成員。

還是夏天,還是同樣的下班路,想哭,但我不能哭,因爲騎電瓶車的時候擦眼淚不安全。


最近原創更文的閱讀量穩步下跌,思前想後,夜裏輾轉反側。

小白 debug 答應我,關注之後,好好學技術,別隻是收藏我的表情包。。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DbZdF5c4PyhDwEf7-2rEbQ