MySQL的分區/分庫/分表總結

前言

本文一切基於MySql InnoDB

概念

  1. 分區:把一張表的數據分成多個區塊,在邏輯上看最終只是一張表,但底層是由多個物理區塊組成的

  2. 分表:把一張表按一定的規則分解成多個具有獨立存儲空間的實體表。系統讀寫時需要根據定義好的規則得到對應的字表明,然後操作它。

  3. 分庫:把一個庫拆成多個庫,突破庫級別的數據庫操作I/O瓶頸。

在mysql中,schema和庫(database)是一個概念

數據庫架構演變

一開始,我們只用單機數據庫就足夠滿足業務需要了,但隨着業務的拓展,帶來越來越多的請求,我們將數據庫的寫操作和讀操作進行分離,使用多個從庫副本(Slaver Replication)負責讀,使用主庫(Master)負責寫。從庫從主庫同步更新數據,保持數據一致。架構上的數據庫主從同步,使得從庫可以水平擴展,所以更多的讀請求不成問題。

但是當用戶量級上來後,寫請求越來越多,該怎麼辦?加一個Master是不能解決問題的, 因爲數據要保存一致性,寫操作需要2個master之間同步,相當於是重複了,而且更加複雜。

這時就需要用到分庫分表,對寫操作進行切分。

什麼情況下要分庫分表

任何問題都是太大或者太小的問題,我們這主要面對的是數據量太大的問題。

  1. 用戶請求量太大

    • 瓶頸:單服務器的TPS,內存,IO都是有限的。
    • 解決方法:分散請求到多個服務器上; 其實用戶請求和執行一個sql查詢是本質是一樣的,都是請求一個資源,只是用戶請求還會經過網關,路由,http服務器等。
  2. 單表數據量太大

    • 瓶頸:索引膨脹,查詢耗時長,影響正常CRUD。
    • 解決方法:切分成多個數據集更小的表。
  3. 單庫數據量太大

    • 瓶頸:單個數據庫處理能力有限,單庫所在服務器上磁盤空間不足,I/O有限;
    • 解決方法:切分成更多更小的庫

1 分區

首先,我們要明白分區的區是指什麼!

我們在《【InnoDB詳解二】MySQL文件系統和InnoDB存儲結構》一文中提到過,MySQL的物理數據,存儲在表空間文件(.ibdata1和.ibd)中,這裏講的分區的意思是指將同一表中不同行的記錄分配到不同的物理文件中,幾個分區就有幾個.idb文件

MySQL在5.1時添加了對水平分區的支持。分區是將一個表或索引分解成多個更小,更可管理的部分。每個區都是獨立的,可以獨立處理,也可以作爲一個更大對象的一部分進行處理。這個是MySQL支持的功能,業務代碼無需改動。

可以通過使用SHOW VARIABLES命令來確定MySQL是否支持分區,如:

mysql> SHOW VARIABLES LIKE '%partition%';
 
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| have_partition_engine | YES   |
+-----------------------+-------+
1 row in set (0.00 sec)

在如上列出的一個正確的SHOW VARIABLES 命令所產生的輸出中,如果沒有看到變量have_partition_engine的值爲YES,那麼MySQL的版本就不支持分區。

MySQL是面向OLTP的數據庫,對於分區的使用應該更加小心,如果不清楚如何使用分區可能會對性能產生負面的影響。

1.1 MySQL分區類型

目前MySQL支持一下幾種類型的分區:

  1. RANGE分區:基於一個給定區間邊界,得到若干個連續區間範圍,按照分區鍵的落點,把數據分配到不同的分區;
  2. LIST分區:類似RANGE分區,區別在於LIST分區是基於枚舉出的值列表分區,RANGE是基於給定連續區間範圍分區;
  3. HASH分區:基於用戶自定義的表達式的返回值,對其根據分區數來取模,從而進行記錄在分區間的分配的模式。這個用戶自定義的表達式,就是MySQL希望用戶填入的哈希函數。
  4. KEY分區:類似於按HASH分區,區別在於KEY分區只支持計算一列或多列,且使用MySQL 服務器提供的自身的哈希函數。

如果表存在主鍵或者唯一索引時,分區列必須是唯一索引的一個組成部分。

在實戰中,十有八九都是用RANGE分區。

1.1.1 RANGE分區

RANGE分區是實戰最常用的一種分區類型,行數據基於屬於一個給定的連續區間的列值被放入分區。

但是記住,當插入的數據不在一個分區中定義的值的時候,會拋異常。

RANGE分區主要用於日期列的分區,比如交易表啊,銷售表啊等。可以根據年月來存放數據。

如果你分區走的唯一索引中date類型的數據,那麼注意了,優化器只能對YEAR(),TO_DAYS(),TO_SECONDS(),UNIX_TIMESTAMP()這類函數進行優化選擇。

實戰中可以用int類型的字段來存時間戳做分區列,那麼只用存yyyyMM就好了,也不用關心函數了。

MySQL使用PARTITION命令來做分區,sql語句如下:

CREATE TABLE
    `Order` (
        `id`
        INT NOT NULL AUTO_INCREMENT,
        `partition_key`
        INT NOT NULL,
        `amt`
        DECIMAL(5) NULL) PARTITION BY RANGE(partition_key)
PARTITIONS 5(
    PARTITION part0 VALUES LESS THAN(201901),
    PARTITION part1 VALUES LESS THAN(201902),
    PARTITION part2 VALUES LESS THAN(201903),
    PARTITION part3 VALUES LESS THAN(201904),
    PARTITION part4 VALUES LESS THAN(201905),
    PARTITION part4 VALUES LESS THAN MAXVALUE;

RANGE分區通過使用PARTITION BY RANGE(expr)實現,其中“expr” 可以是某個列值,如id,no,partition_key等。或一個基於某個列值並返回一個整數值的表達式,如YEAR(date)。不過值得注意的是,expr的返回值,不可以爲NULL。

其中,MAXVALUE 表示最大的可能的整數值。如果沒有設置MAXVALUE這個分區,那麼此時如果insert一個partition_key大於201905的記錄,MySQL就會拋出異常,插入失敗。

VALUES LESS THAN的排列必須從小到大順序列出,這樣MySQL才能識別一個一個的區間段。

這時候我們先插入一些數據:

INSERT INTO `Order` (`id`, `partition_key`, `amt`) VALUES ('1', '201901', '1000');
INSERT INTO `Order` (`id`, `partition_key`, `amt`) VALUES ('2', '201902', '800');
INSERT INTO `Order` (`id`, `partition_key`, `amt`) VALUES ('3', '201903', '1200');

現在我們查詢一下,通過EXPLAIN PARTITION命令發現SQL優化器只需搜對應的區,不會搜索所有分區:

因爲partition_key是分區鍵。當然,我們也可以直接指定搜索哪個分區:

SELECT * FROM Order PARTITION (part0,part1) WHERE status amt > 1000

注意,如果sql語句不指定分區,則會走所有分區,性能反而會不升反降。所以分區表後,select語句必須走分區鍵。

涉及聚合函數SUM()、COUNT()的查詢時,如果不指定分區,那麼會在每個分區上並行處理。例如執行這條語句SELECT COUNT(1) FROM Order,則會在每個分區上都同時運行查詢;

一個例子不夠,我們再舉一個例子,來看看expr是個函數表達式的場景,假如現在有如下僱員表:

CREATE TABLE employees (
    id INT NOT NULL,
    fname VARCHAR(30),
    lname VARCHAR(30),
    hired DATE NOT NULL DEFAULT '1970-01-01',
    separated DATE NOT NULL DEFAULT '9999-12-31',
    job_code INT,
    store_id INT
)
PARTITION BY RANGE (YEAR(separated)) (
    PARTITION p0 VALUES LESS THAN (1991),
    PARTITION p1 VALUES LESS THAN (1996),
    PARTITION p2 VALUES LESS THAN (2001),
    PARTITION p3 VALUES LESS THAN MAXVALUE
);

在這個方案中,在1991年前離職的所有僱員的記錄保存在分區p0中,1991年到1995年期間離職的所有僱員的記錄保存在分區p1中, 1996年到2000年期間離職的所有僱員的記錄保存在分區p2中,2000年後離職的所有工人的信息保存在p3中。

當需要刪除“舊的”數據時,使用分區會有意想不到的效果。

假如我們想刪除所有在1991年前就已經離職的僱員的記錄,你只需簡單地使用ALTER TABLE employees DROP PARTITION p0;

對於有大量行的表,這比運行DELETE FROM employees WHERE YEAR(separated) <= 1990要有效得多。

1.1.2 LIST分區

MySQL中的LIST分區在很多方面類似於RANGE分區。和RANGE分區一樣,LIST分區的每個分區必須明確定義。它們的主要區別在於,LIST分區是基於枚舉出的值列表分區,RANGE是基於給定連續區間範圍分區;

LIST分區通過使用PARTITION BY LIST(expr)來實現,我們假定有20個音像店,分佈在4個有經銷權的地區,如下表所示:

地區 商店ID 號
北區 3, 5, 6, 9, 17
東區 1, 2, 10, 11, 19, 20
西區 4, 12, 13, 14, 18
中心區 7, 8, 15, 16

要按照屬於同一個地區商店的記錄保存在同一個分區的原則來分割表,可以使用下面的CREATE TABLE語句:

CREATE TABLE employees (
    id INT NOT NULL,
    fname VARCHAR(30),
    lname VARCHAR(30),
    hired DATE NOT NULL DEFAULT '1970-01-01',
    separated DATE NOT NULL DEFAULT '9999-12-31',
    job_code INT,
    store_id INT
)
PARTITION BY LIST(store_id)
    PARTITION pNorth VALUES IN (3,5,6,9,17),
    PARTITION pEast VALUES IN (1,2,10,11,19,20),
    PARTITION pWest VALUES IN (4,12,13,14,18),
    PARTITION pCentral VALUES IN (7,8,15,16)
);

可以看到,和RANGE分區相比,LIST分區的VALUES IN後面接的是枚舉值列表,不像RANGE是用VALUES LESS THAN來定義區間邊界。

如果試圖插入字段值(或分區表達式的返回值)不在分區值列表中的任何一行時,那麼“INSERT”查詢將失敗並報錯。例如,假定LIST分區的採用上面的方案,那麼下面的查詢將失敗:INSERT INTO employees VALUES (224, 'Linus', 'Torvalds', '2002-05-01', '2004-10-12', 42, 21);。因爲“store_id”字段值21不能在用於定義分區pNorth, pEast, pWest,或pCentral的值列表中找到。

要重點注意的是,LIST分區沒有類似如“VALUES LESS THAN MAXVALUE”這樣的包含其他值在內的定義。所以將要匹配的任何值都必須在值列表中能夠找到。

1.1.3 HASH分區

HASH分區主要用來確保數據在預先確定數目的分區中平均分佈。在RANGE和LIST分區中,我們必須明確指定一個給定的區間或列值集合,來指定哪些記錄進入哪些分區;而在HASH分區中,MySQL自動完成分配記錄到區間的工作,你所要做的只是確定一個用來做哈希的字段或者表達式,以及指定被分區的表將要被分割成的分區數量。

要使用HASH分區來分割一個表,要在CREATE TABLE 語句上添加一個PARTITION BY HASH (expr)子句,其中“expr”同樣可以是一個返回一個整數的表達式,或者僅僅是字段類型爲整型的某個字段。

此外,你很可能需要在後面再添加一個“PARTITIONS num”子句,其中num 是一個非負的整數,它表示表將要被分割成分區的數量。

例如,下面的語句創建了一個使用基於“store_id”列進行哈希處理的表,該表被分成了4個分區:

CREATE TABLE employees (
    id INT NOT NULL,
    fname VARCHAR(30),
    lname VARCHAR(30),
    hired DATE NOT NULL DEFAULT '1970-01-01',
    separated DATE NOT NULL DEFAULT '9999-12-31',
    job_code INT,
    store_id INT
)
PARTITION BY HASH(store_id)
PARTITIONS 4;

如果沒有包括一個PARTITIONS子句,那麼分區的數量將默認爲1。

注意,expr不應該設置的過於複雜,因爲每當插入或更新(或者可能刪除)一行時,這個表達式都要計算一次;這意味着非常複雜的表達式可能會引起性能問題,尤其是在執行同時影響大量行的運算(例如批量插入)的時候。

最有效率的哈希函數是隻對單個表列進行計算,並且它的結果值隨字段值進行一致地增大或減小,因爲這考慮了在分區範圍上的“修剪”。也就是說,表達式值和它所基於的列的值變化越接近,MySQL就可以越有效地使用該表達式來進行HASH分區。

當使用了“PARTITION BY HASH”時,MySQL將基於用戶提供的函數結果的模數來確定使用哪個編號的分區。換句話,對於一個表達式“expr”,將要保存記錄的分區編號爲N ,其中“N = MOD(expr, num)”。

1.1.4 KEY分區

按照KEY進行分區類似於按照HASH分區,除了HASH分區使用的用戶定義的表達式,而KEY分區的哈希函數是由MySQL 服務器提供。

MySQLCluster使用函數MD5()來實現KEY分區;對於使用其他存儲引擎的表,服務器使用其自己內部的 哈希函數,這些函數是基於與PASSWORD()一樣的運算法則。

“CREATE TABLE ... PARTITION BY KEY”的語法規則類似於創建一個通過HASH分區的表的規則。它們唯一的區別在於使用的關鍵字是KEY而不是HASH,並且KEY分區只採用一個或多個列名的一個列表。

CREATE TABLE tk (
    col1 INT NOT NULL,
    col2 CHAR(5),
    col3 DATE
)
PARTITION BY LINEAR KEY (col1)
PARTITIONS 3;

1.1.5 子分區

子分區是分區表中每個分區的再次分割。例如,考慮下面的CREATE TABLE 語句:

CREATE TABLE ts (id INT, purchased DATE)
    PARTITION BY RANGE(YEAR(purchased))
    SUBPARTITION BY HASH(TO_DAYS(purchased))
    SUBPARTITIONS 2
    (
        PARTITION p0 VALUES LESS THAN (1990),
        PARTITION p1 VALUES LESS THAN (2000),
        PARTITION p2 VALUES LESS THAN MAXVALUE
    );

表ts有3個RANGE分區。這3個分區(p0, p1, 和 p2)中的每一個分區又被進一步分成了2個子分區。實際上,整個表被分成了3 * 2 = 6個分區。但是,由於PARTITION BY RANGE子句的作用,p0分區的子分區裏,只會保存“purchased”列中值小於1990的那些記錄。

在MySQL 5.1中,對於已經通過RANGE或LIST分區了的表再進行子分區是可能的。子分區既可以使用HASH希分區,也可以使用KEY分區。這也被稱爲複合分區(composite partitioning)。

爲了對個別的子分區指定選項,使用SUBPARTITION 子句來明確定義子分區也是可能的。例如,創建在前面例子中給出的同一個表的、一個更加詳細的方式如下:

CREATE TABLE ts (id INT, purchased DATE)
    PARTITION BY RANGE(YEAR(purchased))
    SUBPARTITION BY HASH(TO_DAYS(purchased))
    (
        PARTITION p0 VALUES LESS THAN (1990)
        (
            SUBPARTITION s0,
            SUBPARTITION s1
        ),
        PARTITION p1 VALUES LESS THAN (2000)
        (
            SUBPARTITION s2,
            SUBPARTITION s3
        ),
        PARTITION p2 VALUES LESS THAN MAXVALUE
        (
            SUBPARTITION s4,
            SUBPARTITION s5
        )
    );

不過有幾點要注意的語法項:

  1. 每個分區必須有相同數量的子分區。
  2. 如果在一個分區表上的某個分區上使用SUBPARTITION來明確定義子分區,那麼就必須定義其他所有分區的子分區。

子分區可以用於特別大的表,在多個磁盤間分配數據和索引。假設有6個磁盤,分別爲/disk0, /disk1, /disk2等。現在考慮下面的例子:

CREATE TABLE ts (id INT, purchased DATE)
    PARTITION BY RANGE(YEAR(purchased))
    SUBPARTITION BY HASH(TO_DAYS(purchased))
    (
        PARTITION p0 VALUES LESS THAN (1990)
        (
            SUBPARTITION s0a
                DATA DIRECTORY = '/disk0'
                INDEX DIRECTORY = '/disk1',
            SUBPARTITION s0b
                DATA DIRECTORY = '/disk2' 
                INDEX DIRECTORY = '/disk3'
        ),
        PARTITION p1 VALUES LESS THAN (2000)
        (
            SUBPARTITION s1a
                DATA DIRECTORY = '/disk4/data' 
                INDEX DIRECTORY = '/disk4/idx',
            SUBPARTITION s1b
                DATA DIRECTORY = '/disk5/data' 
                INDEX DIRECTORY = '/disk5/idx'
        ),
        PARTITION p2 VALUES LESS THAN MAXVALUE
        (
            SUBPARTITION s2a,
            SUBPARTITION s2b
        )
    );
  • DATA DIRECTORY表示數據的物理文件的存放目錄
  • INDEX DIRECTORY表示索引的物理文件的存放目錄

在這個例子中,每個RANGE分區的數據和索引都使用一個單獨的磁盤。存儲的分配如下:

  1. 購買日期在1990年前的記錄佔了大量的存儲空間,所以把它分爲了四個部分進行存儲,組成p0分區的兩個子分區(s0a 和s0b)的數據和索引都分別用一個單獨的磁盤進行存儲。換句話說:
    • 子分區s0a 的數據保存在磁盤/disk0中。
    • 子分區s0a 的索引保存在磁盤/disk1中。
    • 子分區s0b 的數據保存在磁盤/disk2中。
    • 子分區s0b 的索引保存在磁盤/disk3中。
  2. 保存購買日期從1990年到1999年間的記錄(分區p1)不需要保存購買日期在1990年之前的記錄那麼大的存儲空間。這些記錄分在2個磁盤(/disk4和/disk5)上保存,而不是4個磁盤:
    • 屬於分區p1的第一個子分區(s1a)的數據和索引保存在磁盤/disk4上 — 其中數據保存在路徑/disk4/data下,索引保存在/disk4/idx下。
    • 屬於分區p1的第二個子分區(s1b)的數據和索引保存在磁盤/disk5上 — 其中數據保存在路徑/disk5/data下,索引保存在/disk5/idx下。
  3. 保存購買日期從2000年到現在的記錄(分區p2)不需要前面兩個RANGE分區那麼大的空間。當前,在默認的位置能夠足夠保存所有這些記錄。

1.2 分區維護

MySQL提供了許多修改分區表的方式。添加、刪除、重新定義、合併或拆分已經存在的分區是可能的。所有這些操作都可以通過使用ALTER TABLE命令的分區擴展來實現。

下面我們來總結一下所有分區維護的命令,爲簡便計,我們定義幾種partitions_exprs來替代如下子命令:

  • RANGE分區,range_partitions_exprs(n)即爲:
    PARTITION p VALUES LESS THAN (xxx)
    ...(n個PARTITION子句)
  • LIST分區,list_partitions_exprs即爲:
    PARTITION p VALUES IN (xxx,yyy,...),
    ...(n個PARTITION子句)

1.2.1 添加分區

  1. 爲已創建的未分區表創建分區:
    • RANGE:ALTER TABLE tb PARTITION BY RANGE (expr) ( range_partitions_exprs(n>0) );
    • LIST:ALTER TABLE tb PARTITION BY LIST (expr) ( list_partitions_exprs(n>0) );
    • HASH:ALTER TABLE tb PARTITION BY HASH(expr) PARTITIONS 2;
    • KEY:ALTER TABLE tb PARTITION BY KEY(expr) PARTITIONS 2;
  2. 爲分區表添加n個分區:
    • RANGE:ALTER TABLE tb ADD PARTITION ( range_partitions_exprs(n>0) );
    • LIST:ALTER TABLE tb ADD PARTITION ( list_partitions_exprs(n>0) );
    • HASH & KEY:ALTER TABLE tb ADD PARTITION PARTITIONS n;

對於通過RANGE分區的表,只可以使用ADD PARTITION添加新的分區到分區列表的尾端。設法通過這種方式在現有分區的前面或之間增加一個新的分區,將會導致報錯。此時建議使用下文的拆分操作,REORGANIZE命令可以運行expr重疊。

不能添加這樣一個新的LIST分區,該分區包含有已經包含在現有分區值列表中的任意值。如果試圖這樣做,將會導致錯誤。此時建議使用下文的拆分操作,REORGANIZE命令可以運行expr重疊。

1.2.2 重調整分區

  1. 數據不丟失的前提下,將m個分區合併爲n個分區(m>n),即減量重新組織分區
    • RANGE:ALTER TABLE tb REORGANIZE PARTITION s0,s1,... INTO ( range_partitions_exprs(n) )
    • LIST:ALTER TABLE tb REORGANIZE PARTITION s0,s1,... INTO ( list_partitions_exprs(n) )
    • HASH & KEY:ALTER TABLE clients COALESCE PARTITION n; (n小於原有分區數)
  2. 數據不丟失的前提下,將分區表的m個分區拆分爲n個分區(m<n),即增量重新組織分區
    • RANGE:ALTER TABLE tb REORGANIZE PARTITION p0,p1,... INTO ( range_partitions_exprs(n) )
    • LIST:ALTER TABLE tb REORGANIZE PARTITION p0,p1,... INTO ( list_partitions_exprs(n) )

不能使用REORGANIZE PARTITION來改變表的分區類型;也就是說,例如,不能把RANGE分區變爲HASH分區,反之亦然。也不能使用該命令來改變分區表達式或列。

  1. 重建分區,即先刪除分區中的所有記錄,然後重新插入。可用於整理分區碎片。
    • ALTER TABLE tb REBUILD PARTITION p0, p1;
  2. 優化分區,整理分區碎片
    • ALTER TABLE tb OPTIMIZE PARTITION p0, p1;

如從分區中刪除了大量的行,或者對一個帶有可變長度字段(VARCHAR、BLOB、TEXT類型)的行作了許多修改,可以使用優化分區來收回沒有使用的空間,並整理分區數據文件的碎片。

  1. 修復分區,修補被破壞的分區。
    • ALTER TABLE tb REPAIR PARTITION p0,p1;
  2. 檢查分區,這個命令可以告訴你分區中的數據或索引是否已經被破壞,如果被破壞,請使用修復分區來修補
    • ALTER TABLE tb CHECK PARTITION p1;

1.2.3 刪除分區

  1. 刪除一個分區,以及分區內的所有數據:
    • ALTER TABLE tb DROP PARTITION p2;
  2. 刪除一個分區,但保留分區內的所有數據(MySQL 5.5引入):
    • ALTER TABLE tb TRUNCATE PARTITION p2;

1.2.4 查詢分區數據

  1. 查看某個schema下某個表的分區信息
    • SELECT * FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = 'xxx' AND TABLE_NAME LIKE 'xxxx';
  2. 分析某個分區,主要看行數和名稱以及狀態
    • ALTER TABLE tb ANALYZE PARTITION p3;

2 分表

分表顧名思義,就是把一張超大的數據表,拆分爲多個較小的表,使得一些超大表的痼疾,得到有效的緩解。

超大表會帶來如下的影響:

  1. 單表數據量太大,會被頻繁讀寫,加鎖操作密集,導致性能降低。
  2. 單表數據量太大,對應的索引也會很大,查詢效率降低,增刪操作的性能也會降低。

分表和分區看起來十分類似,確實,分區已經能夠在磁盤層面將一張表拆分成多個文件了,理論上前面提到的大表的問題都能得到有效解決。因爲分區就是分表的數據庫實現版本。

在MySQL 5.1分區功能出現以前,要想解決超大表問題,只能採用分表操作,因爲這類問題十分常見,MySQL才自帶了一個分區功能,以達到相同的效果。

所以你可以直接說分區就是分表的替代,分表是分區出現以前的做法。不過這不代表我們就沒有必要學習分表了,相反,水平分表的功能或許可以用更加便捷的分區來替代,但是垂直分表的功能,分區卻無法替代。

分表只能通過程序代碼來實現,目前市面上有許多分表的框架。

2.1 分表和分區的區別

  1. 分區只是一張表中的數據和索引的存儲位置發生改變,分表則是將一張表分成多張表,是真實的有多套表的配套文件
  2. 分區沒法突破數據庫層面,不論怎麼分區,這些分區都要在一個數據庫下。而分表可以將子表分配在同一個庫中,也可以分配在不同庫中,突破數據庫性能的限制。
  3. 分區只能替代水平分表的功能,無法取代垂直分表的功能。

2.2 分表的類型

分表分爲水平分表和垂直分表。

2.2.1 水平分表

水平分表和分區很像,或者說分區就是水平分表的數據庫實現版本,它們分的都是行記錄,就像用一把刀,水平的將一個表切成多張表一樣。

針對數據量巨大的單張表(比如訂單表),我們按照某種規則,切分到多張表裏面去。

但是需要注意,如果這些表還是在同一個庫中,所以庫級別的數據庫操作還是有IO瓶頸。分表可以將單張表的數據切分到多個服務器上去,每個服務器具有相應的庫與子表,這是分區所不能有的優勢。

水平分表的切分規則一般有如下幾種:

  1. 範圍切分
    • 可以根據某個字段的範圍做劃分,比如訂單號字段,從0到10000一個表,10001到20000一個表。
  2. HASH取模
    • 可以根據某個字段的HASH取模做劃分,比如將一個用戶表分成10個子表,可以取用戶id,然後hash後取10的模,從而分配到不同的數據庫上。不過這種劃分一旦確定後,就無法改變子表數量了。
  3. 地理/國籍/類型等
    • 比如按照華東,華南,華北這樣來區分業務表,或者安卓用戶,IOS用戶等來區分用戶表。
  4. 時間
    • 按照時間切分,比如將6個月前,甚至一年前的數據切出去放到另外的一張表,因爲隨着時間流逝,這些表的數據被查詢的概率變小,所以沒必要和“熱數據”放在一起,這個也是“冷熱數據分離”。

2.2.2 垂直分表

水平分表分的是行記錄,而垂直分表,分的是列字段,它就像用一把刀,垂直的將一個表切成多張表一樣。

垂直分表是基於列字段進行的。一般是表中的字段較多,或者有數據較大長度較長(比如text,blob,varchar(1000)以上的字段)的字段時,我們將不常用的,或者數據量大的字段拆分到“擴展表”上。這樣避免查詢時,數據量太大造成的“跨頁”問題。

垂直分表的切分規則很好理解,一般是“不常用”或者“字段數據量大”這兩點來做切割,我們不多贅述。

3 分庫

分庫同樣是爲了應對超大數據帶來的巨大的IO需求,如果不拆庫,那麼單庫所能支持的吞吐能力和磁盤空間,就會成爲制衡業務發展的瓶頸。分庫的主要目的是爲突破單節點數據庫服務器的I/O能力限制,解決數據庫水平擴展性問題。

3.1 分區分表之外的分庫作用

也許你會問,我們有了分區和分表技術,還需要分庫來解決大數據量的問題嗎?對的,需要。

分區和分表可以把單表分到不同的硬盤上,但不能分配到不同服務器上。一臺機器的性能是有限制的,用分庫可以解決單臺服務器性能不夠,或者成本過高問題。

將一個庫分成多個庫,並在多個服務器上部署,就可以突破單服務器的性能瓶頸,這是分庫必要性的最主要原因。

3.2 分庫的類型

分庫同樣分爲水平分庫和垂直分庫。

  1. 水平分庫
    • 水平分庫和水平分表相似,並且關係緊密,水平分庫就是將單個庫中的表作水平分表,然後將子表分別置於不同的子庫當中,獨立部署。
    • 因爲庫中內容的主要載體是表,所以水平分庫和水平分表基本上如影隨形。
    • 例如用戶表,我們可以使用註冊時間的範圍來分表,將2020年註冊的用戶表usrtb2020部署在usrdata20中,2021年註冊的用戶表usrtb2021部署在usrdata21中。
  2. 垂直分庫
    • 同樣的,垂直分庫和垂直分表也十分類似,不過垂直分表拆分的是字段,而垂直分庫,拆分的是表。
    • 垂直分庫是將一個庫下的表作不同維度的分類,然後將其分配給不同子庫的策略。
    • 例如,我們可以將用戶相關的表都放置在usrdata這個庫中,將訂單相關的表都放置在odrdata中,以此類推。
    • 垂直分庫的分類維度有很多,可以按照業務模塊劃分(用戶/訂單...),按照技術模塊分(日誌類庫/圖片類庫...),或者空間,時間等等。

4 分庫分表存在的問題

  1. 事務問題。

    • 問題描述:在執行分庫分表之後,由於數據存儲到了不同的庫上,數據庫事務管理出現了困難。如果依賴數據庫本身的分佈式事務管理功能去執行事務,將付出高昂的性能代價;如果由應用程序去協助控制,形成程序邏輯上的事務,又會造成編程方面的負擔。
    • 解決方法:利用分佈式事務,協調不同庫之間的數據原子性,一致性。
  2. 跨庫跨表的join問題。

    • 問題描述:在執行了分庫分表之後,難以避免會將原本邏輯關聯性很強的數據劃分到不同的表、不同的庫上,這時,表的關聯操作將受到限制,我們無法join位於不同分庫的表,也無法join分表粒度不同的表,結果原本一次查詢能夠完成的業務,可能需要多次查詢才能完成。
    • 解決方法:tddl、MyCAT等都支持跨分片join。但是我們應該盡力避免跨庫join,如果一定要整合數據,那麼請在代碼中多次查詢完成。
  3. 額外的數據管理負擔和數據運算壓力。

    • 問題描述:額外的數據管理負擔,最顯而易見的就是數據的定位問題和數據的增刪改查的重複執行問題,這些都可以通過應用程序解決,但必然引起額外的邏輯運算,例如,對於一個記錄用戶成績的用戶數據表userTable,業務要求查出成績最好的100位,在進行分表之前,只需一個order by語句就可以搞定,但是在進行分表之後,將需要n個order by語句,分別查出每一個分表的前100名用戶數據,然後再對這些數據進行合併計算,才能得出結果。
    • 解決方法:無解,這是水平拓展的代價。

5 分庫分表方案產品

目前市面上的分庫分表中間件相對較多,其中基於代理方式的有MySQL Proxy和Amoeba;基於Hibernate框架的是Hibernate Shards;基於jdbc的有當當sharding-jdbc;基於mybatis的類似maven插件式的有蘑菇街的蘑菇街TSharding;通過重寫spring的ibatis template類的Cobar Client。

還有一些大公司的開源產品:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章