隨著互聯網的高速發展,帶來了海量資料存盤的問題,比如像物聯網行業,每個智能終端每天進行資料采集和上報,每天能夠產幾千萬甚至上億的資料,在互聯網電商行業,或者一些O2O平臺,每天也能產生上千萬的訂單資料,這些量級的資料在傳統的關系型資料庫中已經無法支撐了,那么如何解決海量資料存盤和計算等問題,在業內引入了分布式存盤和分布式計算等解決方案,特別是NoSql的生態,我在前面講過的k-v資料庫、檔案資料庫、圖形資料庫等,都是比較主流的分布式資料庫解決方案,
即便如此,關系型資料庫仍然有它不可替代的特性,所以關系型資料庫仍然是核心業務的基礎資料平臺,因此關系型資料庫必然會面臨資料量日益增長帶來的海量資料處理問題,
Mysql資料庫海量資料帶來的性能問題
目前幾乎所有的互聯網公司都是采用mysql這個開源資料庫,根據阿里巴巴的《Java開發手冊》上提到的,當單表行數超過500W行或者單表資料容量超過2G時,就會對查詢性能產生較大影響,這個時候建議對表進行優化,
其實500W資料只是一個折中的值,具體的資料量和資料庫服務器配置以及mysql配置有關,因為Mysql為了提升性能,會把表的索引裝載到記憶體,innodb_buffer_pool_size 足夠的情況下,mysql能把全部資料加載進記憶體,查詢不會有問題,
但是,當單表資料庫到達某個量級的上限時,導致記憶體無法存盤其索引,使得之后的 SQL 查詢會產生磁盤 IO,從而導致性能下降,當然,這個還有具體的表結構的設計有關,最終導致的問題都是記憶體限制,這里,增加硬體配置,可能會帶來立竿見影的性能提升,
innodb_buffer_pool_size 包含資料快取、索引快取等,
Mysql常見的優化手段
當然,我們首先要進行的優化是基于Mysql本身的優化,常見的優化手段有:
- 增加索引,索引是直觀也是最快速優化檢索效率的方式,
- 基于Sql陳述句的優化,比如最左匹配原則,用索引欄位查詢、降低sql陳述句的復雜度等
- 表的合理設計,比如符合三范式、或者為了一定的效率破壞三范式設計等
- 資料庫引數優化,比如并發連接數、資料刷盤策略、調整快取大小
- 資料庫服務器硬體升級
- mysql大家主從復制方案,實作讀寫分離
這些常見的優化手段,在資料量較小的情況下效果非常好,但是資料量到達一定瓶頸時,常規的優化手段已經解決不了實際問題,那怎么辦呢?
大資料表優化方案
對于大資料表的優化最直觀的方式就是減少單表資料量,所以常見的解決方案是:
-
分庫分表,大表拆小表,
-
冷熱資料分離,所謂的冷熱資料,其實就是根據訪問頻次來劃分的,訪問頻次較多的資料是熱資料,訪問頻次少的資料是冷資料,冷熱資料分離就是把這兩類資料分離到不同的表中,從而減少熱資料表的大小,
其實在很多地方大家都能看到類似的實作,比如去一些網站查詢訂單或者交易記錄,默認只允許查詢1到3個月,3個月之前的資料,基本上大家都很少關心,訪問頻次較少,所以可以把3個月之前的資料保存到冷庫中,
-
歷史資料歸檔,簡單來說就是把時間比較久遠的資料分離出來存檔,保證實時庫的資料的有效生命周期,
其實這些解決方案都是屬于偏業務類的方案,并不完全是技術上的方案,所以在實施的時候,需要根據業務的特性來選擇合適的方式,
詳解分庫分表
分庫分表是非常常見針對單個資料表資料量過大的優化方式,它的核心思想是把一個大的資料表拆分成多個小的資料表,這個程序也叫(資料分片),它的本質其實有點類似于傳統資料庫中的磁區表,比如mysql和oracle都支持磁區表機制,
分庫分表是一種水平擴展手段,每個分片上包含原來總的資料集的一個子集,這種分而治之的思想在技術中很常見,比如多CPU、分布式架構、分布式快取等等,像前面我們講redis cluster集群時,slot槽的分配就是一種資料分片的思想,
如圖6-1所示,資料庫分庫分表一般有兩種實作方式:
- 水平拆分,基于表或欄位劃分,表結構不同,有單庫的分表,也有多庫的分庫,
- 垂直拆分,基于資料劃分,表結構相同,資料不同,也有同庫的水平切分和多庫的切分,

垂直拆分
垂直拆分有兩種,一種是單庫的垂直拆分,另一種是多個資料庫的垂直拆分,
單庫垂直分表
單個表的欄位數量建議控制在20~50個之間,之所以建議做這個限制,是因為如果欄位加上資料累計的長度超過一個閾值后,資料就不是存盤在一個頁上,就會產生分頁的問題,而這個問題會導致查詢性能下降,
所以如果當某些業務表的欄位過多時,我們一般會拆去垂直拆分的方式,把一個表的欄位拆分成多個表,如圖6-2所示,把一個訂單表垂直拆分成一個訂單主表和一個訂單明細表,

在Innodb引擎中,單表欄位最大限制為1017
參考: https://dev.mysql.com/doc/mysql-reslimits-excerpt/5.6/en/column-count-limit.html
多庫垂直分表
多庫垂直拆分實際上就是把存在于一個庫中的多個表,按照一定的緯度拆分到多個庫中,如圖6-3所示,這種拆分方式在微服務架構中也是很常見,基本上會按照業務緯度拆分資料庫,同樣該緯度也會影響到微服務的拆分,基本上服務和資料庫是獨立的,

多庫垂直拆分最大的好處就是實作了業務資料的隔離,其次就是緩解了請求的壓力,原本所有的表在一個庫的時候,所有請求都會打到一個資料庫服務器上,通過資料庫的拆分,可以分攤掉請求,在這個層面上提升了資料庫的吞吐能力,
水平拆分
垂直拆分的方式并沒有解決單表資料量過大的問題,所以我們還需要通過水平拆分的方式把大表資料做資料分片,
水平切分也可以分成兩種,一種是單庫的,一種是多庫的,
單庫水平分表
如圖6-4所示,表示把一張有10000條資料的用戶表,按照某種規則拆分成了4張表,每張表的資料量是2500條,

兩個案例:
銀行的交易流水表,所有進出的交易都需要登記這張表,因為絕大部分時候客戶都是查詢當天的交易和一個月以內的交易資料,所以我們根據使用頻率把這張表拆分成三張表:
當天表:只存盤當天的資料,
當月表:我們在夜間運行一個定時任務,前一天的資料,全部遷移到當月表,用的是insert into select,然后delete,
歷史表:同樣是通過定時任務,把登記時間超過30天的資料,遷移到history歷史表(歷史表的資料非常大,我們按照月度,每個月建立磁區),
費用表:消費金融公司跟線下商戶合作,給客戶辦理了貸款以后,消費金融公司要給商戶返費用,或者叫提成,每天都會產生很多的費用的資料,為了方便管理,我們每個月建立一張費用表,例如fee_detail_201901……fee_detail_201912,
但是注意,跟磁區一樣,這種方式雖然可以一定程度解決單表查詢性能的問題,但是并不能解決單機存盤瓶頸的問題,
多庫水平分表
多庫水平分表,其實有點類似于分庫分表的綜合實作方案,從分表來說是減少了單表的資料量,從分庫層面來說,降低了單個資料庫訪問的性能瓶頸,如圖6-5所示,

常見的水平分表策略
分庫更多的是關注業務的耦合度,也就是每個庫應該放那些表,是由業務耦合度來決定的,這個在前期做領域建模的時候都會先考慮好,所以問題不大,只是分庫之后帶來的其他問題,我們在后續內容中來分析,
而分表這塊,需要考慮的問題會更多一些,也就是我們應該根據什么樣的策略來水平分表?這里就需要涉及到分表策略了,下面簡單介紹幾種最常見的分片策略,
哈希取模分片
哈希分片,其實就是通過表中的某一個欄位進行hash演算法得到一個哈希值,然后通過取模運算確定資料應該放在哪個分片中,如圖6-6所示,這種方式非常適合隨機讀寫的場景中,它能夠很好的將一個大表的資料隨機分散到多個小表,

hash取模的問題
hash取模運算有個比較嚴重的問題,假設根據當前資料表的量以及增長情況,我們把一個大表拆分成了4個小表,看起來滿足目前的需求,但是經過一段時間的運行后,發現四個表不夠,需要再增加4個表來存盤,這種情況下,就需要對原來的資料進行整體遷移,這個程序非常麻煩,
一般為了減少這種方式帶來的資料遷移的影響,我們會采用一致性hash演算法,
一致性hash演算法
在前面我們講的hash取模演算法,實際上對目標表或者目標資料庫進行hash取模,一旦目標表或者資料庫發生數量上的變化,就會導致所有資料都需要進行遷移,為了減少這種大規模的資料影響,才引入了一致性hash演算法,
如圖6-7所示,簡單來說,一致性哈希將整個哈希值空間組織成一個虛擬的圓環,如假設某哈希函式H的值空間為0-232-1(即哈希值是一個32位無符號整形),什么意思呢?
就是我們通過0-232-1的數字組成一個虛擬的圓環,圓環的正上方的點代表0,0點右側的第一個點代表1,以此類推,2、3、4、5、6……直到232-1,也就是說0點左側的第一個點代表232-1,我們把這個由2的32次方個點組成的圓環稱為hash環,

那一致性hash演算法和上面的虛擬環有什么關系呢?繼續回到前面我們講解hash取模的例子,假設現在有四個表,table_1、table_2、table_3、table_4,在一致性hash演算法中,取模運算不是直接對這四個表來完成,而是對232來實作,
hash(table編號)%232
通過上述公式算出的結果一定是一個0到232-1之間的一個整數,然后在這個數對應的位置標注目標表,如圖6-8所示,四個表通過hash取模之后分別落在hash環的某個位置上,

好了,到目前為止,我們已經把目標表與hash環聯系在了一起,那么接下來我們需要把一條資料保存到某個目標表中,怎么做呢?如圖6-9所示,當添加一條資料時,同樣通過hash和hash環取模運算得到一個目標值,然后根據目標值所在的hash環的位置順時針查找最近的一個目標表,把資料存盤到這個目標表中即可,

不知道大家是否發現了一致性hash的好處,就是hash運算不是直接面向目標表,而是面向hash環,這樣的好處就是當需要洗掉某張表或者增加表的時候,對于整個資料變化的影響是區域的,而不是全域,舉個例子,假設我們發現需要增加一張表table_04,如圖6-10所示,增加一個表,并不會對其他四個已經產生了資料的表造成影響,原來已經分片的資料完全不需要做任何改動,
如果需要洗掉一個節點,同樣只會影響洗掉節點本身的資料,前后表的資料完全不受影響,

hash環偏斜
上述設計有一個問題,理論情況下我們目標表是能夠均衡的分布在整個hash環中,但實際情況有可能是圖6-11所示的樣子,也就是產生了hash環偏斜的現象,這種現象導致的問題就是大量的資料都會保存到同一個表中,倒是資料分配極度不均勻,

為了解決這個問題,必須要保證目標節點要均勻的分布在整個hash環中,但是真實的節點就只有4個,如何均勻分布呢?最簡單的方法就是,把這四個節點分別復制一份出來分散到這個hash環中,這個復制出來的節點叫虛擬節點,根據實際需要可以虛擬出多個節點出來,如圖6-12所示,

按照范圍分片
按范圍分片,其實就是基于資料表的業務特性,按照某種范圍拆分,這個范圍的有很多含義,比如:
- 時間范圍,比如我們按照資料創建時間,按照每一個月保存一個表,基于時間劃分還可以用來做冷熱資料分離,越早的資料訪問頻次越少,
- 區域范圍,區域一般指的是地理位置,比如一個表里面存盤了來自全國各地的資料,如果資料量較大的情況下,可以按照地域來劃分多個表,
- 資料范圍,比如根據某個欄位的資料區間來進行劃分,
如圖6-7所示,表示按照資料范圍進行拆分,

范圍分片最終要的是選擇一個合適的分片鍵,這個是否合適來自于業務需求,比如之前有個學員是在做智能家居的,他們賣的是硬體設備,這些設備會采集資料上報到服務器上,當來自全國范圍的資料統一保存在一個表中后,資料量達到了億級別,所以這種場景比較適合按照城市和地域來拆分,
分庫分表實戰
為了讓大家理解分庫分表以及實操,我們通過一個簡單的案例來演示一下,代碼詳見:springboot-split-table-example專案
假設存在一個用戶表,用戶表的欄位如下,
該表主要提供注冊、登錄、查詢、修改等功能,

該表的具體的業務情況如下(需要注意,在進行分表之前,需要了解業務層面對這個表的使用情況,然后再決定使用什么樣的方案,否則脫離業務去設計技術方案是耍流氓)
用戶端: 前臺訪問量較大,主要涉及兩類請求:
- 用戶登錄,面向C端,對可用性和一致性要求較高,主要通過login_name、email、phone來查詢用戶資訊,1%的請求屬于這種型別
- 用戶資訊查詢,登錄成功后,通過uid來查詢用戶資訊,99%屬于這種型別,
運營端: 主要是運營后臺的資訊訪問,需要支持根據性別、手機號、注冊時間、用戶昵稱等進行分頁查詢,由于是內部系統,訪問量較低,對可用性一致性要求不高,
根據uid進行水平分表
由于99%的請求是基于uid進行用戶資訊查詢,所以毫無疑問我們選擇使用uid進行水平分表,那么這里我們采用uid的hash取模方法來進行分表,具體的實施如圖6-9所示,根據uid進行一致性hash取模運算得到目標表進行存盤,

按照圖6-9的結構,分別復制user_info表,重新命名為01~04,如圖6-10所示,

如何生成全域唯一id
當完成上述動作后,就需要開始開始落地實施,這里需要考慮在資料添加、修改、洗掉時,要正確路由到目標資料表,其次是老資料的遷移,
老資料遷移,一般我們是寫一個腳本或者一個程式,把舊表中的資料查詢出來,然后根據分表規則重新路由分發到新的表中,這里不是很復雜,就不做展開說明,我們重點說一下資料添加/修改/洗掉的路由,
在實施之前,我們需要先考慮一個非常重要的問題,就是在單個表中,我們使用遞增主鍵來保證資料的唯一性,但是如果把資料拆分到了四個表,每個表都采用自己的遞增主鍵規則,就會存在重復id的問題,也就是說遞增主鍵不是全域唯一的,
我們需要知道一個點是,user_info雖然拆分成了多張表,但是本質上它應該還是一個完整的資料整體,當id存在重復的時候,就失去了資料的唯一性,因此我們需要考慮如何生成一個全域唯一ID,
如何實作全域唯一ID
全域唯一ID的特性就是能夠保證ID的唯一性,那么基于這個特性,我們可以輕松找到很多的解決方案,
- 資料庫自增ID(定義全域表)
- UUID
- Redis的原子遞增
- Twitter-Snowflake演算法
- 美團的leaf
- MongoDB的ObjectId
- 百度的UidGenerator
分布式ID的特性
- 唯一性:確保生成的ID是全域唯一的,
- 有序遞增性:確保生成的ID是對于某個用戶或者業務是按一定的數字有序遞增的,
- 高可用性:確保任何時候都能正確的生成ID,
- 帶時間:ID里面包含時間,一眼掃過去就知道哪天的資料
資料庫自增方案
在資料庫中專門創建一張序串列,利用資料庫表中的自增ID來為其他業務的資料生成一個全域ID,那么每次要用ID的時候,直接從這個表中獲取即可,
CREATE TABLE `uid_table` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`business_id` int(11) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE (business_type)
)
在應用程式中,每次呼叫下面這段代碼,就可以持續獲得一個遞增的ID,
begin;
REPLACE INTO uid_table (business_id) VALUES (2);
SELECT LAST_INSERT_ID();
commit;
其中,replace into是每次洗掉原來相同的資料,同時加1條,就能保證我們每次得到的就是一個自增的ID
這個方案的優點是非常簡單,它也有缺點,就是對于資料庫的壓力比較大,而且最好是獨立部署一個DB,而獨立部署又會增加整體的成本,這個在美團的leaf里面設計了一個很巧妙的設計方案,后面再講
優點:
- 非常簡單,利用現有資料庫系統的功能實作,成本小,有DBA專業維護,
- ID號單調自增,可以實作一些對ID有特殊要求的業務,
缺點:
- 強依賴DB,當DB例外時整個系統不可用,屬于致命問題,配置主從復制可以盡可能的增加可用性,但是資料一致性在特殊情況下難以保證,主從切換時的不一致可能會導致重復發號,
- ID發號性能瓶頸限制在單臺MySQL的讀寫性能,
UUID
UUID的格式是: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 8-4-4-4-12共36個字符,它是一個128bit的二進制轉化為16進制的32個字符,然后用4個-連接起來的字串,
UUID的五種生成方式
- 基于時間的UUID(date-time & MAC address): 主要依賴當前的時間戳及機器mac地址,因此可以保證全球唯一性,(使用了Mac地址,因此會暴露Mac地址和生成時間,)
- 分布式安全的UUID(date-time & group/user id)將版本1的時間戳前四位換為POSIX的UID或GID,
- 基于名字空間的UUID-MD5版(MD5 hash & namespace),基于指定的名字空間/名字生成MD5散列值得到,標準不推薦,
- 基于亂數的UUID(pseudo-random number):基于亂數或偽亂數生成,
- 基于名字空間的UUID-SHA1版(SHA-1 hash & namespace):將版本3的散列演算法改為SHA1
在Java中,提供了基于MD5演算法的UUID、以及基于亂數的UUID,
優點:
- 本地生成,沒有網路消耗,生成簡單,沒有高可用風險,
缺點:
- 不易于存盤:UUID太長,16位元組128位,通常以36長度的字串表示,很多場景不適用,
- 資訊不安全:基于MAC地址生成UUID的演算法可能會造成MAC地址泄露,這個漏洞曾被用于尋找梅麗莎病毒的制作者位置,
- 無序查詢效率低:由于生成的UUID是無序不可讀的字串,所以其查詢效率低,
- UUID不適合用來做資料庫的唯一ID,如果用UUID做主鍵,無序的不遞增,大家都知道,主鍵是有索引的,然后mysql的索引是通過b+樹來實作的,每一次新的UUID資料的插入,為了查詢的優化,都會對索引底層的b+樹進行修改,因為UUID資料是無序的,所以每一次UUID資料的插入都會對主鍵的b+樹進行很大的修改,嚴重影響性能
雪花演算法
SnowFlake 演算法,是 Twitter 開源的分布式 id 生成演算法,其核心思想就是:使用一個 64 bit 的 long 型的數字作為全域唯一 id,雪花演算法比較常見,在百度的UidGenerator、美團的Leaf中,都有用到雪花演算法的實作,
如圖6-11所示,表示雪花演算法的組成,一共64bit,這64個bit位由四個部分組成,
-
第一部分,1bit位,用來表示符號位,而ID一般是正數,所以這個符號位一般情況下是0,
-
第二部分,占41 個 bit:表示的是時間戳,是系統時間的毫秒數,但是這個時間戳不是當前系統的時間,而是當前
系統時間-開始時間,更大的保證這個ID生成方案的使用的時間! -
那么我們為什么需要這個時間戳,目的是為了保證有序性,可讀性,我一看我就能猜到ID是什么時候生成的,
41位可以241 - 1表示個數字,
如果只用來表示正整數(計算機中正數包含0),可以表示的數值范圍是:0 至 241-1,減1是因為可表示的數值范圍是從0開始算的,而不是1,
也就是說41位可以表示241-1個毫秒的值,轉化成單位年則是(241-1)/1000 * 60 * 60 * 24 *365=69年,也就是能容納69年的時間
-
第三部分,用來記錄作業機器id,id包含10bit,意味著這個服務最多可以部署在 2^10 臺機器上,也就是 1024 臺機器,
其中這10bit又可以分成2個5bit,前5bit表示機房id、5bit表示機器id,意味著最多支持2^5個機房(32),每個機房可以支持32臺機器,
-
第四部分,第四部分由12bit組成,它表示一個遞增序列,用來記錄同毫秒內產生的不同id,
那么我們為什么需要這個序列號,設想下,如果是同一毫秒同一臺機器來請求,那么我們怎么保證他的唯一性,這個時候,我們就能用到我們的序列號,
目的是為了保證同一毫秒內同一機器生成的ID是唯一的,這個其實就是為了滿足我們ID的這個高并發,就是保證我同一毫秒進來的并發場景的唯一性
12位(bit)可以表示的最大正整數是2^12-1=4095,即可以用0、1、2、3、....4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號,
12位2進制,如果全部都是1的情況下,那么最終的值就是4095,也就是12bit能夠存盤的最大的數字是4095.

分庫分表之后的資料DML操作
有序需要用到全域id,所以在user_info表需要添加一個唯一id的欄位,

配置完成之后,在如下代碼中引入signal方法,
@Slf4j
@RestController
@RequestMapping("/users")
public class UserInfoController {
@Autowired
IUserInfoService userInfoService;
SnowFlakeGenerator snowFlakeGenerator=new SnowFlakeGenerator(1,1,1);
@PostMapping("/batch")
public void user(@RequestBody List<UserInfo> userInfos){
log.info("begin UserInfoController.user");
userInfoService.saveBatch(userInfos);
}
@PostMapping
public void signal(@RequestBody UserInfo userInfo){
Long bizId=snowFlakeGenerator.nextId();
userInfo.setBizId(bizId);
String table=ConsistentHashing.getServer(bizId.toString());
log.info("UserInfoController.signal:{}",table);
MybatisPlusConfig.TABLE_NAME.set(table);
userInfoService.save(userInfo);
}
}
并且,需要增加一個mybatis攔截器,針對user_info表進行攔截和替換,從而實作動態表的路由,
@Configuration
public class MybatisPlusConfig {
public static ThreadLocal<String> TABLE_NAME = new ThreadLocal<>();
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
Map<String, TableNameHandler> tableNameHandlerMap = new HashMap<>();
tableNameHandlerMap.put("user_info", (sql, tableName) -> TABLE_NAME.get());
dynamicTableNameInnerInterceptor.setTableNameHandlerMap(tableNameHandlerMap);
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
return interceptor;
}
}
至此,一個基礎的分庫分表的演練就完成了,但問題仍然還未完全解決,
非分片鍵查詢
我們對user_info表的分片,是基于biz_id來實作的,也就是意味著如果我們想查詢某張表的資料,必須先要使用biz_id路由找到對應的表才能查詢到,
那么問題來了,如果查詢的欄位不是分片鍵(也就是不是biz_id),比如本次分庫分表實戰案例中,運營端查詢就有根據名字、手機號、性別等欄位來查,這時候我們并不知道去哪張表查詢這些資訊,
非分片鍵和分片鍵建立映射關系
第一種解決辦法就是,把非分片鍵和分片鍵建立映射關系,比如login_name -> biz_id 建立映射,相當于建立一個簡單的索引,當基于login_name查詢資料時,先通過映射表查詢出login_name對應的biz_id,再通過biz_id定位到目標表,
映射表的只有兩列,可以成再很多的資料,當資料量過大時,也可以對映射表做水平拆分, 同時這種映射關系其實就是k-v鍵值對的關系,所以我們可以使用k-v快取來存盤提升性能,
同時因為這種映射關系的變更頻率很低,所以快取命中率很高,性能也很好,
用戶端資料庫和運營端資料庫進行分離
運營端的查詢可能不止于單個欄位的映射來查詢,可能更多的會涉及到一些復雜查詢,以及分頁查詢等,這種查詢本身對資料庫性能影響較大,很可能影響到用戶端對于用戶表的操作,所以一般主流的解決方案就是把兩個庫進行分離,
由于運營端對于資料的一致性和可用性要求不是很高,也不需要實時訪問資料庫,所以我們可以把C端用戶表的資料同步到運營端的用戶表,而且用戶表可以不需要做分表操作,直接全量查表即可,
當然,如果運營端的操作性能實在是太慢了,我們還可以采用ElasticSearch搜索引擎來滿足后臺復雜查詢的需求,
實際應用中會遇到的問題
在實際應用中,并不是一開始就會想到未來會對這個表做拆分,因此很多時候我們面臨的問題是在資料量已經達到一定瓶頸的時候,才開始去考慮這個問題,
所以分庫分表最大的難點不是在于拆分的方法論,而是在運行了很長時間的資料庫中,如何根據實際業務情況選擇合適的拆分方式,以及在拆分之前對于資料的遷移方案的思考,而且,在整個資料遷移和拆分程序中,系統仍然需要保持可用,
對于運行中的表的分表,一般會分為三個階段,
階段一,新老庫雙寫
由于老的資料表肯定沒有考慮到未來分表的設計,同時隨著業務的迭代,可能有些模型也需要優化,因此會設計一個新的表來承載老的資料,而這個程序中,需要做幾件事情
- 資料庫表的雙寫,老的資料庫表和新的資料庫表同步寫入資料,事務的成功以老的模型為準,查詢也走老的模型
- 通過定時任務對資料進行核對,補平差異
- 通過定時任務把歷史資料遷移到新的模型中
階段二,以新的模型為準
到了第二個階段,歷史資料已經導完了,并且校驗資料沒有問題,
- 仍然保持資料雙寫,但是事務的成功和查詢都以新模型為準,
- 定時任務進行資料核對,補平資料差異
階段三,結束雙寫
到了第三個階段,說明資料已經完全遷移好了,因此,
- 取消雙寫,所有資料只需要保存到新的模型中,老模型不需要再寫入新的資料,
- 如果仍然有部分老的業務依賴老的模型,所以等到所有業務都改造完成后, 再廢除老的模型,
分庫分表后帶來的問題
分庫分表帶來性能提升的好處的同時,也帶來了很多的麻煩,
分布式事務問題
分庫分表之后,原本在一個庫中的事務,變成了跨越多個庫,如何保證跨庫資料的一致性問題,也是一個常見的難題,如圖6-13所示,用戶創建訂單時,需要在訂單庫中保存一條訂單記錄,并且修改庫存庫中的商品庫存,這里就涉及到跨庫事務的一致性問題,也就是說我怎么保證當前兩個事務操作要么同時成功,要么同時失敗,

跨庫查詢
比如查詢在合同資訊的時候要關聯客戶資料,由于是合同資料和客戶資料是在不同的資料庫,那么我們肯定不能直接使用join的這種方式去做關聯查詢,
我們有幾種主要的解決方案:
- 欄位冗余,比如我們查詢合同庫的合同表的時候需要關聯客戶庫的客戶表,我們可以直接把一些經常關聯查詢的客戶欄位放到合同表,通過這種方式避免跨庫關聯查詢的問題,
- 資料同步:比如商戶系統要查詢產品系統的產品表,我們干脆在商戶系統創建一張產品表,通過ETL或者其他方式定時同步產品資料,
- 全域表(廣播表) 比如基礎資料被很多業務系統用到,如果我們放在核心系統,每個系統都要去關聯查詢,這個時候我們可以在所有的資料庫都存盤相同的基礎資料,
- ER表(系結表),我們有些表的資料是存在邏輯的主外鍵關系的,比如訂單表order_info,存的是匯總的商品數,商品金額;訂單明細表order_detail,是每個商品的價格,個數等等,或者叫做從屬關系,父表和子表的關系,他們之間會經常有關聯查詢的操作,如果父表的資料和子表的資料分別存盤在不同的資料庫,跨庫關聯查詢也比較麻煩,所以我們能不能把父表和資料和從屬于父表的資料落到一個節點上呢?比如order_id=1001的資料在node1,它所有的明細資料也放到node1;order_id=1002的資料在node2,它所有的明細資料都放到node2,這樣在關聯查詢的時候依然是在一個資料庫,
上面的思路都是通過合理的資料分布避免跨庫關聯查詢,實際上在我們的業務中,也是盡量不要用跨庫關聯查詢,如果出現了這種情況,就要分析一下業務或者資料拆分是不是合理,如果還是出現了需要跨庫關聯的情況,那我們就只能用最后一種辦法,
- 系統層組裝
在不同的資料庫節點把符合條件資料的資料查詢出來,然后重新組裝,回傳給客戶端,
排序、翻頁、函式計算等問題
跨節點多庫進行查詢時,會出現limit分頁,order by排序的問題,比如有兩個節點,節點1存的是奇數id=1,3,5,7,9……;節點2存的是偶數id=2,4,6,8,10……
執行select * from user_info order by id limit 0,10
需要在兩個節點上各取出10條,然后合并資料,重新排序,
max、min、sum、count之類的函式在進行計算的時候,也需要先在每個分片上執行相應的函式,然后將各個分片的結果集進行匯總和再次計算,最終將結果回傳,
全域唯一ID
全域唯一id的問題,前面已經說了,水平分表之后,需要考慮全域唯一id設計問題,
多資料源的問題
分庫分表之后,難免會存在一個應用配置多個資料源,
另外,資料庫層面有可能會設計讀寫分離的方案,也使得一個應用會訪問多個資料源,并且還需要實作讀寫分離的動態路由,
而這些問題在每個應用系統中都會存在并且需要解決,所以為了提供統一的分庫分表相關問題的解決方案,引入了很多的開源技術,
分庫分表解決方案
目前市面上分庫分表的中間件相對來說說比較多,比如
- Cobar,淘寶開源的分庫分表組件,目前基本上沒有維護了,
- Sharding-Sphere,當當開源的一個分庫分表組件,已經捐獻給了Apache基金會
- Atlas, 奇虎360開源的分庫分表組件,也是沒怎么維護了
- Mycat,從阿里cobar升級而來,由開源組織維護,
- Vitess,谷歌開源的分庫分表組件
目前很多公司選擇較多的是Mycat或者Sharding-Sphere,所以我重點介紹Sharding-Sphere的使用和原理,
對于同類技術的選擇,無非就是看社區活躍度、技術的成熟度、以及功能和當前需求是否匹配,
關注[跟著Mic學架構]公眾號,獲取更多精品原創

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/333227.html
標籤:Java
上一篇:mac無坑安裝nginx
