深度解析Innodb記錄格式源碼
可以通過(guò)一個(gè)最普遍的插入操作來(lái)跟蹤Innodb的記錄格式,因?yàn)樵诓迦霑r(shí),系統(tǒng)得到的是公共的mysql記錄格式record,現(xiàn)在它沒(méi)有涉及到任何的存儲(chǔ)引擎,那么這里不管當(dāng)前這個(gè)表對(duì)應(yīng)的存儲(chǔ)引擎是什么,記錄格式是一樣的,對(duì)于插入,mysql函數(shù)對(duì)應(yīng)的是ha_write_row,具體到Innodb存儲(chǔ)引擎,實(shí)際調(diào)用的函數(shù)是ha_innobase::write_row函數(shù),那么在這里,Innodb首先會(huì)將接收到的record記錄轉(zhuǎn)換為它自己的一個(gè)元組tuple,這其實(shí)是與record對(duì)應(yīng)的innodb的表示方式,它是一個(gè)內(nèi)存的記錄,邏輯的記錄,那么在系統(tǒng)將其真正的寫入到頁(yè)面之前,這條記錄的存在方式都是這個(gè)tuple,那么下面主要是從源碼的角度研究Innodb是如何將一個(gè)tuple轉(zhuǎn)換為它的物理的存儲(chǔ)記錄的,主要研究代碼的實(shí)現(xiàn)邏輯及記錄的格式。
這里只介紹格式為Compact類型的記錄格式。
實(shí)現(xiàn)在某一個(gè)頁(yè)面插入一個(gè)元組(一條記錄)操作的函數(shù)是page_cur_tuple_insert,它的參數(shù)就是一個(gè)dtuple_t*類型的tuple,在這里,它首先要分配一片空間來(lái)存儲(chǔ)將要轉(zhuǎn)換過(guò)來(lái)的物理記錄,所以這里需要先計(jì)算空間的大小,計(jì)算方法如下:
1. 首先每條記錄都要包括下面2個(gè)部分:REC_N_NEW_EXTRA_BYTES + UT_BITS_IN_BYTES(n_null),前面表示的是這種格式的固定長(zhǎng)度的extra部分,這部分用來(lái)存儲(chǔ)什么內(nèi)容后面會(huì)給出,后面表示的是所有字段中哪些字段的值是null,當(dāng)然這里只存儲(chǔ)那些nullable屬于的字段,如果創(chuàng)建表的時(shí)候指定是not null的話,這里就不會(huì)被存儲(chǔ),那么這里是用一個(gè)位來(lái)表示一個(gè)字段的null屬性。那么上面這部分被系統(tǒng)代碼命名為extra_size變量值。
2. 統(tǒng)計(jì)每一個(gè)列中數(shù)據(jù)的長(zhǎng)度,在統(tǒng)計(jì)這個(gè)信息的時(shí)候,又有多種情況,主要分定長(zhǎng)字段和變長(zhǎng)字段,對(duì)于定長(zhǎng)字段,它的長(zhǎng)度直接就是數(shù)據(jù)類型的長(zhǎng)度,比如int類型的那就是4個(gè)字節(jié),rowid列就是6個(gè)字節(jié)等,沒(méi)有其它附加長(zhǎng)度。對(duì)于變長(zhǎng)字段而言,除了數(shù)據(jù)內(nèi)容本身的長(zhǎng)度外,還需要計(jì)算其數(shù)據(jù)長(zhǎng)度的存儲(chǔ)空間,如果字段的字義長(zhǎng)度大于255個(gè)字節(jié),或者字段的數(shù)據(jù)類型為BLOB的,那么需要用2個(gè)字節(jié)來(lái)存儲(chǔ)這個(gè)字段的長(zhǎng)度;如果定義長(zhǎng)度小于128個(gè)字節(jié),或者小于256個(gè)字節(jié),但類型不是BLOB類型的,那么這個(gè)字段的數(shù)據(jù)長(zhǎng)度用一個(gè)字節(jié)來(lái)存儲(chǔ),除上面2種情況之外,都用2個(gè)字節(jié)來(lái)存儲(chǔ)。那么在這一部分中,用來(lái)存儲(chǔ)變長(zhǎng)字段數(shù)據(jù)的長(zhǎng)度的空間的長(zhǎng)度也是被Innodb計(jì)算為extra_size的。
所以現(xiàn)在可以知道,一個(gè)innodb的記錄包括2個(gè)部分,一部分是extra_size,另一部分是數(shù)據(jù)內(nèi)容,那么這2部分的總長(zhǎng)度就是上面計(jì)算出來(lái)的結(jié)果,這里把它定義為record_size。
接下來(lái),申請(qǐng)空間,進(jìn)行元組到記錄的轉(zhuǎn)換工作。
轉(zhuǎn)換函數(shù)為rec_convert_dtuple_to_rec_new,參數(shù)有申請(qǐng)好的記錄空間buf,元組和索引的內(nèi)存結(jié)構(gòu)。
首先這里有一個(gè)操作是rec = buf + extra_size,變量rec表示的是數(shù)據(jù)內(nèi)容的存儲(chǔ)開(kāi)始位置。extra_size就是上面計(jì)算出來(lái)的2個(gè)數(shù)據(jù)部分。
那么真正執(zhí)行轉(zhuǎn)換的是接下來(lái)調(diào)用的rec_convert_dtuple_to_rec_comp函數(shù),下面是其原型:
- void
- rec_convert_dtuple_to_rec_comp(
- /*===========================*/
- rec_t* rec, /*!< in: origin of record */
- ulint extra, /*!< in: number of bytes to
- reserve between the record
- header and the data payload
- (normally REC_N_NEW_EXTRA_BYTES) */
- const dict_index_t* index, /*!< in: record descriptor */
- ulint status, /*!< in: status bits of the record */
- const dfield_t* fields, /*!< in: array of data fields */
- ulint n_fields)/*!< in: number of data fields */
rec表示的是剛才上面計(jì)算出來(lái)的rec變量,extra表示的是固定長(zhǎng)度的REC_N_NEW_EXTRA_BYTES。
- end = rec;
- nulls = rec - (extra + 1);
- n_null = index->n_nullable;
- lens = nulls - UT_BITS_IN_BYTES(n_null);
- /* clear the SQL-null flags */
- memset(lens + 1, 0, nulls - lens);
在這里,這段代碼一下子很難看明白,那么首先這里畫一下記錄存儲(chǔ)格式:
|---------------------extra_size-----------------------------------------|---------fields_data------------|
|--columns_lens---|---null lens----|------fixed_extrasize(5)------|--col1---|---col2---|---col2----|
那么語(yǔ)句nulls = rec - (extra + 1);得到的結(jié)果是什么呢?想干什么?因?yàn)閑xtra表示的是REC_N_NEW_EXTRA_BYTES,固定長(zhǎng)度的fixed_extrasize,rec表示的是圖中col1的開(kāi)始位置,那么現(xiàn)在可以知道這條語(yǔ)句的結(jié)果就是使得nulls指向了前面nulllens的后一個(gè)字節(jié)的開(kāi)始位置。那現(xiàn)在我們知道nulls是一個(gè)或者多個(gè)字節(jié),用來(lái)存儲(chǔ)每一個(gè)nullable字段的空標(biāo)志的,那現(xiàn)在為什么要指向這個(gè)數(shù)組的后一個(gè)字節(jié)的開(kāi)始位置呢?一下子很難想明白,不過(guò)從后面的代碼中可以知道,寫入nulls是從后面向前面寫的,所以這也理解了為什么指向了后面一個(gè)字節(jié)的位置了。
那接下來(lái)的一個(gè)語(yǔ)句lens = nulls - UT_BITS_IN_BYTES(n_null);道理也是一樣的,因?yàn)閏olumns_lens正好是在nulllens的前面,那么如果向前跳過(guò)null標(biāo)志的所有空間,則指向的位置lens就是columns_lens的后面一個(gè)字節(jié)的位置了。在寫入值的時(shí)候也是從后面向前面寫。
那最后一個(gè)語(yǔ)句memset(lens + 1, 0, nulls - lens);表示的意思就很明白了,因?yàn)閘ens指向的是columns_lens的最后一個(gè)字節(jié)的開(kāi)始位置,那么加1就指向了nulls空間的開(kāi)始位置,nulls – lens表示的是nulls空間的長(zhǎng)度。這里是將nulls空間清零。
上面有兩個(gè)部分都是從后面向前面填寫數(shù)據(jù),那是不是擔(dān)心在寫入的時(shí)候會(huì)不會(huì)向前面越界呢?其實(shí)是不會(huì)的,因?yàn)檫@些都是在前面計(jì)算好的,extrasize已經(jīng)是固定的,包括了nulls和columns_lens的長(zhǎng)度的。
上面算是初始化工作,下面就是根據(jù)每一個(gè)字段來(lái)填寫record記錄了,下面一段代碼是處理null信息的,對(duì)于每一個(gè)字段,都會(huì)做下面的處理:
- if (!(dtype_get_prtype(type) & DATA_NOT_NULL)) {
- /* nullable field */
- ut_ad(n_null--);
- if (UNIV_UNLIKELY(!(byte) null_mask)) {
- nulls--;
- null_mask = 1;
- }
- ut_ad(*nulls < null_mask);
- if (dfield_is_null(field)) {
- *nulls |= null_mask;
- null_mask <<= 1;
- continue;
- }
- null_mask <<= 1;
- }
從第一行可以看出,要處理這個(gè)的條件首先必須是沒(méi)有定義not null屬性,所以nulls空間只存儲(chǔ)這些字段的信息。
第4行表示的是如果(byte) null_mask)為0時(shí),nulls向前退一個(gè)字節(jié),并且將null_mask恢復(fù)為1的初值,因?yàn)檫@個(gè)值初始值就是1的,可以猜到,如果這個(gè)條件滿足了,則說(shuō)明已經(jīng)寫入了8個(gè)nullable列了,那么需要移向前一個(gè)字節(jié)繼續(xù)寫null信息了,但發(fā)現(xiàn)null_mask是int類型的,而nulls是一個(gè)字節(jié)一個(gè)字節(jié)的填的,不匹配啊,不過(guò)仔細(xì)看,判斷條件是(byte) null_mask),所以只要寫入8個(gè)之后,這個(gè)值就為0了。因?yàn)閷?duì)于每一個(gè)字段,都是執(zhí)行null_mask向左移1個(gè)位的,所以移8次之后,低8位就都是0了。
第9行表示的是如果這個(gè)列的數(shù)據(jù)就是null值,那么需要將這個(gè)null反映到nulls數(shù)組中去,因?yàn)閚ull_mask當(dāng)前的值(其實(shí)是1的位置)其實(shí)表示的是當(dāng)前nulls這個(gè)字節(jié)中正在處理的字段的對(duì)應(yīng)關(guān)系,也就是說(shuō),如果當(dāng)前的字段的值為null,那么像第10行所示的,將null_mask或到nulls字節(jié)上去,如果不為null,就不管,對(duì)應(yīng)的位的值為0。
所以從這里可以看出,整個(gè)nulls空間中的位圖是以從后面向前面的順序來(lái)表示所有nullable列的null信息的。
- if (fixed_len) {
- } else if (dfield_is_ext(field)) {
- *lens-- = (byte) (len >> 8) | 0xc0;
- *lens-- = (byte) len;
- } else {
- if (len < 128 || (dtype_get_len(type) < 256 && dtype_get_mtype(type) != DATA_BLOB)) {
- *lens-- = (byte) len;
- } else {
- *lens-- = (byte) (len >> 8) | 0x80;
- *lens-- = (byte) len;
- }
- }
- memcpy(end, dfield_get_data(field), len);
- end += len;
從第一行可以看出,對(duì)于定長(zhǎng)數(shù)據(jù),只需要將其數(shù)據(jù)寫入到記錄里面即可,主要處理的是變長(zhǎng)數(shù)據(jù),第2行表示的是如果長(zhǎng)度大于256個(gè)字節(jié),或者數(shù)據(jù)類型為BLOB,則用兩個(gè)字節(jié)來(lái)存儲(chǔ)其長(zhǎng)度,低字節(jié)存儲(chǔ)(len >> 8) | 0xc0,高字節(jié)存儲(chǔ)(byte) len(被截?cái)啵F渌梢灾苯涌闯鰜?lái)。
到13行,是直接將數(shù)據(jù)拷到數(shù)據(jù)存儲(chǔ)空間,用end來(lái)表示,存儲(chǔ)完一個(gè)字段接著下一個(gè)字段,是按照索引定義的順序存儲(chǔ)的。
到這里,一條記錄的邏輯到物理的轉(zhuǎn)換就完成了,從中也知道了Innodb是如何實(shí)現(xiàn)其物理記錄的存儲(chǔ)的。
總結(jié):看innodb的代碼,可以說(shuō)它的代碼非常優(yōu)美,非常精練的,所以有些地方很難一下子看懂,需要揣測(cè),體會(huì)才能深入的理解。同時(shí)有很多地方是直接硬編碼的,這樣導(dǎo)致更加難理解,最好的方式是通過(guò)宏將其命名,有助于理解。
原文鏈接:http://www.cnblogs.com/bamboos/archive/2013/03/04/2943160.html
【編輯推薦】