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