DDL 是數據庫很是核心的組件,其正確性和穩定性是整個 SQL 引擎的基石,在分佈式數據庫中,如何在保證數據一致性的前提下實現無鎖的 DDL 操做是一件有挑戰的事情。本文首先會介紹 TiDB DDL 組件的整體設計,介紹如何在分佈式場景下支持無鎖 shema 變動,描述這套算法的大體流程,而後詳細介紹一些常見的 DDL 語句的源碼實現,包括 create table
、add index
、drop column
、drop table
這四種。git
TiDB 的 DDL 經過實現 Google F1 的在線異步 schema 變動算法,來完成在分佈式場景下的無鎖,在線 schema 變動。爲了簡化設計,TiDB 在同一時刻,只容許一個節點執行 DDL 操做。用戶能夠把多個 DDL 請求發給任何 TiDB 節點,可是全部的 DDL 請求在 TiDB 內部是由 owner 節點的 worker 串行執行的。github
這裏只是簡單概述了 TiDB 的 DDL 設計,下兩篇文章詳細介紹了 TiDB DDL 的設計實現以及優化,推薦閱讀:算法
下圖描述了一個 DDL 請求在 TiDB 中的簡單處理流程:api
<center>圖 1:TiDB 中 DDL SQL 的處理流程</center>session
TiDB 的 DDL 組件相關代碼存放在源碼目錄的 ddl
目錄下。數據結構
File | Introduction |
---|---|
ddl.go |
包含 DDL 接口定義和其實現。 |
ddl_api.go |
提供 create , drop , alter , truncate , rename 等操做的 API,供 Executor 調用。主要功能是封裝 DDL 操做的 job 而後存入 DDL job queue,等待 job 執行完成後返回。 |
ddl_worker.go |
DDL worker 的實現。owner 節點的 worker 從 job queue 中取 job,而後執行,執行完成後將 job 存入 job history queue 中。 |
syncer.go |
負責同步 ddl worker 的 owner 和 follower 間的 schema version 。 每次 DDL 狀態變動後 schema version ID 都會加 1。 |
ddl owner
相關的代碼單獨放在 owner
目錄下,實現了 owner 選舉等功能。併發
另外,ddl job queue
和 history ddl job queue
這兩個隊列都是持久化到 TiKV 中的。structure
目錄下有 list,hash
等數據結構在 TiKV 上的實現。異步
本文接下來按照 TiDB 源碼的 origin/source-code 分支講解,最新的 master 分支和 source-code 分支代碼會稍有一些差別。
create table
須要把 table 的元信息(TableInfo)從 SQL 中解析出來,作一些檢查,而後把 table 的元信息持久化保存到 TiKV 中。具體流程以下:
語法解析:ParseSQL 解析成抽象語法樹 CreateTableStmt。
編譯生成 Plan:Compile 生成 DDL plan , 並 check 權限等。
生成執行器:buildExecutor 生成 DDLExec 執行器。TiDB 的執行器是火山模型。
執行器調用 e.Next 開始執行,即 DDLExec.Next 方法,判斷 DDL 類型後執行 executeCreateTable , 其實質是調用 ddl_api.go
的 CreateTable 函數。
CreateTable 方法是主要流程以下:
tableInfo
, 即 table 的元信息,而後封裝成一個 DDL job,這個 job 包含了 table ID
和 tableInfo
,並將這個 job 的 type 標記爲 ActionCreateTable
。ddl_worker
協程運行 onDDLWorker 函數(最新 Master 分支函數名已重命名爲 start),每隔一段時間調用 handleDDLJobQueu 函數去嘗試處理 DDL job 隊列裏的 job,ddl_worker
會先 check 本身是否是 owner,若是不是 owner,就什麼也不作,而後返回;若是是 owner,就調用 getFirstDDLJob 函數獲取 DDL 隊列中的第一個 job,而後調 runDDLJob 函數執行 job。
create table
類型的 job,會調用 onCreateTable 函數,而後作一些 check 後,會調用 t.CreateTable 函數,將 db_ID
和 table_ID
映射爲 key
,tableInfo
做爲 value 存到 TiKV 裏面去,並更新 job 的狀態。add index
主要作 2 件事:
修改 table 的元信息,把 indexInfo
加入到 table 的元信息中去。
把 table 中已有了的數據行,把 index columns
的值所有回填到 index record
中去。
具體執行流程的前部分的 SQL 解析、Compile 等流程,和 create table
同樣,能夠直接從 DDLExec.Next 開始看,而後調用 alter
語句的 e.executeAlterTable(x) 函數,其實質調 ddl 的 AlterTable 函數,而後調用 CreateIndex 函數,開始執行 add index 的主要工做,具體流程以下:
Check 一些限制,好比 table 是否存在,索引是否已經存在,索引名是否太長等。
封裝成一個 job,包含了索引名,索引列等,並將 job 的 type 標記爲 ActionAddIndex
。
給 job 獲取一個 global job ID 而後放到 DDL job 隊列中去。
owner ddl worker
從 DDL job 隊列中取出 job,根據 job 的類型調用 onCreateIndex 函數。
buildIndexInfo
生成 indexInfo
,而後更新 tableInfo
中的 Indices
,持久化到 TiKV 中去。none -> delete only -> write only -> reorganization -> public
。在 reorganization -> public
時,首先調用 getReorgInfo 獲取 reorgInfo
,主要包含須要 reorganization
的 range,即從表的第一行一直到最後一行數據都須要回填到 index record
中。而後調用 runReorgJob , addTableIndex 函數開始填充數據到 index record
中去。runReorgJob 函數會按期保存回填數據的進度到 TiKV。addTableIndex 的流程以下:
worker
用於併發回填數據到 index record
。reorgInfo
中須要 reorganization
分裂成多個 range。掃描的默認範圍是 [startHandle , endHandle]
,而後默認以 128 爲間隔分裂成多個 range,以後並行掃描對應數據行。在 master 分支中,range 範圍信息是從 PD 中獲取。worker
並行回填 index record
。worker
完成後,更新 reorg
進度,而後持續第 3 步直到全部的 task 都作完。後續執行 finishDDLJob,檢測 history ddl job 流程和 create table
相似。
drop Column
只要修改 table 的元信息,把 table 元信息中對應的要刪除的 column 刪除。drop Column
不會刪除原有 table 數據行中的對應的 Column 數據,在 decode 一行數據時,會根據 table 的元信息來 decode。
具體執行流程的前部分都相似,直接跳到 DropColumn 函數開始,具體執行流程以下:
Check table 是否存在,要 drop 的 column 是否存在等。
封裝成一個 job, 將 job 類型標記爲 ActionDropColumn
,而後放到 DDL job 隊列中去
owner ddl worker
從 DDL job 隊列中取出 job,根據 job 的類型調用 onDropColumn 函數:
column info
的狀態變化和 add index
時的變化幾乎相反:public -> write only -> delete only -> reorganization -> absent
。後續執行 finishDDLJob,檢測 history ddl job 流程和 create table
相似。
drop table
須要刪除 table 的元信息和 table 中的數據。
具體執行流程的前部分都相似,owner ddl worker
從 DDL job 隊列中取出 job 後執行 onDropTable 函數:
tableInfo
的狀態變化是:public -> write only -> delete only -> none
。
tableInfo
的狀態變爲 none
以後,會調用 DropTable 將 table 的元信息從 TiKV 上刪除。
至於刪除 table 中的數據,後面在調用 finishDDLJob 函數將 job 從 job queue 中移除,加入 history ddl job queue 前,會調用 delRangeManager.addDelRangeJob(job),將要刪除的 table 數據範圍插入到表 gc_delete_range
中,而後由 GC worker 根據 gc_delete_range
中的信息在 GC 過程當中作真正的刪除數據操做。
目前 TiDB 最新的 Master 分支的 DDL 引入了並行 DDL,用來加速多個 DDL 語句的執行速度。由於串行執行 DDL 時,add index
操做須要把 table 中已有的數據回填到 index record
中,若是 table 中的數據較多,回填數據的耗時較長,就會阻塞後面 DDL 的操做。目前並行 DDL 的設計是將 add index job
放到新增的 add index job queue
中去,其它類型的 DDL job 仍是放在原來的 job queue。相應的,也增長一個 add index worker
來處理 add index job queue
中的 job。
<center>圖 2:並行 DDL 處理流程</center>
並行 DDL 同時也引入了 job 依賴的問題。job 依賴是指同一 table 的 DDL job,job ID 小的須要先執行。由於對於同一個 table 的 DDL 操做必須是順序執行的。好比說,add column a
,而後 add index on column a
, 若是 add index
先執行,而 add column
的 DDL 假設還在排隊未執行,這時 add index on column a
就會報錯說找不到 column a
。因此當 add index job queue
中的 job2 執行前,須要檢測 job queue 是否有同一 table 的 job1 還未執行,經過對比 job 的 job ID 大小來判斷。執行 job queue 中的 job 時也須要檢查 add index job queue
中是否有依賴的 job 還未執行。
TiDB 目前一共支持 十多種 DDL,具體以及和 MySQL 兼容性對比能夠看 這裏。剩餘其它類型的 DDL 源碼實現讀者能夠自行閱讀,流程和上述幾種 DDL 相似。
做者:陳霜