難道沒(méi)有完美的存儲(chǔ)引擎?
存儲(chǔ)引擎是數(shù)據(jù)庫(kù)的核心組件,存儲(chǔ)引擎的一些特性決定了數(shù)據(jù)庫(kù)的一些基本性能特性。存儲(chǔ)引擎的選擇也是數(shù)據(jù)庫(kù)開(kāi)發(fā)人員十分謹(jǐn)慎的一個(gè)方面。以前我也在一些文章里介紹了一些常見(jiàn)的存儲(chǔ)引擎,也對(duì)這些存儲(chǔ)引擎的優(yōu)缺點(diǎn)做了概括。不過(guò)如果沒(méi)有真實(shí)的去使用,去匹配應(yīng)用負(fù)載,僅僅是紙上談兵,是完全不夠的。
目前數(shù)據(jù)庫(kù)中使用最為廣泛的存儲(chǔ)引擎不外乎兩個(gè)系列三種引擎,分布式數(shù)據(jù)庫(kù)喜歡采用的LSM-TREE存儲(chǔ),以及普通通用型數(shù)據(jù)庫(kù)常用的BTREE/HEAP存儲(chǔ)。BTREE和HEAP存儲(chǔ)是兩種十分相近的存儲(chǔ)引擎結(jié)構(gòu),Oracle,PostgreSQL,MySQL等數(shù)據(jù)庫(kù)都使用BTREE/HEAP結(jié)構(gòu)。BTREE是一個(gè)泛稱(chēng),實(shí)際上數(shù)據(jù)結(jié)構(gòu)是一棵增強(qiáng)的B+TREE。
這是一張來(lái)自網(wǎng)上的兩大陣營(yíng)的一些數(shù)據(jù)庫(kù)產(chǎn)品的清單,我們可以看出傳統(tǒng)數(shù)據(jù)庫(kù)使用B-TREE結(jié)構(gòu)的比較多,而一些新興的數(shù)據(jù)庫(kù)系統(tǒng)往往使用LSM-TREE。LSM-TREE是Log-Structured Merge Tree的簡(jiǎn)稱(chēng),是一種典型的不可變結(jié)構(gòu)的存儲(chǔ)引擎,其目的是為了降低可變結(jié)構(gòu)存儲(chǔ)引擎中寫(xiě)入數(shù)據(jù)需要先找到已經(jīng)存在的PAGE,然后再進(jìn)行修改的成本,從而實(shí)現(xiàn)更高并發(fā)的寫(xiě)入。因此LSM-TREE引擎的數(shù)據(jù)庫(kù)天生對(duì)高并發(fā)寫(xiě)入/修改操作十分友好。不過(guò)LSM-TREE結(jié)構(gòu)也不是完美的,在讀性能上需要在多個(gè)副本之間做協(xié)同,因此讀性能會(huì)受到一定的影響。
HEAP/BTREE結(jié)構(gòu)的存儲(chǔ)引擎雖然從結(jié)構(gòu)上來(lái)說(shuō)比較接近,不過(guò)依然存在一定的差異。在我們常見(jiàn)的數(shù)據(jù)庫(kù)中,Oracle、PostgreSQL等是采用HEAP結(jié)構(gòu)的,這種結(jié)構(gòu)的頁(yè)和不同的BTREE頁(yè)不同,采用slotted page。Mysql、達(dá)夢(mèng)等數(shù)據(jù)庫(kù)使用的是傳統(tǒng)的BTREE存儲(chǔ)結(jié)構(gòu)。
實(shí)際上這么說(shuō)也不是很準(zhǔn)確,只能說(shuō)這些數(shù)據(jù)庫(kù)的默認(rèn)存儲(chǔ)引擎是使用這種數(shù)據(jù)組織方式的。實(shí)際上Oracle中有heap表,有簇表,有混合列壓縮的表。其中簇表是BREE結(jié)構(gòu)的。達(dá)夢(mèng)的存儲(chǔ)引擎有多種數(shù)據(jù)組織方式:B樹(shù)數(shù)據(jù)、堆表數(shù)據(jù)、列存數(shù)據(jù)、位圖索引,其中B樹(shù)數(shù)據(jù)是普通的達(dá)夢(mèng)表的默認(rèn)組織方式。
Oracle的數(shù)據(jù)塊結(jié)構(gòu)是一種典型的SLOTTED PAGE的結(jié)構(gòu),塊頭從上往下增長(zhǎng),而數(shù)據(jù)從尾部向塊頭生長(zhǎng)。中間是一個(gè)可變長(zhǎng)的slot指示器。
如上圖所示,通過(guò)一個(gè)定長(zhǎng)數(shù)組指示器結(jié)構(gòu),指出每一行行頭的位置,這主要是為了解決不定長(zhǎng)記錄的存儲(chǔ)問(wèn)題,從而使空間利用率達(dá)到最高。
Oracle數(shù)據(jù)塊使用一個(gè)kdbt的結(jié)構(gòu)來(lái)指出某個(gè)block中有多少條記錄,并且kdbpri這個(gè)指示器與塊頭的偏移量。kdbpri就是這個(gè)slot數(shù)組。
上面是MySQL innodb存儲(chǔ)引擎的一個(gè)邏輯示意圖,這是一種典型的BTREE結(jié)構(gòu)存儲(chǔ)引擎。BTREE結(jié)構(gòu)的存儲(chǔ)引擎也不是完全相同的。主要的區(qū)別是leaf node segment是否和數(shù)據(jù)存儲(chǔ)在一個(gè)段里。達(dá)夢(mèng)明顯是分開(kāi)的。實(shí)際上BTREE結(jié)構(gòu)的存儲(chǔ)結(jié)構(gòu),所有的數(shù)據(jù)存儲(chǔ)都是按照主鍵的順序存儲(chǔ)的。
表空間是一個(gè)邏輯結(jié)構(gòu),可以被認(rèn)為是innodb的頂層邏輯結(jié)構(gòu),所有的數(shù)據(jù)都必須屬于某個(gè)表空間。默認(rèn)的innodb引擎有一個(gè)ibdata1表空間,默認(rèn)的數(shù)據(jù)都存儲(chǔ)在這個(gè)表空格鍵中。如果設(shè)置了innodb_file_per_table參數(shù),每張表都會(huì)創(chuàng)建獨(dú)立的文件。不過(guò)只有表的數(shù)據(jù)、索引等會(huì)存儲(chǔ)在每張表自己的文件中,UNDO數(shù)據(jù)、事務(wù)控制信息、INSERT BUFFER等仍然會(huì)存儲(chǔ)在系統(tǒng)共享的表空間中。在Innodb存儲(chǔ)引擎中,一張表會(huì)分為兩個(gè)段,其中一個(gè)段是葉節(jié)點(diǎn)段,存儲(chǔ)實(shí)際的數(shù)據(jù),另外一個(gè)段是索引段,存儲(chǔ)索引的指針信息。而表中DML的UNDO信息會(huì)存儲(chǔ)在rollback segment中。
上面這張innodb的邏輯結(jié)構(gòu)圖畫(huà)的十分清晰,所有的表的行數(shù)據(jù)是存儲(chǔ)在extent中的,而每個(gè)extent是多個(gè)連續(xù)的PAGE組成的,每個(gè)PAGE中存儲(chǔ)了行數(shù)據(jù)。實(shí)際上這個(gè)leaf node segment和Oracle的TABLE SEGMENT是十分類(lèi)似的,所不同的是多了一個(gè)Index segments。相當(dāng)于在創(chuàng)建表的時(shí)候同時(shí)又默認(rèn)創(chuàng)建了一個(gè)主鍵索引。Mysql在窗這個(gè)主鍵索引的時(shí)候,會(huì)區(qū)分不同的情況。如果要?jiǎng)?chuàng)建的表上沒(méi)有設(shè)置主鍵索引,那么會(huì)選擇表上的一個(gè)非空唯一性索引作為主鍵索引,如果不存在這樣的索引,那么Mysql會(huì)使用一個(gè)六字節(jié)的唯一性自增值窗一個(gè)主鍵索引。
Mysql innodb等采用B樹(shù)存儲(chǔ)結(jié)構(gòu)的存儲(chǔ)引擎一般采用上圖的模式,當(dāng)數(shù)據(jù)被插入表的時(shí)候,會(huì)根據(jù)主鍵索引或者簇索引的指示插入到某個(gè)位置,而不會(huì)像Oracle那樣,通過(guò)segment的free space bitmap尋找空閑位置插入。
Innodb的PAGE結(jié)構(gòu)與HEAP結(jié)構(gòu)的類(lèi)似,不過(guò)在空閑空間管理上是完全不同的。前面是FILE HEADER/PAGE HEADER,中間是數(shù)據(jù)記錄,數(shù)據(jù)記錄也是從低地址往高地址寫(xiě),和Oracle相反。這是因?yàn)锽TREE存儲(chǔ)結(jié)構(gòu)不需要和slotted page一樣,在塊里放一個(gè)指示器,其行指示器的功能被BTREE替代了。
Innodb的這種存儲(chǔ)結(jié)構(gòu),并不存在一個(gè)十分友好的類(lèi)似Oracle的記錄物理地址的ROWID這樣的結(jié)構(gòu)。所以要想定位某條數(shù)據(jù)記錄,需要使用主鍵或者簇主鍵的方式來(lái)實(shí)現(xiàn)。主鍵可以定義某條記錄的唯一性地址,因此Mysql的某張表上的其他索引(secondary index)的索引中存儲(chǔ)的鍵值不像Oracle那樣存儲(chǔ)ROWID就可以了,而是存儲(chǔ)的是主鍵中這一行的地址指針?;谝粋€(gè)secondary index的查詢(xún)首先找出某些行的主鍵,然后再去掃描一次主鍵索引,才能找到相關(guān)行的地址,再找到這條記錄。比起有rowid的Oracle數(shù)據(jù)庫(kù),這里多了一次主鍵索引的掃描。
可能有些朋友會(huì)覺(jué)得,是不是heap結(jié)構(gòu)一定優(yōu)于BTREE結(jié)構(gòu)呢?其實(shí)還是回到今天的標(biāo)題,沒(méi)有完美的存儲(chǔ)引擎。針對(duì)不同的應(yīng)用場(chǎng)景,heap和BTREE各有優(yōu)勢(shì)。BTREE結(jié)構(gòu)寫(xiě)入數(shù)據(jù)時(shí)按主鍵排序的,而且并發(fā)寫(xiě)入時(shí)數(shù)據(jù)并不是按照插入順序?qū)懭霐?shù)據(jù)塊,如果主鍵存在一定的無(wú)序性,那么并發(fā)寫(xiě)入的數(shù)據(jù)可以被打散到多個(gè)塊中,從而緩解熱塊沖突的壓力。而二級(jí)索引的結(jié)構(gòu)雖然對(duì)讀取數(shù)據(jù)的操作有影響,對(duì)于存在多條索引的數(shù)據(jù)寫(xiě)入,數(shù)據(jù)修改,是有優(yōu)勢(shì)的。因?yàn)橹灰麈I的鍵值不變,行數(shù)據(jù)的變化,行在數(shù)據(jù)塊中存儲(chǔ)的變化,不需要變更第二索引。
因此我們可以十分明確的肯定,不同的存儲(chǔ)結(jié)構(gòu)都各有利弊,并不能很直接的說(shuō)哪種更好。不過(guò)在開(kāi)發(fā)高并發(fā),大數(shù)據(jù)量的系統(tǒng)的時(shí)候,了解存儲(chǔ)引擎的一些特點(diǎn),可以有效的避免一些問(wèn)題。比如在Mysql、達(dá)夢(mèng)等數(shù)據(jù)庫(kù)中建表,盡可能定義一個(gè)顯式的主鍵,從而避免系統(tǒng)自動(dòng)添加主鍵。另外如果某張表的熱塊沖突特別嚴(yán)重的時(shí)候,主鍵可以考慮選擇隨機(jī)性的數(shù)據(jù),而不是單邊增長(zhǎng)的數(shù)據(jù),就可以有效的進(jìn)行數(shù)據(jù)打散,從而降低熱塊沖突的可能性。