面試官問單表資料量大一定要分庫分表嗎?我們用六個字和十張圖回答

語言: CN / TW / HK

歡迎大家關注公眾號「JAVA前線」檢視更多精彩分享文章,主要包括原始碼分析、實際應用、架構思維、職場分享、產品思考等等,同時歡迎大家加我個人微信「java_front」一起交流學習

1 文章概述

在業務發展初期單表完全可以滿足業務需求,在阿里巴巴開發手冊也建議:單錶行數超過500萬行或者單表容量超過2GB才推薦進行分庫分表,如果預計三年後資料量根本達不到這個級別,請不要在建立表時就分庫分表。

但是隨著業務的發展和深入,單表資料量不斷增加,逐漸成為業務系統的瓶頸。這是為什麼呢?

從巨集觀層面分析任何物體都必然有其物理極限。1965年英特爾創始人摩爾預測:積體電路上可容納的元器件的數目,約每隔24個月增加一倍,效能提升一倍,即計算機效能每兩年翻一番。

但是摩爾定律會有終點嗎?有些科學家認為摩爾定律是有終點的:半導體晶片單位面積可整合的元件數量是有極限的,因為半導體晶片製程工藝的物理極限為2到3奈米。當然也有科學家不支援這種說法,但是我們可以從中看出物理極限是很難突破的,當單表資料量達到一定規模時必然也達到極限。

從細節層面分析我們將資料儲存在資料庫,實際上是儲存在磁碟中,一次磁碟IO操作需要經歷尋道、旋轉延時、資料傳輸三個步驟,那麼一次磁碟IO耗時公式如下:

單次IO時間 = 尋道時間 + 旋轉延遲 + 傳送時間

總體來說上述操作都較為耗時,速度和記憶體相比有著數量級的差距,當資料量過大磁碟這一瓶頸更加明顯。那麼應該怎麼辦?處理單表資料量過大有以下六字口訣:刪、換、分、拆、異、熱。

長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字

 

刪是指刪除歷史資料並進行歸檔。換是指不要只使用資料庫資源,有些資料可以儲存至其它替代資源。分是指讀寫分離,增加多個讀例項應對讀多寫少的網際網路場景。拆是指分庫分表,將資料分散至不同的庫表中減輕壓力。異指資料異構,將一份資料根據不同業務需求儲存多份。熱是指熱點資料,這是一個非常值得注意的問題。

2 刪

我們分析這樣一個場景:消費者會經常查詢一年之前的訂單記錄嗎?答案是一般不會,或者說這種查詢需求量很小。根據上述分析那麼一年前的資料我們就沒有必要放在單表這張業務主表,可以將一年前的資料遷移到歷史歸檔表。

長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字

 

在查詢歷史資料表時,可以限制查詢條件如必須選擇日期範圍,日期範圍不能超過X個月等等從而減輕查詢壓力。

處理歷史存量資料比較簡單,因為存量資料一般是靜態的,此時狀態已經不再改變了。資料處理一般分為以下兩個步驟:

(1) 遷移一年前資料至歷史歸檔表
(2) 根據主鍵分批刪除主表資料

不能一次性刪除所有資料,因為資料量太大可能會引發超時,而是應該根據ID分批刪除,例如每次刪除500條資料。

第一步查詢一年前主鍵最大值和最小值,這是我們需要刪除的資料範圍:

SELECT
MIN(id) AS minId, 
MAX(id) AS maxId 
FROM biz_table 
WHERE create_time < DATE_SUB(now(),INTERVAL 1 YEAR)

第二步刪除資料時不能一次性全部刪掉,因為很可能會超時,我們可以通過程式碼動態更新endId進行批量刪除:

DELETE FROM biz_table 
WHERE id >= #{minId}
AND id <= #{maxId}
AND id <= #{endId}
LIMIT 500

3 換

換是指換一個儲存介質,當然並不是說完全替換,而是用其它儲存介質對資料庫做一個補充。例如海量流水記錄,這類資料量級是巨量的,根本不適合儲存在MySQL資料庫中,那麼這些資料可以存在哪裡呢?

現在網際網路公司一般都具備與之規模相對應的大資料服務或者平臺,那麼作為業務開發者要善於應用公司大資料能力,減輕業務資料庫壓力。

3.1 訊息佇列

這些海量資料可以儲存至Kafka,因為其本質上就是分散式的流資料儲存系統。使用Kafka有如下優點:

第一個優點是Kafka社群活躍功能強大,已經成為了一種事實上的工業標準。大資料很多元件都提供了Kafka接入元件,經過生產驗證並且對接成本較小,可以為下游業務提供更多選擇。

第二個優點是Kafka具有訊息佇列本身的優點例如解耦、非同步和削峰。

長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字

 

假設這些海量資料都已經儲存在Kafka,現在我們希望這些資料可以產生業務價值,這涉及到兩種資料分析任務:離線任務和實時任務。

離線任務對實時性要求不高,例如每天、每週、每月的資料報表統計分析,我們可以使用基於MapReduce資料倉庫工具Hive進行報表統計。

實時任務對實時性要求高,例如根據使用者相關行為推薦使用者感興趣的商品,提高使用者購買體驗和效率,可以使用Flink進行流處理分析。例如運營後臺查詢分析,可以將資料同步至ES進行檢索。

還有一種分類方式是將任務分為批處理任務和流處理任務,我們可以這麼理解:離線任務一般使用批處理技術,實時任務一般使用流處理技術。

3.2 API

上一個章節我們使用了Kafka進行海量資料儲存,由於其強大相容性和整合度,可以作為資料中介將資料進行中轉和解耦。

當然我們並不是必須使用Kafka進行中轉,例如我們直接可以使用相關Java API將資料存入Hive、ES、HBASE等。

但是我並不推薦這種做法,因為將儲存流水這樣操作耦合進業務程式碼並不合適,違反了高內聚低耦合的原則,儘量不要使用。

3.3 快取

從廣義上理解換這個字,我們還可以引入Redis遠端快取,把Redis放在MySQL前面,攔下一些高頻讀請求,但是要注意快取穿透和擊穿問題。

快取穿透和擊穿從最終結果上來說都是流量繞過快取打到了資料庫,可能會導致資料庫掛掉或者系統雪崩,但是仔細區分還是有一些不同,我們分析一張業務讀取快取一般流程圖。

長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字

 

我們用文字簡要描述這張圖:

(1) 業務查詢資料時首先查詢快取,如果快取存在資料則返回,流程結束
(2) 如果快取不存在資料則查詢資料庫,如果資料庫不存在資料則返回空資料,流程結束
(3) 如果資料庫存在資料則將資料寫入快取並返回資料給業務,流程結束

假設業務方要查詢A資料,快取穿透是指資料庫根本不存在A資料,所以根本沒有資料可以寫入快取,導致快取層失去意義,大量請求會頻繁訪問資料庫。

快取擊穿是指請求在查詢資料庫前,首先查快取看看是否存在,這是沒有問題的。但是併發量太大,導致第一個請求還沒有來得及將資料寫入快取,後續大量請求已經開始訪問快取,這是資料在快取中還是不存在的,所以瞬時大量請求會打到資料庫。

我們可以使用分散式鎖加上自旋解決這個問題,本文給出一段示例程式碼,具體原理和程式碼實現請參看我之前的文章:流程圖+原始碼深入分析:快取穿透和擊穿問題原理以及解決方案

/**
 * 業務回撥
 *
 * @author 今日頭條號「JAVA前線」
 *
 */
public interface RedisBizCall {
    /**
     * 業務回撥方法
     *
     * @return 序列化後資料值
     */
    String call();
}
/**
 * 安全快取管理器
 *
 * @author 今天頭條號「JAVA前線」
 *
 */
@Service
public class SafeRedisManager {
    @Resource
    private RedisClient RedisClient;
    @Resource
    private RedisLockManager redisLockManager;
    public String getDataSafe(String key, int lockExpireSeconds, int dataExpireSeconds, RedisBizCall bizCall, boolean alwaysRetry) {
        boolean getLockSuccess = false;
        try {
            while(true) {
                String value = redisClient.get(key);
                if (StringUtils.isNotEmpty(value)) {
                    return value;
                }
                /** 競爭分散式鎖 **/
                if (getLockSuccess = redisLockManager.tryLock(key, lockExpireSeconds)) {
                    value = redisClient.get(key);
                    if (StringUtils.isNotEmpty(value)) {
                        return value;
                    }
                    /** 查詢資料庫 **/
                    value = bizCall.call();
                    /** 資料庫無資料則返回**/
                    if (StringUtils.isEmpty(value)) {
                        return null;
                    }
                    /** 資料存入快取 **/
                    redisClient.setex(key, dataExpireSeconds, value);
                    return value;
                } else {
                    if (!alwaysRetry) {
                        logger.warn("競爭分散式鎖失敗,key={}", key);
                        return null;
                    }
                    Thread.sleep(100L);
                    logger.warn("嘗試重新獲取資料,key={}", key);
                }
            }
        } catch (Exception ex) {
            logger.error("getDistributeSafeError", ex);
            return null;
        } finally {
            if (getLockSuccess) {
                redisLockManager.unLock(key);
            }
        }
    }
}

4 分

我們首先看一個概念:讀寫比。網際網路場景中一般是讀多寫少,例如瀏覽20次訂單列表資訊才會進行1次確認收貨,此時讀寫比例就是20:1。面對讀多寫少這種情況我們可以做什麼呢?

我們可以部署多臺MySQL讀庫專門用來接收讀請求,主庫接收寫請求並通過binlog實時同步的方式將資料同步至讀庫。MySQL官方即提供這種能力,進行簡單配置即可。

長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字

 

那麼客戶端怎麼知道訪問讀庫還是寫庫呢?推薦使用ShardingSphere元件,通過配置將讀寫請求分別路由至讀庫或者寫庫。

5 拆

如果刪除了歷史資料並採用了其它儲存介質,也用了讀寫分離,但是單表壓力還是太大怎麼辦?這時我們只能拆分資料表,即把單庫單表資料遷移到多庫多張表中。

假設有一個電商資料庫存放訂單、商品、支付三張業務表。隨著業務量越來越大,這三張業務資料表也越來越大,我們就以這個例子進行分析。

5.1 垂直拆分

垂直拆分就是按照業務拆分,我們將電商資料庫拆分成三個庫,訂單庫、商品庫。支付庫,訂單表在訂單庫,商品表在商品庫,支付表在支付庫。這樣每個庫只需要儲存本業務資料,物理隔離不會互相影響。

長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字

 

5.2 水平拆分

按照垂直拆分方案,現在我們已經有三個庫了,平穩運行了一段時間。但是隨著業務增長,每個單庫單表的資料量也越來越大,逐漸到達瓶頸。

這時我們就要對資料表進行水平拆分,所謂水平拆分就是根據某種規則將單庫單表資料分散到多庫多表,從而減小單庫單表的壓力。

水平拆分策略有很多方案,最重要的一點是選好ShardingKey,也就是按照哪一列進行拆分,怎麼分取決於我們訪問資料的方式。

5.2.1 範圍分片

現在我們要對訂單庫進行水平拆分,我們選擇的ShardingKey是訂單建立時間,拆分策略如下:

(1) 拆分為四個資料庫,分別儲存每個季度的資料
(2) 每個庫三張表,分別儲存每個月的資料

上述方法優點是對範圍查詢比較友好,例如我們需要統計第一季度的相關資料,查詢條件直接輸入時間範圍即可。

長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字

 

但是這個方案問題是容易產生熱點資料。例如雙11當天下單量特別大,就會導致11月這張表資料量特別大從而造成訪問壓力。

5.2.2 查表分片

查表法是根據一張路由表決定ShardingKey路由到哪一張表,每次路由時首先到路由表裡查到分片資訊,再到這個分片去取資料。

我們分析一個查表法實際案例。Redis官方在3.0版本之後提供了叢集方案Redis Cluster,其中引入了雜湊槽(slot)這個概念。

一個叢集固定有16384個槽,在叢集初始化時這些槽會平均分配到Redis叢集節點上。每個key請求最終落到哪個槽計算公式是固定的:

SLOT = CRC16(key) mod 16384

那麼問題來了:一個key請求過來怎麼知道去哪臺Redis節點獲取資料?這就要用到查表法思想。

(1) 客戶端連線任意一臺Redis節點,假設隨機訪問到為節點A
(2) 節點A根據key計算出slot值
(3) 每個節點都維護著slot和節點對映關係表
(4) 如果節點A查表發現該slot在本節點則直接返回資料給客戶端
(5) 如果節點A查表發現該slot不在本節點則返回給客戶端一個重定向命令,告訴客戶端應該去哪個節點上請求這個key的資料
(6) 客戶端再向正確節點發起連線請求

查表法優點是可以靈活制定路由策略,如果我們發現有的分片已經成為熱點則修改路由策略。缺點是多一次查詢路由表操作增加耗時,而且路由表如果是單點也可能會有單點問題。

5.2.3 雜湊分片

現在比較流行的分片方法是雜湊分片,相較於範圍分片,雜湊分片可以較為均勻將資料分散在資料庫中。

我們現在將訂單庫拆分為4個庫編號為[0,3],每個庫4張表編號為[0,3],如下圖如所示:

長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字

 

現在使用orderId作為ShardingKey,那麼orderId=100的訂單會儲存在哪張表?我們來計算一下:由於是分庫分表,首先確定路由到哪一個庫,取模計算得到序號為0表示路由到db[0]

db_index = 100 % 4 = 0

庫確定了接著在db[0]進行取模表路由

table_index = 100 % 4 = 0

最終這條資料應該路由至下表

db[0]_table[0]

最終計算結果如下圖所示:

長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字

 

在實際開發中最終路由到哪張表,並不需要我們自己算,因為有許多開源框架就可以完成路由功能,例如ShardingSphere、TDDL等等。

6 異

現在資料已經使用雜湊分片方法完成了水平拆分,我們選擇的ShardingKey是orderId。這時客戶端需要查詢orderId=111的資料,查詢語句很簡單如下:

SELECT * FROM order WHERE orderId = 111

這個語句沒有問題,因為查詢條件包含orderId,可以路由到具體的資料表。

現在如果業務想要查詢使用者維度的資料,希望查詢userId=222的資料,現在問題來了:以下這個語句可以查出資料嗎?

SELECT * FROM order WHERE userId = 222

答案是可以,但是需要掃描所有庫的所有表,因為無法根據userId路由到具體某一張表,這樣時間成本會非常高,這種場景怎麼辦呢?

這就要用到資料異構的思想。資料異構核心是用空間換時間,簡單一句話就是一份資料按照不同業務需求儲存多份,這樣做是因為儲存硬體成本不是很高,而網際網路場景對響應速度要求很高。

對於上述需要使用userId進行查詢的場景,我們完全可以新建庫和表,數量和結構與訂單庫表完全一致,唯一不同點是ShardingKey改用userId,這樣就可以使用userId查詢了。

現在又引出一個新問題,業務不可能每次都將資料寫入多個數據源,這樣會帶來效能問題和資料一致行為。怎麼解決老庫和新庫資料同步問題?我們可以使用阿里開源的canal元件解決這個問題,看一張官網介紹canal架構圖:

長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字

 

canal元件的主要用途是基於MySQL資料庫增量日誌解析,提供增量資料訂閱和消費服務,工作原理如下:

(1) canal偽裝成為MySQL slave模擬互動協議向master傳送dump協議
(2) master收到canal傳送的dump請求,開始推送binlog給canal
(3) canal解析binlog併發送到儲存目的地,例如MySQL、Kafka、Elasticsearch

canal元件下游可以對接很多其它資料來源,這樣給業務提供了更多選擇。我們可以像上述例項中新建使用者維度訂單表,也可以將資料存在ES中提供運營檢索能力等等。

7 熱

我們來分析這樣一個場景:社交業務有一張使用者關係表,主要記錄誰關注了誰。其中有一個明星粉絲特別多,如果以userId作為分片,那麼其所在分片資料量就會特別大。

不僅分片資料量特別大,而且可以預見這個分片訪問頻率也會非常高。此時資料量大並且訪問頻繁,很有可能造成系統壓力。

7.1 熱點概念

我們將訪問行為稱為熱點行為,將訪問對應的資料稱為熱點資料。我們通過例項來分析。

在電商雙11活動中百分之八十的訪問量會集中在百分之二十的商品上。使用者重新整理、新增購物車、下單被稱為熱點行為,相應商品資料就被稱為熱點資料。

在微博場景中大V釋出一條訊息會獲得大量訪問。使用者對這條訊息的瀏覽、點贊、轉發、評論被稱為熱點行為,這條訊息資料被稱為熱點資料。

在秒殺場景中參與秒殺的商品會獲得極大的瞬時訪問量。使用者對這個商品的頻繁重新整理、點選、下單被稱為熱點行為,參與秒殺的商品資料被稱為熱點資料。

我們必須將熱點資料進行一些處理,使得熱點訪問更加流暢,更是為了保護系統免於崩潰。我們從發現熱點資料、處理熱點資料來展開分析。

7.2 發現熱點資料

我們把發現熱點資料分為兩種方式:靜態發現和動態發現。

靜態發現:在開始秒殺活動之前,參與商家一定知道哪些商品參與秒殺,那麼他們可以提前將這些商品報備告知平臺。

在微博場景中,具有影響力的大V一般都很知名,網站運營同學可以提前知道。技術同學還可以通過分析歷史資料找出TOP N資料。對於這些可以提前預判的資料,完全可以通過後臺系統上報,這樣系統可以提前做出預處理。

動態發現:有些商品可能並沒有上報為熱點商品,但是在實際銷售中卻非常搶手。在微博場景中,有些話題熱度突然升溫。這些資料成為事實上的熱點資料。對於這些無法提前預判的資料,需要動態進行判斷。

我們需要一個熱點發現系統去主動發現熱點資料。大體思路是首先非同步收集訪問日誌,再統計單位時間內訪問頻次,當超過一定閾值時可以判斷為熱點資料。

7.3 處理熱點問題

(1) 熱點行為

熱點行為可以採取高頻檢測方式,如果發現頻率過高則進行限制。或者採用記憶體佇列實現的生產者與消費者這種非同步化方式,消費者根據能力處理請求。

(2) 熱點資料

處理熱點資料核心主要是根據業務形態來進行處理,我一般採用以下方案配合執行:

(1) 選擇合適ShardingKey進行分庫分表
(2) 異構資料至其它適合檢索的資料來源例如ES
(3) 在MySQL之前設定快取層
(4) 儘量不在MySQL進行耗時操作(例如聚合)

8 文章總結

本文我們詳細介紹處理單表資料量過大的六字口訣:刪、換、分、拆、異、熱。這並不是意味這每次遇到單表資料量過大情況六種方案全部都要使用,例如拆分資料表成本確實比較高,會帶來分散式事務、資料難以聚合等問題,如果不分表可以解決那麼就不要分表,核心還是根據自身業務情況選擇合適的方案。

歡迎大家關注公眾號「JAVA前線」檢視更多精彩分享文章,主要包括原始碼分析、實際應用、架構思維、職場分享、產品思考等等,同時歡迎大家加我個人微信「java_front」一起交流學習

分享到: