新聞中心
今天這篇文章,其實(shí)也是我曾經(jīng)面試中遇到過的真題。

成都創(chuàng)新互聯(lián)公司是創(chuàng)新、創(chuàng)意、研發(fā)型一體的綜合型網(wǎng)站建設(shè)公司,自成立以來公司不斷探索創(chuàng)新,始終堅(jiān)持為客戶提供滿意周到的服務(wù),在本地打下了良好的口碑,在過去的10余年時(shí)間我們累計(jì)服務(wù)了上千家以及全國(guó)政企客戶,如假山制作等企業(yè)單位,完善的項(xiàng)目管理流程,嚴(yán)格把控項(xiàng)目進(jìn)度與質(zhì)量監(jiān)控加上過硬的技術(shù)實(shí)力獲得客戶的一致贊譽(yù)。
分庫(kù)分表大家可能聽得多了,但 讀擴(kuò)散 問題大家了解嗎?
這里涉及到幾個(gè)問題。
分庫(kù)分表是什么?
讀擴(kuò)散問題是什么?
分庫(kù)分表為什么會(huì)引發(fā)讀擴(kuò)散問題?
怎么解決讀擴(kuò)散問題?
這些問題還是比較有意思的。
相信兄弟們也一定有機(jī)會(huì)遇到哈哈哈。
我們先從分庫(kù)分表的話題聊起吧。
分庫(kù)分表
我們平時(shí)做項(xiàng)目開發(fā)。一開始,通常都先用一張數(shù)據(jù)表,而一般來說數(shù)據(jù)表寫到2kw條數(shù)據(jù)之后,底層B+樹的層級(jí)結(jié)構(gòu)就可能會(huì)變高,不同層級(jí)的數(shù)據(jù)頁(yè)一般都放在磁盤里不同的地方,換言之,磁盤IO就會(huì)增多,帶來的便是查詢性能變差。 如果對(duì)上面這句話有疑惑的話,可以去看下我之前寫的文章。
于是,當(dāng)我們單表需要管理的數(shù)據(jù)變得越來越多,就不得不考慮數(shù)據(jù)庫(kù) 分表 。而這里的分表,分為 水平分表和垂直分表 。
垂直分表的原理比較簡(jiǎn)單,一般就是把某幾列拆成一個(gè)新表,這樣單行數(shù)據(jù)就會(huì)變小,B+樹里的單個(gè)數(shù)據(jù)頁(yè)(固定16kb)內(nèi)能放入的行數(shù)就會(huì)變多,從而使單表能放入更多的數(shù)據(jù)。
垂直分表沒有太多可以說的點(diǎn)。下面,我們重點(diǎn)說說最常見的 水平分表 。
水平分表有好幾種做法,但不管是哪種,本質(zhì)上都是將原來的 user 表,變成 user_0, user1, user2 .... uerN 這樣的N多張小表。
從讀寫一張user 大表 ,變成讀寫 user_1 … userN 這樣的N張 小表 。
分表
每一張小表里,只保存一部分?jǐn)?shù)據(jù),但具體保存多少,這個(gè)自己定,一般就訂個(gè) 500w~2kw 。
那分表具體怎么做?
根據(jù)id范圍分表
我認(rèn)為最好用的,是根據(jù)id范圍進(jìn)行分表。
我們假設(shè)每張分表能放 2kw 行數(shù)據(jù)。那user0就放主鍵id為 1~2kw 的數(shù)據(jù)。user1就放id為 2kw+1 ~ 4kw ,user2就放id為 4kw+1 ~ 6kw , userN就放 2N kw+1 ~ 2(N+1)kw 。
根據(jù)id范圍分表
假設(shè)現(xiàn)在有條數(shù)據(jù),id=3kw,將這個(gè) 3kw除2kw = 1.5 ,向下取整得到 1 ,那就可以得到這條數(shù)據(jù)屬于 user1表 。于是去讀寫user1表就行了。這就完成了數(shù)據(jù)的路由邏輯,我們把這部分邏輯封裝起來,放在數(shù)據(jù)庫(kù)和業(yè)務(wù)代碼之間。
這樣。 對(duì)于業(yè)務(wù)代碼來說 ,它只知道自己在讀寫一張 user 表,根本不知道底下還分了那么多張小表。
對(duì)于數(shù)據(jù)庫(kù)來說,它并不知道自己被分表了,它只知道有那么幾張表,正好名字長(zhǎng)得比較像而已。
這還只是在 一個(gè)數(shù)據(jù)庫(kù) 里做分表,如果范圍再搞大點(diǎn),還能在 多個(gè)數(shù)據(jù)庫(kù) 里做分表,這就是所謂的 分庫(kù)分表 。
不管是單庫(kù)分表還是分庫(kù)分表,都可以通過這樣一個(gè)中間層邏輯做路由。
還真的就應(yīng)了那句話,沒有什么是加中間層不能解決的。
如果有,就多加一層。
至于這個(gè)中間層的實(shí)現(xiàn)方式就更靈活了,它既可以像 第三方orm庫(kù) 那樣加在業(yè)務(wù)代碼中。
通過orm讀寫分表
也可以在mysql和業(yè)務(wù)代碼之間加個(gè) proxy服務(wù) 。
如果是通過第三方orm庫(kù)的方式來做的話,那需要根據(jù)不同語言實(shí)現(xiàn)不同的代碼庫(kù),所以不少?gòu)S都選擇后者加個(gè)proxy的方式,這樣就不需要關(guān)心上游服務(wù)用的是什么語言。
通過proxy管理分表
根據(jù)id取模分表
這時(shí)候就有兄弟要提出問題了,"我看很多方案都 對(duì)id取模 ,你這個(gè)方案是不是不完整?"。
取模的方案也是很常見的。
比如一個(gè)id=31進(jìn)來,我們一共分了5張表,分別是user0到user4。對(duì) 31%5=1 ,取模得 1 ,于是就能知道應(yīng)該讀寫 user1 表。
根據(jù)id取模分表
優(yōu)點(diǎn)當(dāng)然是比較簡(jiǎn)單。而且讀寫數(shù)據(jù)都可以很均勻的分?jǐn)偟矫總€(gè)分表上。
但 缺點(diǎn) 也比較明顯,如果想要擴(kuò)展表的個(gè)數(shù),比如從5張表變成8張表。那同樣還是id=31的數(shù)據(jù), 31%8 = 7 ,就需要讀寫user7這張表。跟原來就對(duì)不上了。
這就需要考慮 數(shù)據(jù)遷移 的問題。很頭禿。
為了避免后續(xù)擴(kuò)展的問題,我見過一些業(yè)務(wù)一開始就將數(shù)據(jù)預(yù)估得很大,然后心一橫,分成100張表,一張表如果存?zhèn)€2kw條,那也能存20億數(shù)據(jù)了。
也不是說這樣不行吧,就是這個(gè)業(yè)務(wù)直到最后放棄的時(shí)候,也就存了百萬條數(shù)據(jù),每次打開數(shù)據(jù)庫(kù)表能看到茫茫多的user_xx,就是不太舒服,專業(yè)點(diǎn),叫增加了程序員的 心智負(fù)擔(dān) 。
而上面一種方式,根據(jù)id范圍去分表,就能很好的解決這些問題,數(shù)據(jù)少的時(shí)候,表也少,隨著數(shù)據(jù)增多,表會(huì)慢慢變多。而且這樣表還可以無限擴(kuò)展。
那是不是說取模的做法就用不上了呢?
也不是。
將上面兩種方式結(jié)合起來
id取模的做法,最大的好處是,新寫入的數(shù)據(jù)都是實(shí)實(shí)在在的分散到了 多張表 上。
而根據(jù)id范圍去做分表,因?yàn)閕d是遞增的,那新寫入的數(shù)據(jù)一般都會(huì)落到 某一張表 上,如果你的業(yè)務(wù)場(chǎng)景寫數(shù)據(jù)特別頻繁,那這張表就會(huì)出現(xiàn) 寫熱點(diǎn) 的問題。
這時(shí)候就可以將id取模和id范圍分表的方式結(jié)合起來。
我們可以在某個(gè)id范圍里,引入取模的功能。比如 以前 2kw~4kw 是user1表,現(xiàn)在可以在這個(gè)范圍 再分成5個(gè)表 ,也就是引入user1-0, user1-2到user1-4,在這5個(gè)表里取模。
舉個(gè)例子,id=3kw,根據(jù)范圍,會(huì)分到user1表,然后再進(jìn)行取模 3kw % 5 = 0,也就是讀寫user1-0表。
這樣就可以將寫單表分?jǐn)倿閷懚啾怼?/p>
這在分庫(kù)的場(chǎng)景下優(yōu)勢(shì)會(huì)更明顯,不同的庫(kù),可以把服務(wù)部署到不同的機(jī)器上,這樣各個(gè)機(jī)器的性能都能被用起來。
根據(jù)id范圍分表后再取模
讀擴(kuò)散問題
我們上面提到的好幾種分表方式,都用了id這一列作為 分表的依據(jù) ,這其實(shí)就是所謂的 分片鍵 。
實(shí)際上我們一般也是用的 數(shù)據(jù)庫(kù)主鍵 作為 分片鍵 。
這樣,理想情況下我們已知一個(gè)id,不管是根據(jù)哪種規(guī)則,我們都能很快定位到該讀哪個(gè)分表。
但很多情況下,我們的查詢又不是只查主鍵,如果我的數(shù)據(jù)庫(kù)表有一列name,并且加了個(gè)普通索引。
這樣我執(zhí)行下面的sql
select * from user where name = "小白";
由于name并不是分片鍵,我們沒法定位到具體要到哪個(gè)分表上去執(zhí)行sql。
于是就會(huì)對(duì) 所有分表 都執(zhí)行上面的sql,當(dāng)然不會(huì)是串行執(zhí)行sql,一般都是 并發(fā) 執(zhí)行sql的。
如果我有100張表,就執(zhí)行100次sql。
如果我有200張表,就執(zhí)行200次sql。
隨著我的表越來越多,次數(shù)會(huì)越來越多,這就是所謂的 讀擴(kuò)散問題 。
讀擴(kuò)散問題
這是個(gè)比較有趣的問題,它確實(shí)是個(gè)問題,但大部分的業(yè)務(wù)不會(huì)去處理它,讀100次怎么了,數(shù)據(jù)增長(zhǎng)之后讀的次數(shù)會(huì)不斷增加又怎么了?但架不住我的 業(yè)務(wù)不賺錢 啊,也根本 長(zhǎng)不了那么多數(shù)據(jù) 啊。
話是這么說沒錯(cuò),但面試官問你的時(shí)候,你得知道怎么處理啊。
引入新表來做分表
問題的核心在于,主鍵是分片鍵,而普通索引列并不分片。
那好辦,我們單獨(dú)建個(gè) 新的分片表 ,這個(gè)新表里的列就只有舊表的主鍵id和普通索引列,而這次換普通索引列來做分片鍵。
通過新索引表解決讀擴(kuò)散問題
這樣當(dāng)我們要查詢普通索引列時(shí),先到這個(gè)新的分片表里做一次查詢,就能迅速定位到對(duì)應(yīng)的主鍵id,然后再拿主鍵id去舊的分片表里查一次數(shù)據(jù)。這樣就從原來漫無目的的全表擴(kuò)散查詢,縮減為只查固定幾個(gè)表了。
舉個(gè)例子。比如我的表原本長(zhǎng)下面這樣,其中id列是主鍵,同時(shí)也是分片鍵,name列是非主鍵索引。為了簡(jiǎn)化,假設(shè)三條數(shù)據(jù)一張表。
此時(shí)分表里 id=1,4,6 的都有 name="小白" 的數(shù)據(jù)。
當(dāng)我們執(zhí)行 select * from user where name = "小白"; 則需要并發(fā)查3張表,隨著表變多,查詢次數(shù)會(huì)變得更多。
舉例說明讀擴(kuò)散問題
但如果我們?yōu)閚ame列 建個(gè)新表(nameX),以name為新的分片鍵 。
這樣我們可以先執(zhí)行 select id from nameX where name = "小白";
再拿著結(jié)果里的ids去查詢 select * from user where id in (ids); 這樣就算表變多了,也可以迅速定位到某幾張具體的表,減少了查詢次數(shù)。
舉例說明通過新索引表解決讀擴(kuò)散問題
但這個(gè)做法的缺點(diǎn)也比較明顯,你需要維護(hù)兩套表,并且普通索引列更新時(shí),要兩張表同時(shí)進(jìn)行更改。
有一定的開發(fā)量
有沒有更簡(jiǎn)單的方案?
使用其他更合適的存儲(chǔ)
我們常規(guī)的查詢是通過id主鍵去查詢對(duì)應(yīng)的name列。而像上面的方案,則通過引入一個(gè)新表, 倒過來 ,先用name查到對(duì)應(yīng)的id,再拿id去獲取具體的數(shù)據(jù)。這其實(shí)就像是建立了一個(gè)新的索引一樣,像這種,通過name列反查原數(shù)據(jù)的思想,其實(shí)就很類似于 倒排索引 。
相當(dāng)于我們是利用了倒排索引的思路去解決分表下的數(shù)據(jù)查詢問題。
回想下,其實(shí)我們的 原始需求 無非就是在大量數(shù)據(jù)的場(chǎng)景下依然能提供普通索引列或其他更多維度的查詢。
這種場(chǎng)合,更適合使用es,es天然分片,而且內(nèi)部利用 倒排索引 的形式來加速數(shù)據(jù)查詢。
哦?兄弟萌,又是它, 倒排索引 ,又是個(gè)極小的細(xì)節(jié),做好筆記。
舉個(gè)例子,我同樣是一行數(shù)據(jù) id,name,age。在mysql里,你得根據(jù)id分片,如果要支持name和age的查詢,為了防止讀擴(kuò)散,你得分別再建一個(gè)name的分片表和一個(gè)age的分片表。
而如果你用es,它會(huì)在它內(nèi)部以id分片鍵進(jìn)行分片,同時(shí)還能建一個(gè)name到id,和一個(gè)age到id的倒排索引。這是不是就跟上面做的事情沒啥區(qū)別。
而且將mysql接入es也非常簡(jiǎn)單,我們可以通過開源工具 canal 監(jiān)聽mysql的 binlog 日志變更,再將數(shù)據(jù)解析后寫入es,這樣es就能提供 近實(shí)時(shí) 的查詢能力。
mysql同步es
覺得es+mysql還是繁瑣?有沒有其他更簡(jiǎn)潔的方案?
有。
別用mysql了,改用 tidb 吧,相信大家多少也聽說過這個(gè)名稱,這是個(gè) 分布式數(shù)據(jù)庫(kù) 。
它通過引入 Range 的概念進(jìn)行數(shù)據(jù)表分片,比如第一個(gè)分片表的id在0~2kw,第二個(gè)分片表的id在2kw~4kw。
哦?有沒有很熟悉,這不就是文章開頭提到的根據(jù)id范圍進(jìn)行數(shù)據(jù)庫(kù)分表嗎?
它支持普通索引,并且普通索引也是分片的,這是不是又跟上面提到的倒排索引方案很類似。
又是個(gè)極小的細(xì)節(jié)。
并且tidb跟mysql的語法幾乎一致,現(xiàn)在也有非常多現(xiàn)成的工具可以幫你把數(shù)據(jù)從mysql遷移到tidb。所以開發(fā)成本并不高。
用tidb替換mysql
總結(jié)
mysql在單表數(shù)據(jù)過大時(shí),查詢性能會(huì)變差,因此當(dāng)數(shù)據(jù)量變得巨大時(shí),需要考慮水平分表。
水平分表需要選定一個(gè)分片鍵,一般選擇主鍵,然后根據(jù)id進(jìn)行取模,或者根據(jù)id的范圍進(jìn)行分表。
mysql水平分表后,對(duì)于非分片鍵字段的查詢會(huì)有讀擴(kuò)散的問題,可以用普通索引列作分片鍵建一個(gè)新表,先查新表拿到id后再回到原表再查一次原表。這本質(zhì)上是借鑒了倒排索引的思路。
如果想要支持更多維度的查詢,可以監(jiān)聽mysql的binlog,將數(shù)據(jù)寫入到es,提供近實(shí)時(shí)的查詢能力。
當(dāng)然,用tidb替換mysql也是個(gè)思路。tidb屬實(shí)是個(gè)好東西,不少?gòu)S都拿它換個(gè)皮貼個(gè)標(biāo),做成自己的 自研數(shù)據(jù)庫(kù) ,非常推薦大家學(xué)習(xí)一波。
不要做過早的優(yōu)化,沒事別上來就分100個(gè)表,很多時(shí)候真用不上。
參考資料
《圖解分庫(kù)分表》
https://mp.weixin.qq.com/s/OI5y4HMTuEZR1hoz9aOMxg
最后
當(dāng)年我還在某個(gè)游戲項(xiàng)目組里做開發(fā)的時(shí)候,從企鵝那邊挖來的策劃信誓旦旦的說,我們要做的這款游戲老少皆宜,肯定是爆款。要做成全球同服。上線至少 過億注冊(cè) , 十萬人同時(shí)在線 。要好好規(guī)劃和設(shè)計(jì)。
我們算了下,信他能有個(gè)1億注冊(cè)。用了id范圍的方式進(jìn)行分片,分了 4張表 。
搞得我熱血沸騰。
那天晚上下班,夏蟬鳴泣,從赤道吹來的熱風(fēng)陣陣拂過我的手臂,我聽著澤野弘之的歌,就算是開電瓶車,我都感覺自己像是在開高達(dá)。
一年后。
游戲上線前一天通知運(yùn)維加機(jī)器,怕頂不住,要整夜關(guān)注。
后來上線了,全球最高在線人數(shù) 58 人。其中有 7 個(gè)是項(xiàng)目組成員。
還是夏天,還是同樣的下班路,想哭,但我不能哭,因?yàn)轵T電瓶車的時(shí)候擦眼淚不安全。
網(wǎng)頁(yè)標(biāo)題:光知道分庫(kù)分表可不敢直接去面試,分表后讀擴(kuò)散怎么解決才是重點(diǎn)
網(wǎng)站網(wǎng)址:http://www.fisionsoft.com.cn/article/djieghc.html


咨詢
建站咨詢
