1. 程式人生 > >轉:MySQL InnoDB Add Index實現調研

轉:MySQL InnoDB Add Index實現調研

MySQL InnoDB Add Index實現調研

MySQL Add Index實現

MySQL各版本,對於add Index的處理方式是不同的,主要有三種:

Copy Table方式

這是InnoDB最早支援的建立索引的方式。顧名思義,建立索引是通過臨時表拷貝的方式實現的。

新建一個帶有新索引的臨時表,將原表資料全部拷貝到臨時表,然後Rename,完成建立索引的操作。

這個方式建立索引,建立過程中,原表是可讀的。但是會消耗一倍的儲存空間

Inplace方式

這是原生MySQL 5.5,以及innodb_plugin中提供的建立索引的方式。所謂Inplace,也就是索引建立在原表上直接進行,不會拷貝臨時表。相對於

Copy Table方式,這是一個進步。

Inplace方式建立索引,建立過程中,原表同樣可讀的,但是不可寫

Online方式

這是MySQL 5.6.7中提供的建立索引的方式。無論是Copy Table方式,還是Inplace方式,建立索引的過程中,原表只能允許讀取,不可寫。對應用有較大的限制,因此MySQL最新版本中,InnoDB支援了所謂的Online方式建立索引。

InnoDBOnline Add Index,首先是Inplace方式建立索引,無需使用臨時表。在遍歷聚簇索引,收集記錄並插入到新索引的過程中,原表記錄可修改。而修改的記錄儲存在Row Log中。當聚簇索引遍歷完畢,並全部插入到新索引之後,

重放Row Log中的記錄修改,使得新索引與聚簇索引記錄達到一致狀態。

Copy Table方式相比,Online Add Index採用的是Inplace方式,無需Copy Table,減少了空間開銷;與此同時,Online Add Index只有在重放Row Log最後一個Block時鎖表,減少了鎖表的時間。

Inplace方式相比,Online Add Index吸收了Inplace方式的優勢,卻減少了鎖表的時間。

 

 

Inplace add Index

本章節,主要通過測試/原始碼跟蹤的方式,調研InnoDB Inplace Add Index的實現方式。以及分析

Inplace add Index有哪些需要注意的地方。

 

測試準備5.5

測試版本

MySQL 5.5.25

測試表

    create table t1 (a int primary key, b int)engine=innodb;

    insert into t1 values (1,1),(2,2),(3,3),(4,4);

Inplace Add Index處理流程

SQL

    alter table t1 add index idx_t1_b(b);

處理流程

 sql_table.cc::mysql_alter_table();

        // 判斷當前操作是否可以進行Inplace實現,不可進行Inplace Alter的包括:

        // 1. Auto Increment欄位修改;

        // 2. 列重新命名;

        // 3. 行儲存格式修改;等

        mysql_compare_tables() -> ha_innobase::check_if_incompatible_data();

        // Inplace建立索引第一階段(主要階段)

        handler0alter.cc::add_index();

            …

            // 建立索引資料字典

            row0merge.c::row_merge_create_index();

                index = dict_mem_index_create();

                // 每個索引資料字典上,有一個trx_id,記錄建立此索引的事務

                // trx_id有何功能,接著往下看

                index->trx_id = trx_id;

                 // 讀取聚簇索引,構造新索引的項,排序並插入新索引

                 row0merge.c::row_merge_build_indexes();

                        // 讀取聚簇索引,注意:只讀取其中的非刪除項

                        // 跳過所有刪除項,為什麼可以這麼做?往下看

                        row_merge_read_clustered_index();

                        // 檔案排序

                        row_merge_sort();

                        // 順序讀取排序檔案中的索引項,逐個插入新建索引中

                        row_merge_insert_index_tuples();

        // 等待開啟當前表的所有隻讀事務提交

        sql_base.cc::wait_while_table_is_used();

        // 建立索引結束,做最後的清理工作

        handler0alter.cc::final_add_index();

        // Inplace add Index完畢

 

 

 

Inplace Add Index實現分析

在索引建立完成之後,MySQL Server立即可以使用新建的索引,做查詢。但是,根據以上流程,對我個人來說,有三個疑問點:

索引資料字典上,為何需要維護一個trx_id

trx_id有何作用?

遍歷聚簇索引讀取所有記錄時,為何可跳過刪除項?

只讀取非刪除項,那麼新建索引上沒有版本資訊,無法處理原有事務的快照讀;

MySQL Server層,為何需要等待開啟表的只讀事務提交?

等待當前表上的只讀事務,可以保證這些事務不會使用到新建索引

根據分析,等待開啟表的只讀事務結束較好理解。因為新索引上沒有版本資訊,若這些事務使用新的索引,將會讀不到正確的版本記錄。

Online add Index

本章節,主要通過測試/原始碼跟蹤的方式,調研InnoDB Online Add Index的實現方式(MySQL 5.6.7-RC版本提供的新功能)。分析Online add Index有哪些需要注意的地方,最後展示一個InnoDB Online Add Index存在的Bug

 

測試準備5.6

測試版本

MySQL 5.6.7-RC

測試表

    create table t1 (a int primary key, b int)engine=innodb;

    insert into t1 values (1,1),(2,2),(3,3),(4,4);

Online Add Index處理流程

SQL

    alter table t1 add index idx_t1_b(b);

處理流程

    sql_table.cc::mysql_alter_table();

        // 1. 判斷當前DDL操作是否可以Inplace進行

        check_if_supported_inplace_alter();

            …

        // 2. 開始進行Online建立的前期準備工作

        prepare_inplace_alter_table();

            …

            // 修改表的資料字典資訊

            prepare_inplace_alter_table_dict();

                …

                // 等待InnoDB所有的後臺執行緒,停止操作此表

                dict_stats_wait_bg_to_stop_using_tables();

                …

                // Online Add Index區別與Inplace Add Index的關鍵

                // Online操作時,原表同時可以讀寫,因此需要

                // 將此過程中的修改操作記錄到row log之中

                row0log.cc::row_log_allocate();

                    row_log_t* log = (row_log_t*)&buf[2 * srv_sort_buf_size];

                    // 標識當前索引狀態為Online建立,那麼此索引上的

                    // DML操作會被寫入Row Log,而不在索引上進行更新

                    dict_index_set_online_status(index, ONLINE_INDEX_CREATION);

            …

        // 3. 開始進行真正的Online Add Index的操作(最重要的流程)

        inplace_alter_table();

            // 此函式的操作,前部分與Inplace Add Index基本一致

            // 讀取聚簇索引、排序、並插入到新建索引中

            // 最大的不同在於,當插入完成之後,Online Add Index

            // 還需要將row log中的記錄變化,更新到新建索引中

            row0merge.cc::row_merge_build_index();

                …

                // 在聚簇索引讀取、排序、插入新建索引的操作結束之後

                // 進入OnlineInplace真正的不同之處,也是Online操作

                // 的精髓部分——將這個過程中產生的Row Log重用

                row0log.cc::row_log_apply();

                    // 暫時將新建索引整個索引樹完全鎖住

                    // 注意:只是暫時性鎖住,並不是在整個重用Row Log

                    // 過程中一直加鎖(防止加鎖時間過長的優化,如何優化?)

                    rw_lock_x_lock(dict_index_get_lock(new_index));

                        …

                    // InnoDB Online操作最重要的處理流程

                    // Online Copy Table中,記錄的Row Log重放到新建索引上

                    // 重放Row Log的演算法如下

                    // 1. Row Log中記錄的是Online建立索引期間,原表上的DML操作

                    //    這些操作包括:ROW_OP_INSERTROW_OP_DELETE_MARK; …

 

                    // 2. Row LogBlock的方式儲存,若DML較多,那麼Row Logs可能

                    //     會佔用多個Blocksrow_log_t結構中包含兩個指標:headtail

                    //     head指標用於讀取Row Logtail指標用於追加寫新的Row Log

 

                    // 3.在重用Row Log時,演算法遵循一個原則:儘量減少索引樹加鎖

                    //    的時間(索引樹加X鎖,也意味著表上禁止了新的DML操作)

 

                    //     索引樹需要加鎖的場景

                    //    () 在重用Row Log跨越新的Block時,需要短暫加鎖;

 

                    //     () 若應用的Row Log Block是最後一個Block,那麼一直加鎖

                    //         應用最後一個Block,由於禁止了新的DML操作,因此此

                    //         Block應用完畢,新索引記錄與聚簇索引達到一致狀態,

                    //         重用階段結束;

 

                    //    () 在應用中間Row Log Block上的row log時,無需加鎖,新的

                    //         DML操作仍舊可以進行,產生的row log記錄到最後一個

                    //         Row Log Block之上;

 

                    // 4. 如果是建立Unique索引,那麼在應用Row Log時,可能會出現

                    //     違反唯一性約束的情況,這些情況會被記錄到

                    //     row_merge_dup_t結構之中

                    row_log_apply_ops(trx, index, &dup);

                        row_log_apply_op();

                            row_log_apply_op_low();

                                …

                    // New IndexOnline row log設定為NULL

                    // 標識New Index的資料已經與聚簇索引完全一致

                    // 在此之後,新的DML操作,無需記錄Row Log

                    dict_index_set_online_status();

                        index->online_status = ONLINE_INDEX_COMPLETE;

                    index->online_log = NULL;

                    rw_lock_x_unlock(dict_index_get_block(new_index));

                    row_log_free();

            …

        // 4. Online Add Index的最後步驟,做一些後續收尾工作

        commit_inplace_alter_table();

            …

Online Add Index實現分析

在看完前面分析的InnoDB 5.6.7-RC版本中實現的基本處理流程之後,個人仍舊遺留了幾個問題,主要的問題有:

 

Online Add Index是否支援Unique索引

 

確切的答案是:支援(不過存在Bug,後面分析)InnoDB支援Online建立Unique索引。

 

既然支援,就會面臨Check Duplicate Key的問題。Row Log中如果存在與索引中相同的鍵值怎麼處理?怎麼檢測是否存在相同鍵值?

 

InnoDB解決此問題的方案也比較簡介易懂。其維護了一個row_merge_dup_t的資料結構,儲存了在Row log重放過程中遇到的違反唯一性衝突的Row Log。應用完Row Log之後,

外部判斷是否存在Unique衝突(有多少Unique衝突,均會記錄)Online建立Unique索引失敗。

 

Row Log是什麼樣的結構,如何組織的

 

Online Add Index過程中,併發DML產生的修改,被記錄在Row Log中。首先,Row Log不是InnoDBRedo Log,而是每個正在被Online建立的索引的獨佔結構。

 

 

Online建立索引,遵循的是先建立索引資料字典,後填充資料的方式。因此,當索引資料字典建立成功之後,新的DML操作就可以讀取此索引,嘗試進行更新。但是,由於索引結構上

status狀態為ONLINE_INDEX_CREATION,因此這些更新不能直接應用到新索引上,而是放入Row Log之中,等待被重放到索引之上。

 

 

Row Log中,以Block的方式管理DML操作內容的存放。一個Block的大小為由引數innodb_sort_buffer_size控制,預設大小為1M (1048576)。初始化階段,Row Log申請兩個這樣的Block

 

 

Row Log重放的過程中,到底需要多久的鎖表時間

 

前面的流程分析中,也提到了鎖表的問題(內部為鎖新建索引樹的操作實現)

 

在重放Row log時,有兩個情況下,需要鎖表:

 

情況一:在使用完一個Block,跳轉到下一個Block時,需要短暫鎖表,判斷下一個Block是否為Row Log的最後一個Block。若不是最後一個,跳轉完畢後,釋放鎖;使用Block內的row log不加鎖,使用者DML操作仍舊可以進行。

 

情況二:在使用最後一個Block時,會一直持有鎖。此時不允許新的DML操作。保證最後一個Block重放完成之後,新索引與聚簇索引記錄達到一致狀態。

 

Online Add Index是否存在Bug

答案同樣是肯定的,存在Bug

其中有一個Bug,重現方案如下:

create table t1 (a int primary key, b int, c char(250))engine=innodb;

insert into t1(b,c) values (1,’aaaaaaa’);

// 保證資料量夠多

insert into t1(b,c) select b,c from t1;

insert into t1(b,c) select b,c from t1;

insert into t1(b,c) select b,c from t1;

// max(a) = 196591

select max(a) from t1;

// b中同樣沒有相同項

update t1 set b = a;

session 1                                                                     session 2

alter table t1 add unique index idx_t1_b(b);

                                                                                     insert into t1(b,c) values (196592,’b’);

                                                                                     // update,會產生b=196589的重複項

                                                                                     update t1 set b=196589 where a=196582;

                                                                                     delete from t1 where a = 262127;

在以上的測試中,首先為表準備足夠的資料,目的是session 1Online Add Index的讀取聚簇索引階段,session 2新的記錄也能夠被讀到。

 

session 1Online Add Index完成之後(成功),執行以下兩個命令,結果如下

 

mysql> show create table t1;

 

+——-+————————————————–

 

| Table | Create Table

 

+——-+————————————————–

 

| t1 | CREATE TABLE `t1` (

 

`a` int(11) NOT NULL AUTO_INCREMENT,

 

`b` int(11) DEFAULT NULL,

 

`c` char(250) DEFAULT NULL,

 

PRIMARY KEY (`a`),

 

UNIQUE KEY `idx_t1_b` (`b`)

 

) ENGINE=InnoDB AUTO_INCREMENT=262129 DEFAULT CHARSET=gbk |

 

+——-+————————————————–

 

mysql> select * from t1 where a in (196582,196589);

 

+——–+——–+———+

 

| a | b | c |

 

+——–+——–+———+

 

| 196582 | 196589
| aaaaaaa |

 

| 196589 | 196589
| aaaaaaa |

 

+——–+——–+———+

 

2 rows in set (0.04 sec)

 

 

 

可以看到,b上已經有了一個Unique索引,但是表中卻存在兩個相同的取值為196589的值。

 

Bug,是處理Row Log的重放過程,未詳盡考慮所有情況導致的。因此,在MySQL 5.6版本穩定之前,慎用!

 

 

 

 

 

 

 

 

--此文章轉載自登博的部落格,給大家分享。