微服務(wù)架構(gòu)的數(shù)據(jù)庫為什么喜歡分庫分表?
1.引入
微服務(wù)架構(gòu)想必大家都是有所耳聞。
簡單來說,微服務(wù)架構(gòu)就是把傳統(tǒng)的一個(gè)單體應(yīng)用以一套"小服務(wù)"的方式進(jìn)行開發(fā),這些"小服務(wù)"可以運(yùn)行在不同機(jī)器上,它們在自己的進(jìn)程中運(yùn)行,"小服務(wù)"之間可以通過像是 HTTP API 這樣的輕量級的機(jī)制進(jìn)行通信,這些"小服務(wù)"緊緊圍繞項(xiàng)目的業(yè)務(wù)需求開發(fā),同時(shí),它們是以業(yè)務(wù)邊界進(jìn)行劃分成獨(dú)立的微服務(wù)。這些微服務(wù)看似獨(dú)立又像是一個(gè)整體,構(gòu)成了一個(gè)業(yè)務(wù)集群。
2.為何分庫
微服務(wù)架構(gòu)從業(yè)務(wù)邏輯實(shí)現(xiàn)的角度上看系統(tǒng)的性能得到了優(yōu)化,可是對數(shù)據(jù)庫的負(fù)擔(dān)就加重了。假設(shè)一個(gè)分布式電子商務(wù)系統(tǒng),那么這個(gè)系統(tǒng)會包含會員信息、訂單信息、商品信息、商品庫存信息等等內(nèi)容,數(shù)據(jù)存放在數(shù)據(jù)庫中,要訪問數(shù)據(jù),就要與數(shù)據(jù)庫建立連接,而數(shù)據(jù)庫的連接是有限的,況且在這樣的業(yè)務(wù)環(huán)境下,會出現(xiàn)較多的高并發(fā)場景,如果都同時(shí)向這個(gè)商城數(shù)據(jù)庫訪問數(shù)據(jù),數(shù)據(jù)庫顯然是受不起這樣的折騰。如圖:
上文提到,微服務(wù)是以業(yè)務(wù)邊界進(jìn)行劃分的,那么這些服務(wù)就可以使用不同的編程語言書寫,以及不同數(shù)據(jù)存儲技術(shù),前提是保持最低限度的集中式管理。也就是說,各個(gè)微服務(wù)處理的數(shù)據(jù)可以達(dá)到自治。
因此,為了處理高并發(fā),設(shè)計(jì)數(shù)據(jù)庫就可以采取分庫的方式進(jìn)行,使得各個(gè)微服務(wù)擁有自己獨(dú)立的數(shù)據(jù)庫,就好比訂單微服務(wù)自治訂單信息、支付流水信息、退款信息等等,當(dāng)訂單微服務(wù)需要會員微服務(wù)的會員數(shù)據(jù)時(shí),可以通過服務(wù)的通訊機(jī)制,比如feign,以此達(dá)到分擔(dān)傳統(tǒng)模式壓力的效果,如圖:
同時(shí),對于每一個(gè)劃分好的庫也可以再進(jìn)行分庫部署,劃分出的庫擁有相同的表,不同的只有存放的數(shù)據(jù)集。它可以有效的緩解單機(jī)單庫的性能瓶頸和壓力隨著需求的細(xì)化,項(xiàng)目的業(yè)務(wù)量是龐大的,這也導(dǎo)致項(xiàng)目的數(shù)據(jù)量是龐大的,數(shù)據(jù)庫分庫部署可以有效減輕磁盤負(fù)擔(dān)。如圖:
3.為何分表
微服務(wù)開發(fā)中,我們經(jīng)常會遇到大表的情況,所謂大表是指存儲了百萬級乃至千萬級條記錄的表,這樣的表數(shù)據(jù)過于龐大,導(dǎo)致數(shù)據(jù)庫在查詢和插入的時(shí)候耗時(shí)太長,就算使用索引,在大量的數(shù)據(jù)面前,查詢的效率也會有所降低,更何況是使用不到索引的情況,下邊列舉一些使用不到索引的情況:
# 使用LIKE通配符置于字符串前面
mysql> SELECT * FROM test WHERE name LIKE '%小王';
# 函數(shù)運(yùn)算
mysql> SELECT * FROM test WHERE UPPER(name) = 'ZS';
分表是對表進(jìn)行分區(qū),最主要的目的就是減輕數(shù)據(jù)庫的負(fù)擔(dān),提高數(shù)據(jù)庫的效率。表分區(qū)是根據(jù)一定的規(guī)則,把數(shù)據(jù)庫的一張表分解為多個(gè)更小的表,使用分區(qū)的表從邏輯上看還是一個(gè)表,但物理存儲分為了多份,表分區(qū)后的每個(gè)部分,都可以獨(dú)立的進(jìn)行數(shù)據(jù)處理,分區(qū)具有以下好處:
- 存儲空間更大了
- 查詢速度更快,只需要掃描需要的分區(qū)表,再將結(jié)果進(jìn)行合并。不會因?yàn)槿頀呙?,而浪費(fèi)不必要的資源
- 對于刪除數(shù)據(jù)來說,處理更方便了,只需要刪除對應(yīng)分區(qū)的數(shù)據(jù)即可
- 跨越磁盤存儲,充分利用磁盤讀取,提高吞吐量
分區(qū)是將數(shù)據(jù)分段劃分在多個(gè)位置存放,可以是同一塊磁盤也可以在不同的機(jī)器。表分區(qū)有很多的策略,根據(jù)不同的策略可以適應(yīng)多種業(yè)務(wù)場景,例如可以通過表內(nèi)屬性值的范圍進(jìn)行分表,如下圖,將商城支付流水表以流水時(shí)間進(jìn)行劃分:
表在分區(qū)后,表面上還是一張表,但數(shù)據(jù)散列到多個(gè)位置了。應(yīng)用程序讀寫的時(shí)候操作的還是大表的表名,數(shù)據(jù)庫系統(tǒng)自動去組織分區(qū)的數(shù)據(jù)。 使用分區(qū)需要注意:
MySQL 8.0版本前支持創(chuàng)建表分區(qū)的存儲引擎有InnoDB、Memory、MyISAM、MERGE,MySQL 8.0之后就只支持InnoDB存儲引擎了
分區(qū)表必須一致,即同一張表分區(qū)后,各個(gè)分區(qū)表必須使用一致的存儲引擎
3.1.表分區(qū)
表分區(qū)可在創(chuàng)建數(shù)據(jù)庫表的時(shí)候進(jìn)行指定,格式如下:
CREATE TABLE TABLE_NAME(
………
)
PARTITION BY RANGE|LIST|HASH(TABLE_COLUMN)(
PARTITION P0……
)
上文提到表分區(qū)有不同策略,也可以稱為不同類型:
- RANGE分區(qū)
- LIST分區(qū)
- COLUMNS分區(qū)
- HASH分區(qū)
- KEY分區(qū)
- 子分區(qū)
3.1.1.RANGE分區(qū)
RANGE分區(qū)是基于一個(gè)給定連續(xù)區(qū)間范圍,區(qū)間之間的不能互相重疊,數(shù)據(jù)會根據(jù)范圍,分配到不同的分區(qū),RANGE的分區(qū)鍵必須是單列的int類型,每個(gè)分區(qū)范圍必須按順序(后一個(gè)分區(qū)范圍值比前一個(gè)值大)。 假設(shè)指定一表為RANGE分區(qū),分4個(gè)區(qū),最后一個(gè)區(qū)為了防止數(shù)據(jù)定義問題,將其設(shè)置為數(shù)值最大值“MAXVALUE”:
mysql> CREATE TABLE testrange(
-> id INT PRIMARY KEY AUTO_INCREMENT,
-> name VARCHAR(10))
-> PARTITION BY RANGE(id)(
-> PARTITION p0 VALUES LESS THAN(50),
-> PARTITION p1 VALUES LESS THAN(100),
-> PARTITION p2 VALUES LESS THAN(150),
-> PARTITION p3 VALUES LESS THAN(MAXVALUE));
3.1.2.LIST分區(qū)
LIST分區(qū)的分區(qū)鍵的類型也只能是int類型,LIST分區(qū)是基于枚舉值列表進(jìn)行分區(qū),枚舉的范圍同樣不能有重復(fù)的值,如果插入數(shù)據(jù)不在枚舉范圍之內(nèi),則會報(bào)錯(cuò)。
# 指定為LIST分區(qū),分2個(gè)區(qū)
mysql> CREATE TABLE testlist(
-> id INT PRIMARY KEY AUTO_INCREMENT,
-> name VARCHAR(10))
-> PARTITION BY LIST(id)(
-> PARTITION p0 VALUES IN (1,2,3),
-> PARTITION p1 VALUES IN (4,6,9));
mysql> INSERT INTO testlist VALUES(null,'張三');
mysql> INSERT INTO testlist VALUES(2,'李四');
# 插入列限制數(shù)值不存在的數(shù),則會提示這張表沒有改值的分區(qū)
mysql> INSERT INTO testlist VALUES(5,'李三');
ERROR 1526 (HY000): Table has no partition for value 5
3.1.3.COLUMNS分區(qū)
COLUMNS分區(qū)區(qū)別于RANGE分區(qū)和LIST分區(qū)的最大特點(diǎn)是支持多列的分區(qū),COLUMNS分區(qū)有兩種形式,RANGE COLUMNS和LIST COLUMNS分區(qū)。兩種分區(qū)形式都支持整數(shù)類型,日期類型,字符類型,區(qū)別在于,如果COLUMNS分區(qū)的分區(qū)鍵有多個(gè),當(dāng)數(shù)據(jù)庫要進(jìn)行數(shù)據(jù)插入時(shí),會先考慮第一個(gè)鍵是否滿足,如果滿足條件就會進(jìn)行數(shù)據(jù)寫入,如果不滿足條件就要對第二個(gè)鍵的條件進(jìn)行判斷,以此類推。
mysql> CREATE TABLE testrancol(
-> sid INT,cid INT,PRIMARY KEY(sid,cid))
-> PARTITION BY RANGE COLUMNS(sid,cid)(
-> PARTITION p0 VALUES LESS THAN(1,10),
-> PARTITION p1 VALUES LESS THAN(10,20));
3.1.4.HASH分區(qū)
HASH分區(qū)主要用于將一整個(gè)數(shù)據(jù),分散為若干個(gè)相等數(shù)量的分區(qū),HASH分區(qū)有兩種類型:
(1)常規(guī)HASH分區(qū),使用的是取模運(yùn)算。假設(shè)分區(qū)數(shù)為4,則有0,1,2,3四個(gè)值,對應(yīng)分區(qū)為四個(gè)。因?yàn)槭褂玫娜∧_\(yùn)算,所以分區(qū)鍵必須是整數(shù)類型的列或返回整數(shù)類型的表達(dá)式:
mysql> CREATE TABLE testhash1(
-> id INT PRIMARY KEY,num VARCHAR(10))
-> PARTITION BY HASH(id) PARTITIONS 4;
# 如果此時(shí)插入數(shù)據(jù)id為83,則模4取余得3,將數(shù)據(jù)放在第三個(gè)分區(qū)中
(2)線性HASH分區(qū),其語法書寫不同于常規(guī)HASH分區(qū),需要加上LINEAR關(guān)鍵字。在數(shù)據(jù)分配的時(shí)候,使用的是2的冪運(yùn)算進(jìn)行分配數(shù)據(jù)的配分有兩步運(yùn)算:
mysql> CREATE TABLE testhash2(
-> id INT PRIMARY KEY,num VARCHAR(10))
-> PARTITION BY LINEAR HASH(id) PARTITIONS 4;
# 1、第一步,算出V的值
# 計(jì)算方式為:V=POWER(2,CEILING(LOG(2,分區(qū)數(shù))))
# 假設(shè)分區(qū)數(shù)為4,log()的值為2
# Ceiling()取最小整數(shù),依舊是2
# power返回2的2次方,最后V=4
# 2、第二步,對分區(qū)鍵和V-1進(jìn)行位與運(yùn)算
# 假設(shè)分區(qū)鍵值為8和10:
# 那么插入8與10和4-1的3進(jìn)行按位與運(yùn)算得出插入分區(qū)分別是0與2分區(qū)
3.1.5.KEY分區(qū)
KEY分區(qū)有與HASH分區(qū)類似,不同的地方有以下幾點(diǎn):
- KEY分區(qū)不允許使用自定義表達(dá)式作為分區(qū)鍵
- KEY分區(qū)如果不指定分區(qū)鍵,則會默認(rèn)使用表中主鍵。如果沒有主鍵,則使用非空唯一鍵。如果都沒有,那就必須手動指定分區(qū)鍵
- 可以使用非數(shù)值類型的列作為分區(qū)鍵 KEY分區(qū)也分為常規(guī)KEY分區(qū)和線性KEY分區(qū),其運(yùn)算規(guī)則與HASH分區(qū)一致,不多做贅述。
mysql> CREATE TABLE testhash1(
-> id INT PRIMARY KEY,num VARCHAR(10))
-> PARTITION BY [LINEAR] KEY(id) PARTITIONS 4;
3.1.6.子分區(qū)
子分區(qū)是對分區(qū)表的每個(gè)區(qū)分,進(jìn)行二次的分區(qū),使用RANGE和LIST對表進(jìn)行分區(qū),則可以使用HASH或KEY進(jìn)行子分區(qū),假設(shè)表有2個(gè)分區(qū),這2個(gè)分區(qū)又被進(jìn)一步的分為2個(gè)子分區(qū),總共有4個(gè)分區(qū),寫法有兩種:
- 隱式創(chuàng)建子分區(qū),子分區(qū)的名字是自動創(chuàng)建且重名,但不會沖突,輸入小于1900的年份,會按HASH分區(qū)的規(guī)則(模2運(yùn)算),分別存放在兩個(gè)P0中:
mysql> CREATE TABLE testfh(
-> id INT,pur DATE)
-> PARTITION BY RANGE(YEAR(pur))
-> SUBPARTITION BY HASH(TO_DAYS(pur))
-> SUBPARTITIONS 2 (
-> PARTITION p0 VALUES LESS THAN(1900),
-> PARTITION p1 VALUES LESS THAN MAXVALUE);
- 顯示創(chuàng)建子分區(qū),要求每個(gè)分區(qū)的子分區(qū)數(shù)量必須一致,且分區(qū)的創(chuàng)建必須一致,即全部子分區(qū)都使用隱式創(chuàng)建或顯式創(chuàng)建,不可混用,同時(shí)子分區(qū)的名字必須唯一
mysql> CREATE TABLE testfh(
-> id INT,pur DATE)
-> PARTITION BY RANGE(YEAR(pur))
-> SUBPARTITION BY HASH(TO_DAYS(pur))(
-> PARTITION p0 VALUES LESS THAN(1900)(
-> SUBPARTITION s0, SUBPARTITION s1),
-> PARTITION p1 VALUES LESS THAN MAXVALUE(
-> SUBPARTITION s2, SUBPARTITION s3));
3.2.表分區(qū)注意點(diǎn)
3.2.1.RANGE表分區(qū)注意點(diǎn)
使用RANGE策略進(jìn)行表分區(qū)的好處在于分區(qū)后數(shù)據(jù)的擴(kuò)容性好,不需要進(jìn)行數(shù)據(jù)遷移,如果插入的數(shù)據(jù)超過原先建立分區(qū)的范圍可以根據(jù)實(shí)際情況考慮在原有基礎(chǔ)上增加表分區(qū)或者水平部署一張相同的表進(jìn)行存放數(shù)據(jù),增加表分區(qū)可參考以下格式:
# RANGE分區(qū)中添加分區(qū),是在尾部進(jìn)行添加,所以如果RANGE已經(jīng)有包含最大值的分區(qū),那么新添加的分區(qū)就會報(bào)錯(cuò)
ALTER TABLE 表名 ADD PARTITION (PARTITION 分區(qū)名 VALES LESS THAN (范圍));
需要注意的是表在RANGE分區(qū)的時(shí)候指定的分區(qū)鍵需要考慮實(shí)際情況,如果使用不當(dāng)會造成個(gè)別分區(qū)數(shù)據(jù)過多的情況。
假設(shè)一個(gè)電子商務(wù)系統(tǒng)需要存放訂單的相關(guān)信息,用戶進(jìn)行商品購買則產(chǎn)生訂單,訂單往往包含多個(gè)不同商品,可以設(shè)置訂單項(xiàng)表用于存放訂單的每個(gè)商品id、商品名、價(jià)格、數(shù)量等數(shù)據(jù),如果以商品id作為分區(qū)鍵則會產(chǎn)生"數(shù)據(jù)熱點(diǎn)"問題,有些商品銷量好,那么分區(qū)的數(shù)據(jù)就多,一些商品銷量不好那么數(shù)據(jù)就會很少。
3.2.2.HASH表分區(qū)注意點(diǎn)
使用HASH策略的好處就在于解決數(shù)據(jù)熱點(diǎn)問題,但是,HASH表分區(qū)的分區(qū)數(shù)量是固定的,如果數(shù)據(jù)過多達(dá)到了瓶頸,就要將分區(qū)數(shù)量進(jìn)行修改了,因此原先已經(jīng)存放的數(shù)據(jù)又要進(jìn)行取模運(yùn)算重新存放,需要進(jìn)行數(shù)據(jù)遷移,語法如下:
# 增加2個(gè)分區(qū)
ALTER TABLE 表名 PARTITION 2;
# 減少2個(gè)分區(qū)
ALTER TABLE 表名 COALESCE PARTITION 2;
3.2.3.MySQL表分區(qū)遇到NULL值
當(dāng)MySQL表分區(qū)遇到NULL值,MySQL不會禁止分區(qū)鍵有NULL值,但不同的分區(qū)類型會將NULL值當(dāng)成不同的數(shù)據(jù)對待,如:
在RANGE分區(qū)中,NULL值被當(dāng)成最小的數(shù)。
在LIST分區(qū)中,NULL被當(dāng)成字符串,如果NULL不在LIST的枚舉范圍中,還有出現(xiàn)報(bào)錯(cuò)
在HASH和KEY中,NULL值被當(dāng)成0 所以,在使用分區(qū)時(shí),要注意處理NULL值輸入,以免出現(xiàn)MySQL的誤判,將分區(qū)鍵設(shè)為NOT NULL或默認(rèn)值都可以好的對應(yīng)這種情況。
4.總結(jié)
本文介紹了為什么微服務(wù)架構(gòu)大多采用分庫分表的方式進(jìn)行設(shè)計(jì)數(shù)據(jù)庫,當(dāng)然,分布式系統(tǒng)在設(shè)計(jì)過程中進(jìn)行分庫分表還需要注意一些問題,比如,在我們創(chuàng)建數(shù)據(jù)庫表的時(shí)候是否可以先考慮表內(nèi)數(shù)據(jù)的特性,事先將一些不經(jīng)常需要更改的內(nèi)容抽離出來,形成一張新的表,從某種程度上說,這種方式也是一種"分表"的操作。
同時(shí),在進(jìn)行分庫在涉及事務(wù)安全性的時(shí)候也需要注意,比如商城中用戶提交了訂單,那么系統(tǒng)就需要對所購買商品的庫存進(jìn)行鎖定,如果出現(xiàn)用戶未支付訂單超時(shí)等問題,就需要將已經(jīng)鎖定的庫存進(jìn)行數(shù)據(jù)回滾了,可是訂單和庫存在不同的數(shù)據(jù),要如何保證事務(wù)的原子性呢?如果都在本地部署,可以使用AOP對事務(wù)進(jìn)行代理,在不同機(jī)器部署的情況下也可以通過設(shè)置undo_log表并通過阿里的Seata進(jìn)行代理。但是在高并發(fā)情況下,這些方式容易造成"雪崩",這個(gè)時(shí)候還可以考慮消息隊(duì)列,通過延遲隊(duì)列來完成庫存的解鎖。