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

歡迎你們關注公衆號「JAVA前線」查看更多精彩分享文章,主要包括源碼分析、實際應用、架構思惟、職場分享、產品思考等等,同時歡迎你們加我微信「java_front」一塊兒交流學習java


1 文章概述

在業務發展初期單表徹底能夠知足業務需求,阿里巴巴開發手冊也建議:單錶行數超過500萬行或者單表容量超過2GB才推薦進行分庫分表,若是預計三年後數據量根本達不到這個級別請不要在建立表時就分庫分表。redis

可是隨着業務的發展和深刻,單表數據量不斷增長,逐漸成爲業務系統的瓶頸。這是爲何呢?sql

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

可是摩爾定律會有終點嗎?有些科學家認爲摩爾定律是有終點的:半導體芯片單位面積可集成的元件數量是有極限的,由於半導體芯片製程工藝的物理極限爲2到3納米。固然也有科學家不支持這種說法,可是咱們能夠從中看出物理極限是很難突破的,當單表數據量達到必定規模時必然也達到極限。緩存

從細節層面分析數據保存在數據庫,其實是保存在磁盤中,一次磁盤IO操做須要經歷尋道、旋轉延時、數據傳輸三個步驟,那麼一次磁盤IO耗時公式以下:安全

單次IO時間 = 尋道時間 + 旋轉延遲 + 傳送時間
複製代碼

整體來講上述操做都較爲耗時,速度和內存相比有着數量級的差距,當數據量一大磁盤這一瓶頸將更加明顯。那麼應該怎麼辦呢?處理單表數據量過大有如下六字口訣:刪、換、分、拆、異、熱。微信

刪是指刪除歷史數據並進行歸檔。換是指不要只使用數據庫資源,有些數據能夠存儲至其它替代資源。分是指讀寫分離,增長多個讀實例應對讀多寫少的互聯網場景。異指數據異構,將一份數據根據不一樣業務需求保存多份。熱是指熱點數據是一個很是值得注意的問題。markdown


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 水平拆分

按照垂直拆分方案,如今咱們已經有三個庫了,平穩運行了一段時間。可是隨着業務增加,每一個單庫單表的數據量也愈來愈大,逐漸到達瓶頸。

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

水平拆分策略有不少,核心是選中Sharding Key,也就是按照哪一列進行拆分,怎麼分取決於咱們訪問數據的方式。


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」一塊兒交流學習

相關文章
相關標籤/搜索