1. 程式人生 > >數據庫 分庫 分表 分區

數據庫 分庫 分表 分區

時報 mat bpa 了解 插入 5.6 切分 支持 sil

我們知道,如果我們使用mysql,當數據庫數據量達到一定數據量之後,會考慮對數據庫進行分庫分表等操作,但是在什麽情況下做怎麽的切分,下面分表介紹。

一、分庫

1 分庫原因

首先,在單臺數據庫服務器性能足夠的情況下,分庫對於數據庫性能是沒有影響的。在數據庫存儲上,database只起到一個namespace的作用。database中的表文件存儲在一個以database名命名的文件夾中。比如下面的employees數據庫:

mysql> show tables in employees;
+---------------------+
| Tables_in_employees |
+---------------------+
| departments         |
| dept_emp            |
| dept_manager        |
| employees           |
| salaries            |
| titles              |
+---------------------+

在操作系統中看是這樣的:

# haitian at haitian-coder.local in /usr/local/var/mysql/employees on git:master ● [21:19:47]
→ ls  
db.opt           dept_emp.frm     dept_manager.ibd salaries.frm     titles.ibd
departments.frm  dept_emp.ibd     employees.frm    salaries.ibd
departments.ibd  dept_manager.frm employees.ibd    titles.frm

database不是文件,只起到namespace的作用,所以MySQLdatabase大小當然也是沒有限制的,而且對裏面的表數量也沒有限制。

所以,為什麽要分庫呢?

答案是為了解決單臺服務器的性能問題,當單臺數據庫服務器無法支撐當前的數據量時,就需要根據業務邏輯緊密程度把表分成幾撮,分別放在不同的數據庫服務器中以降低單臺服務器的負載。

分庫一般考慮的是垂直切分,除非在垂直切分後,數據量仍然多到單臺服務器無法負載,才繼續水平切分。

比如一個論壇系統的數據庫因當前服務器性能無法滿足需要進行分庫。先垂直切分,按業務邏輯把用戶相關數據表比如用戶信息、積分、用戶間私信等放入user數據庫;論壇相關數據表比如板塊,帖子,回復等放入forum數據庫,兩個數據庫放在不同服務器上。

拆分後表往往不可能完全無關聯,比如帖子中的發帖人、回復人這些信息都在user數據庫中。未拆分前可能一次聯表查詢就能獲取當前帖子的回復、發帖人、回復人等所有信息,拆分後因為跨數據庫無法聯表查詢,只能多次查詢獲得最終數據。

所以總結起來,分庫的目的是降低單臺服務器負載,切分原則是根據業務緊密程度拆分,缺點是跨數據庫無法聯表查詢。

二、分表

1 分表的原因

當數據量超大的時候,B-Tree索引就無法起作用了。除非是索引覆蓋查詢,否則數據庫服務器需要根據索引掃描的結果回表,查詢所有符合條件的記錄,如果數據量巨大,這將產生大量隨機I/O,隨之,數據庫的響應時間將大到不可接受的程度。另外,索引維護(磁盤空間、I/O操作)的代價也非常高。

2 垂直分表

原因:

1.根據MySQL索引實現原理及相關優化策略的內容我們知道Innodb主索引葉子節點存儲著當前行的所有信息,所以減少字段可使內存加載更多行數據,有利於查詢。

2.受限於操作系統中的文件大小限制。

切分原則: 把不常用或業務邏輯不緊密或存儲內容比較多的字段分到新的表中可使表存儲更多數據。。

3 水平分表

原因:

1.隨著數據量的增大,table行數巨大,查詢的效率越來越低。

2.同樣受限於操作系統中的文件大小限制,數據量不能無限增加,當到達一定容量時,需要水平切分以降低單表(文件)的大小。

切分原則: 增量區間或散列或其他業務邏輯。

使用哪種切分方法要根據實際業務邏輯判斷。

比如對表的訪問多是近期產生的新數據,歷史數據訪問較少,可以考慮根據時間增量把數據按照一定時間段(比如每年)切分。

如果對表的訪問較均勻,沒有明顯的熱點區域,則可以考慮用範圍(比如每500w一個表)或普通Hash或一致性Hash來切分。

全局主鍵問題:

原本依賴數據庫生成主鍵(比如自增)的表在拆分後需要自己實現主鍵的生成,因為一般拆分規則是建立在主鍵上的,所以在插入新數據時需要確定主鍵後才能找到存儲的表。

實際應用中也已經有了比較成熟的方案。比如對於自增列做主鍵的表,flickr的全局主鍵生成方案很好的解決了性能和單點問題,具體實現原理可以參考這個帖子。除此之外,還有類似於uuid的全局主鍵生成方案,比如達達參考的Instagram的ID生成器。

一致性Hash:

使用一致性Hash切分比普通的Hash切分可擴展性更強,可以實現拆分表的添加和刪除。一致性Hash的具體原理可以參考這個帖子,如果拆分後的表存儲在不同服務器節點上,可以跟帖子一樣對節點名或ip取Hash;如果拆分後的表存在一個服務器中則可對拆分後的表名取Hash。

三、MySQL的分區表

上面介紹的傳統的分庫分表都是在應用層實現,拆分後都要對原有系統進行很大的調整以適應新拆分後的庫或表,比如實現一個SQL中間件、原本的聯表查詢改成兩次查詢、實現一個全局主鍵生成器等等。

而下面介紹的MySQL分區表是在數據庫層面,MySQL自己實現的分表功能,在很大程度上簡化了分表的難度。

1 介紹

對用戶來說,分區表是一個獨立的邏輯表,但是底層由多個物理子表實現。

也就是說,對於原表分區後,對於應用層來說可以不做變化,我們無需改變原有的SQL語句,相當於MySQL幫我們實現了傳統分表後的SQL中間件,當然,MySQL的分區表的實現要復雜很多。

另外,在創建分區時可以指定分區的索引文件和數據文件的存儲位置,所以可以把數據表的數據分布在不同的物理設備上,從而高效地利用多個硬件設備。

一些限制:

1.在5.6.7之前的版本,一個表最多有1024個分區;從5.6.7開始,一個表最多可以有8192個分區。

2.分區表中無法使用外鍵約束。

3.主表的所有唯一索引列(包括主鍵)都必須包含分區字段。MySQL官方文檔中寫的是:

All columns used in the partitioning expression for a partitioned table must be part of every unique key that the table may have.

這句話不是很好理解,需要通過例子才能明白,MySQL官方文檔也為此限制特意做了舉例和解釋。

2 分區表類型

RANGE分區

根據範圍分區,範圍應該連續但是不重疊,使用PARTITION BY RANGE, VALUES LESS THAN關鍵字。不使用COLUMNS關鍵字時RANGE括號內必須為整數字段名或返回確定整數的函數。

根據數值範圍:

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 NOT NULL,
    store_id INT NOT NULL
)
PARTITION BY RANGE (store_id) (
    PARTITION p0 VALUES LESS THAN (6),
    PARTITION p1 VALUES LESS THAN (11),
    PARTITION p2 VALUES LESS THAN (16),
    PARTITION p3 VALUES LESS THAN MAXVALUE
);

根據TIMESTAMP範圍:

CREATE TABLE quarterly_report_status (
    report_id INT NOT NULL,
    report_status VARCHAR(20) NOT NULL,
    report_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
PARTITION BY RANGE ( UNIX_TIMESTAMP(report_updated) ) (
    PARTITION p0 VALUES LESS THAN ( UNIX_TIMESTAMP(‘2008-01-01 00:00:00‘) ),
    PARTITION p1 VALUES LESS THAN ( UNIX_TIMESTAMP(‘2008-04-01 00:00:00‘) ),
    PARTITION p2 VALUES LESS THAN ( UNIX_TIMESTAMP(‘2008-07-01 00:00:00‘) ),
    PARTITION p3 VALUES LESS THAN ( UNIX_TIMESTAMP(‘2008-10-01 00:00:00‘) ),
    PARTITION p4 VALUES LESS THAN ( UNIX_TIMESTAMP(‘2009-01-01 00:00:00‘) ),
    PARTITION p5 VALUES LESS THAN ( UNIX_TIMESTAMP(‘2009-04-01 00:00:00‘) ),
    PARTITION p6 VALUES LESS THAN ( UNIX_TIMESTAMP(‘2009-07-01 00:00:00‘) ),
    PARTITION p7 VALUES LESS THAN ( UNIX_TIMESTAMP(‘2009-10-01 00:00:00‘) ),
    PARTITION p8 VALUES LESS THAN ( UNIX_TIMESTAMP(‘2010-01-01 00:00:00‘) ),
    PARTITION p9 VALUES LESS THAN (MAXVALUE)
);

添加COLUMNS關鍵字可定義非integer範圍及多列範圍,不過需要註意COLUMNS括號內只能是列名,不支持函數;多列範圍時,多列範圍必須呈遞增趨勢:

根據DATEDATETIME範圍:

CREATE TABLE members (
    firstname VARCHAR(25) NOT NULL,
    lastname VARCHAR(25) NOT NULL,
    username VARCHAR(16) NOT NULL,
    email VARCHAR(35),
    joined DATE NOT NULL
)
PARTITION BY RANGE COLUMNS(joined) (
    PARTITION p0 VALUES LESS THAN (‘1960-01-01‘),
    PARTITION p1 VALUES LESS THAN (‘1970-01-01‘),
    PARTITION p2 VALUES LESS THAN (‘1980-01-01‘),
    PARTITION p3 VALUES LESS THAN (‘1990-01-01‘),
    PARTITION p4 VALUES LESS THAN MAXVALUE
);

根據多列範圍:

CREATE TABLE rc3 (
    a INT,
    b INT
)
PARTITION BY RANGE COLUMNS(a,b) (
    PARTITION p0 VALUES LESS THAN (0,10),
    PARTITION p1 VALUES LESS THAN (10,20),
    PARTITION p2 VALUES LESS THAN (10,30),
    PARTITION p3 VALUES LESS THAN (10,35),
    PARTITION p4 VALUES LESS THAN (20,40),
    PARTITION p5 VALUES LESS THAN (MAXVALUE,MAXVALUE)
 );

List分區

根據具體數值分區,每個分區數值不重疊,使用PARTITION BY LISTVALUES IN關鍵字。跟Range分區類似,不使用COLUMNS關鍵字時List括號內必須為整數字段名或返回確定整數的函數。

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)
);

數值必須被所有分區覆蓋,否則插入一個不屬於任何一個分區的數值會報錯。

mysql> CREATE TABLE h2 (
    ->   c1 INT,
    ->   c2 INT
    -> )
    -> PARTITION BY LIST(c1) (
    ->   PARTITION p0 VALUES IN (1, 4, 7),
    ->   PARTITION p1 VALUES IN (2, 5, 8)
    -> );
Query OK, 0 rows affected (0.11 sec)

mysql> INSERT INTO h2 VALUES (3, 5);
ERROR 1525 (HY000): Table has no partition for value 3

當插入多條數據出錯時,如果表的引擎支持事務(Innodb),則不會插入任何數據;如果不支持事務,則出錯前的數據會插入,後面的不會執行。

可以使用IGNORE關鍵字忽略出錯的數據,這樣其他符合條件的數據會全部插入不受影響。

mysql> TRUNCATE h2;
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM h2;
Empty set (0.00 sec)

mysql> INSERT IGNORE INTO h2 VALUES (2, 5), (6, 10), (7, 5), (3, 1), (1, 9);
Query OK, 3 rows affected (0.00 sec)
Records: 5  Duplicates: 2  Warnings: 0

mysql> SELECT * FROM h2;
+------+------+
| c1   | c2   |
+------+------+
|    7 |    5 |
|    1 |    9 |
|    2 |    5 |
+------+------+
3 rows in set (0.00 sec)

Range分區相同,添加COLUMNS關鍵字可支持非整數和多列。

Hash分區

Hash分區主要用來確保數據在預先確定數目的分區中平均分布,Hash括號內只能是整數列或返回確定整數的函數,實際上就是使用返回的整數對分區數取模。

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;
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( YEAR(hired) )
PARTITIONS 4;

Hash分區也存在與傳統Hash分表一樣的問題,可擴展性差。MySQL也提供了一個類似於一致Hash的分區方法-線性Hash分區,只需要在定義分區時添加LINEAR關鍵字,如果對實現原理感興趣,可以查看官方文檔。

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 LINEAR HASH( YEAR(hired) )
PARTITIONS 4;

Key分區

按照KEY進行分區類似於按照HASH分區,除了HASH分區使用的用戶定義的表達式,而KEY分區的 哈希函數是由MySQL 服務器提供。MySQL 簇(Cluster)使用函數MD5()來實現KEY分區;對於使用其他存儲引擎的表,服務器使用其自己內部的 哈希函數,這些函數是基於與PASSWORD()一樣的運算法則。

Key分區與Hash分區很相似,只是Hash函數不同,定義時把Hash關鍵字替換成Key即可,同樣Key分區也有對應與線性Hash的線性Key分區方法。

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

另外,當表存在主鍵或唯一索引時可省略Key括號內的列名,Mysql將按照主鍵-唯一索引的順序選擇,當找不到唯一索引時報錯。

子分區

子分區是分區表中每個分區的再次分割。創建子分區方法:

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
    );

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
                DATA DIRECTORY = ‘/disk0/data‘
                INDEX DIRECTORY = ‘/disk0/idx‘,
            SUBPARTITION s1
                DATA DIRECTORY = ‘/disk1/data‘
                INDEX DIRECTORY = ‘/disk1/idx‘
        ),
        PARTITION p1 VALUES LESS THAN (2000) (
            SUBPARTITION s2
                DATA DIRECTORY = ‘/disk2/data‘
                INDEX DIRECTORY = ‘/disk2/idx‘,
            SUBPARTITION s3
                DATA DIRECTORY = ‘/disk3/data‘
                INDEX DIRECTORY = ‘/disk3/idx‘
        ),
        PARTITION p2 VALUES LESS THAN MAXVALUE (
            SUBPARTITION s4
                DATA DIRECTORY = ‘/disk4/data‘
                INDEX DIRECTORY = ‘/disk4/idx‘,
            SUBPARTITION s5
                DATA DIRECTORY = ‘/disk5/data‘
                INDEX DIRECTORY = ‘/disk5/idx‘
        )
    );

需要註意的是:每個分區的子分區數必須相同。如果在一個分區表上的任何分區上使用SUBPARTITION來明確定義任何子分區,那麽就必須定義所有的子分區,且必須指定一個全表唯一的名字。

分區表的使用及查詢優化

根據實際情況選擇分區方法

對現有表分區的原則與傳統分表一樣。

傳統的按照增量區間分表對應於分區的Range分區,比如對表的訪問多是近期產生的新數據,歷史數據訪問較少,則可以按一定時間段(比如年或月)或一定數量(比如100萬)對表分區,具體根據哪種取決於表索引結構。分區後最後一個分區即為近期產生的數據,當一段時間過後數據量再次變大,可對最後一個分區重新分區(REORGANIZE PARTITION)把一段時間(一年或一月)或一定數量(比如100萬)的數據分離出去。

傳統的散列方法分表對應於分區的Hash/Key分區,具體方法上面已經介紹過。

查詢優化

分區的目的是為了提高查詢效率,如果查詢範圍是所有分區那麽就說明分區沒有起到作用,我們用explain partitions命令來查看SQL對於分區的使用情況。

一般來說,就是在where條件中加入分區列。

比如表salaries結構為:

mysql> show create table salaries\G;
*************************** 1. row ***************************
       Table: salaries
Create Table: CREATE TABLE `salaries` (
  `emp_no` int(11) NOT NULL,
  `salary` int(11) NOT NULL,
  `from_date` date NOT NULL,
  `to_date` date NOT NULL,
  PRIMARY KEY (`emp_no`,`from_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE (year(from_date))
(PARTITION p1 VALUES LESS THAN (1985) ENGINE = InnoDB,
 PARTITION p2 VALUES LESS THAN (1986) ENGINE = InnoDB,
 PARTITION p3 VALUES LESS THAN (1987) ENGINE = InnoDB,
 PARTITION p4 VALUES LESS THAN (1988) ENGINE = InnoDB,
 PARTITION p5 VALUES LESS THAN (1989) ENGINE = InnoDB,
 PARTITION p6 VALUES LESS THAN (1990) ENGINE = InnoDB,
 PARTITION p7 VALUES LESS THAN (1991) ENGINE = InnoDB,
 PARTITION p8 VALUES LESS THAN (1992) ENGINE = InnoDB,
 PARTITION p9 VALUES LESS THAN (1993) ENGINE = InnoDB,
 PARTITION p10 VALUES LESS THAN (1994) ENGINE = InnoDB,
 PARTITION p11 VALUES LESS THAN (1995) ENGINE = InnoDB,
 PARTITION p12 VALUES LESS THAN (1996) ENGINE = InnoDB,
 PARTITION p13 VALUES LESS THAN (1997) ENGINE = InnoDB,
 PARTITION p14 VALUES LESS THAN (1998) ENGINE = InnoDB,
 PARTITION p15 VALUES LESS THAN (1999) ENGINE = InnoDB,
 PARTITION p16 VALUES LESS THAN (2000) ENGINE = InnoDB,
 PARTITION p17 VALUES LESS THAN (2001) ENGINE = InnoDB,
 PARTITION p18 VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */

則下面的查詢沒有利用分區,因為partitions中包含了所有的分區:

mysql> explain partitions select * from salaries where salary > 100000\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: salaries
   partitions: p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,p16,p17,p18
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2835486
        Extra: Using where

只有在where條件中加入分區列才能起到作用,過濾掉不需要的分區:

mysql> explain partitions select * from salaries where salary > 100000 and from_date > ‘1998-01-01‘\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: salaries
   partitions: p15,p16,p17,p18
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1152556
        Extra: Using where

與普通搜索一樣,在運算符左側使用函數將使分區過濾失效,即使與分區函數想同也一樣:

mysql> explain partitions select * from salaries where salary > 100000 and year(from_date) > 1998\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: salaries
   partitions: p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,p16,p17,p18
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2835486
        Extra: Using where

四、分區和分表的比較

  • 傳統分表後,countsum等統計操作只能對所有切分表進行操作後之後在應用層再次計算得出最後統計數據。而分區表則不受影響,可直接統計。

Queries involving aggregate functions such as SUM() and COUNT() can easily be parallelized. A simple example of such a query might be SELECT salesperson_id, COUNT(orders) as order_total FROM sales GROUP BY salesperson_id;. By “parallelized,” we mean that the query can be run simultaneously on each partition, and the final result obtained merely by summing the results obtained for all partitions.

  • 分區對原系統改動最小,分區只涉及數據庫層面,應用層不需要做出改動。

  • 分區有個限制是主表的所有唯一字段(包括主鍵)必須包含分區字段,而分表沒有這個限制。

  • 分表包括垂直切分和水平切分,而分區只能起到水平切分的作用。

五、常用分庫分表

1 tddl介紹

tddl主要分為三次,matrix、group、atom層;

matrix層

Sql解析->規則引擎計算->數據執行->合並結果

group層

讀寫分離、權重、寫的HA切換、讀的HA切換、slave節點

atom層

1 單個數據庫的抽象

2 jboss數據源, ip port 用戶名密碼都可 以動態修改

3 Thread count模式,保護 業務的處理線程,超過指定值,保護啟動。

4 動態阻止某個SQL執行

5 執行次數統計和限制

tddl 唯一鍵生成方式

目前基於tddl進行分庫分表後,原本一個數據庫上的自增id的結果,在分庫分表下並不是全局唯一的. 所以,分庫分表後需要有一種技術可以生成全局的唯一id.

唯一鍵的生成方式必須具備:1)全局唯一;2)高可用;3)高性能;

tddl主要使用數據庫+內存的方式實現,在內存中進行分配 優勢:簡單高效 缺點:無法保證自增順序如下,下面內步長1000:

group value
group_0 0
group_1 1000
group_2 2000
group_3 3000

當需要產生唯一鍵時,從上面4個group中隨機選擇一個,獲取value+步長的id,例如,從group_1獲取1000~1000+1000的id,批量獲取,提高性能。獲取之後,數據庫的記錄變為下面的格式:

group value
group_0 0
group_1 5000
group_2 2000
group_3 3000

每次獲取之後,將對應group的值變為,value+group的個數*步長。

數據庫 分庫 分表 分區