需求緣起(用一個公司的發展作為背景) 1.還是個小公司的時候,註冊用戶就20w,每天活躍用戶1w,每天最大單表數據量就1000,然後高峰期每秒併發請求最多就10,此時一個16核32G的伺服器,每秒請求支撐在2000左右,負載合理,沒有太大壓力,基本沒有宕機風險。 2.當註冊用戶達到2000W,每天活 ...
需求緣起(用一個公司的發展作為背景)
1.還是個小公司的時候,註冊用戶就20w,每天活躍用戶1w,每天最大單表數據量就1000,然後高峰期每秒併發請求最多就10,此時一個16核32G的伺服器,每秒請求支撐在2000左右,負載合理,沒有太大壓力,基本沒有宕機風險。
2.當註冊用戶達到2000W,每天活躍用戶數100W,每天單表新增數據量達到50W條,高峰期請求量達到1W。經過一段時間的運行,單標數據量會越來越多,帶來的問題
2.1 資料庫伺服器的IO,網路寬頻,CPU負載,記憶體消耗都會達到非常高,伺服器已經不堪重負
2.2 高峰時期,單表數據量本來就很大,加上資料庫伺服器負載太高,導致性能下降,此時SQL的性能就更差了,用戶體驗賊差, 點一個按鈕要很久才有響應,如果伺服器的配置再低一點的話,資料庫可能直接宕機
3. 實現一個基本的分庫分表的思路,將一臺資料庫伺服器變成5台資料庫,就能有5個庫,5個表,這樣可以將表中的數據按照ID分別通過同一個映射方法,分佈到這5個庫中。此時寫入數據的時候,需要藉助資料庫中間件,比如shardng-jdbc或者Mycat。查詢的時候先通過一步映射到具體的資料庫,再進行查詢。
4. 當用戶量再次增長時,只能繼續分表,比如將一張表拆分成1024張表,這樣在操作數據的時候,需要兩次路由,一次找到在哪個資料庫,一次找到在哪張表。
5. 除了分表,資料庫還可以做主從架構,主伺服器用以寫入,從伺服器用以查詢,根據業務需求具體實現即可。
分庫分錶帶來的問題
1. 分庫分表之後一個必然的問題,如何獲取一個全局為一個ID?因為表中的數據是通過ID路由映射的,ID不能重覆。
2. 就算有了全局唯一的ID,那面對分頁查詢的需求,應該怎麼處理呢?
唯一ID的生成
下麵列舉幾種常見的唯一ID生成方案,需要滿足兩大核心需求:1.全局唯一 2趨勢有序
1. 用資料庫的auto_increment(自增ID)來生成,每次通過寫入資料庫一條記錄,利用資料庫ID自增的特性獲取唯一,有序的ID。
優點:使用資料庫原有的功能,相對簡單;能夠保證唯一;能夠保證遞增性;ID之間的步長是固定且可以自定義的
缺點:可用性難以保證,當生成ID的那台伺服器宕機,系統就玩不轉了;由於寫入是單點的,所以擴展性差,性能上限取決於資料庫的寫性能。
2. 用UUID
優點:簡單方便;全球唯一,在遇見數據遷移、合併或者變更時可以從容應對;
缺點:沒有遞增性;UUID是很長的字元串,作為主鍵對存儲空間有一定要求,查詢效率也較低。
3. 使用Redis生成ID,主要利用Redis是單線程的,所以也可以用來生成唯一ID。當使用的是Redis集群的時候,比如集群中有5台Redis,初始化每台Redis的值為1,2,3,4,5,設置步長為5,並且確定一個不隨機的負載均衡策略,能夠保證有序,唯一。
優點:不依賴資料庫,靈活,且性能相對於資料庫有一定提高;使用Redis集群策略還能排除單點故障問題;ID天然有序
缺點:如果系統中沒有Redis,還需要引入新的組件;編碼和配置工作量大
4. 使用Twitter的snowflake演算法;其核心思想是一個64位long型ID,使用41bit作為毫秒數,10bit作為機器的ID(5個bit是數據中心,5個bit的機器ID),12bit作為毫秒內的流水號(意味著每個節點在每毫秒可以產生 4096 個 ID),最後還有一個符號位,永遠是0。具體實現的代碼可以參看https://github.com/twitter/snowflake。可以根據自身需求進行一定的修改。
優點:不依賴資料庫,靈活方便,性能優於資料庫;ID按照時間在單機上是遞增的
缺點:單機上遞增,但是當分散式環境下每台機器的時鐘不可能完全同步,有時並不能做做全局遞增。
5. 使用zookeeper生成唯一ID,主要通過znode數據版本來生成序列號,可以生成32為和64為的數據版本號。很少使用,因為是多步調用API,併發情況下還需要考慮分散式鎖,不是很理想。
6. MongoDB的ObjectID,和snowflake演算法類似。4位元組Unix時間戳,3位元組機器編碼,2位元組進程編碼,3位元組隨機數
分庫分表下的分頁查詢
假設有一張用戶表,經過分庫分表之後,現在均勻分佈在2台伺服器實例上。業務需要查詢“最近註冊的第3頁用戶”,雖然資料庫有分庫用的全局的ID,但是沒有排序條件time的全局視野,此時應該怎麼做呢?
1. 全局視野法:因為不清楚按照時間排序之後的第三頁數據到底是如何分佈在資料庫上的,所以必須每個庫都返回3頁數據,所得到的6頁數據在服務層進行記憶體排序,得到全局視野,再取第3頁數據。
優點:通過服務層修改,擴大數據查詢量,得到全局視野,業務無損,精確
缺點(顯而易見):每個分庫都需要返回更多的數據,增大網路傳輸量;除了資料庫要按照time排序,服務層也需要二次排序,損耗性能;隨著頁碼的增大,性能極具下降,數據量和排序量都將大增,性能平方級下降。
2. 業務折中
2.1 禁止跳頁查詢,不提供“直接跳到指定頁面”的功能,只提供下一頁的功能。極大的降低技術方案的複雜度。第一頁的選取方法和全局視野法一樣,但是點擊下一頁時:
2.1.1先找到上一頁的time的最大值,作為第二頁數據拉去的查詢條件,只取每頁的記錄數,
2.2.2這樣服務層還是獲得兩頁數據,再做一次排序,獲取一頁數據。
2.2.3改進了不會因為頁碼增大而導致數據的傳輸量和排序量增大
3. 允許數據精度丟失:需要考慮業務員上是否接受在頁碼較大是返回的數據不是精準的數據。
3.1在數據量較大,且ID映射分佈足夠隨機的話,應該是滿足等概率分佈的情況的,所以取一頁數據,我們在每個資料庫中取前半頁。
3.2當然這樣的到的結果並不是精準的,但是當實際業務可以接受的話, 此時的技術方案的複雜度變大大降低。也不需要服務層記憶體排序了。
4. 二次查詢法:既滿足業務的精確需求,也無需業務折中。現在假設每頁顯示10條數據,要查第三頁,數據分了兩個庫。 正常的語句是 select * from table order by time offset 20 limit 10,取偏移20個之後的10個
4.1首次查詢查詢每個庫的select * from table order by time offset 10 limit 10;得到10條數據。這裡的offset是總offset/分庫數
4.2 服務層得到來自兩個分庫的結果集,得到最小的time,也就是最頂層的time,這個time滿足最少有10條記錄在它前面,然後分別記錄每個庫的最大time
4.3 分別再次查詢最小time->每個庫上一次的最大time的數據,得到每個庫的查詢結果
4.4 在每個集合的最小time都是相同的,所以可以得到該最小time在整個資料庫中的offset,加起來就是這個最小time在全局庫的offset位置。
4.5 再將第二次查詢的結果集拼起來和得到的最小time的offset,推導出 offset 20 limit 10的一頁記錄。
優點:可以精確得到業務數據,且每次返回的數據量都非常小,不會隨著頁碼增加而數據量增大。
缺點:需要進行兩次資料庫查詢