TiDB 源碼閱讀系列文章(二十三)Prepare/Execute 請求處理

做者:蘇立html

在以前的一篇文章《TiDB 源碼閱讀系列文章(三)SQL 的一輩子》中,咱們介紹了 TiDB 在收到客戶端請求包時,最多見的 Command --- COM_QUERY 的請求處理流程。本文咱們將介紹另一種你們常用的 Command --- Prepare/Execute 請求在 TiDB 中的處理過程。mysql

Prepare/Execute Statement 簡介

首先咱們先簡單回顧下客戶端使用 Prepare 請求過程:git

  1. 客戶端發起 Prepare 命令將帶 「?」 參數佔位符的 SQL 語句發送到數據庫,成功後返回 stmtIDgithub

  2. 具體執行 SQL 時,客戶端使用以前返回的 stmtID,並帶上請求參數發起 Execute 命令來執行 SQL。sql

  3. 再也不須要 Prepare 的語句時,關閉 stmtID 對應的 Prepare 語句。數據庫

相比普通請求,Prepare 帶來的好處是:express

  • 減小每次執行通過 Parser 帶來的負擔,由於不少場景,線上運行的 SQL 可能是相同的內容,僅是參數部分不一樣,經過 Prepare 能夠經過首次準備好帶佔位符的 SQL,後續只須要填充參數執行就好,能夠作到「一次 Parse,屢次使用」。緩存

  • 在開啓 PreparePlanCache 後能夠達到「一次優化,屢次使用」,不用進行重複的邏輯和物理優化過程。服務器

  • 更少的網絡傳輸,由於屢次執行只用傳輸參數部分,而且返回結果 Binary 協議。網絡

  • 由於是在執行的同時填充參數,能夠防止 SQL 注入風險。

  • 某些特性好比 serverSideCursor 須要是經過 Prepare statement 才能使用。

TiDB 和 MySQL 協議 同樣,對於發起 Prepare/Execute 這種使用訪問模式提供兩種方式:

  • Binary 協議:即上述的使用 COM_STMT_PREPARECOM_STMT_EXECUTECOM_STMT_CLOSE 命令而且經過 Binary 協議獲取返回結果,這是目前各類應用開發常使用的方式。

  • 文本協議:使用 COM_QUERY,而且用 PREPAREEXECUTEDEALLOCATE PREPARE 使用文本協議獲取結果,這個效率不如上一種,多用於非程序調用場景,好比在 MySQL 客戶端中手工執行。

下面咱們主要以 Binary 協議來看下 TiDB 的處理過程。文本協議的處理與 Binary 協議處理過程比較相似,咱們會在後面簡要介紹一下它們的差別點。

COM_STMT_PREPARE

首先,客戶端發起 COM_STMT_PREPARE,在 TiDB 收到後會進入 clientConn#handleStmtPrepare,這個函數會經過調用 TiDBContext#Prepare 來進行實際 Prepare 操做並返回 結果 給客戶端,實際的 Prepare 處理主要在 session#PrepareStmtPrepareExec 中完成:

  1. 調用 Parser 完成文本到 AST 的轉換,這部分能夠參考《TiDB 源碼閱讀系列文章(五)TiDB SQL Parser 的實現》

  2. 使用名爲 paramMarkerExtractor 的 visitor 從 AST 中提取 「?」 表達式,並根據出現位置(offset)構建排序 Slice,後面咱們會看到在 Execute 時會經過這個 Slice 值來快速定位並替換 「?」 佔位符。

  3. 檢查參數個數是否超過 Uint16 最大值(這個是 協議限制,對於參數只提供 2 個 Byte)。

  4. 進行 Preprocess, 而且建立 LogicPlan, 這部分實現能夠參考以前關於 邏輯優化的介紹,這裏生成 LogicPlan 主要爲了獲取並檢查組成 Prepare 響應中須要的列信息。

  5. 生成 stmtID,生成的方式是當前會話中的遞增 int。

  6. 保存 stmtID 到 ast.Prepared (由 AST,參數類型信息,schema 版本,是否使用 PreparedPlanCache 標記組成) 的映射信息到 SessionVars#PreparedStmts 中供 Execute 部分使用。

  7. 保存 stmtIDTiDBStatement (由 stmtID,參數個數,SQL 返回列類型信息,sendLongDataBoundParams 組成)的映射信息保存到 TiDBContext#stmts

在處理完成以後客戶端會收到並持有 stmtID 和參數類型信息,返回列類型信息,後續便可經過 stmtID 進行執行時,server 能夠經過 六、7 步保存映射找到已經 Prepare 的信息。

COM_STMT_EXECUTE

Prepare 成功以後,客戶端會經過 COM_STMT_EXECUTE 命令請求執行,TiDB 會進入 clientConn#handleStmtExecute,首先會經過 stmtID 在上節介紹中保存的 TiDBContext#stmts 中獲取前面保存的 TiDBStatement,並解析出是否使用 userCursor 和請求參數信息,而且調用對應 TiDBStatement 的 Execute 進行實際的 Execute 邏輯:

  1. 生成 ast.ExecuteStmt 並調用 planer.Optimize 生成 plancore.Execute,和普通優化過程不一樣的是會執行 Exeucte#OptimizePreparedPlan

  2. 使用 stmtID 經過 SessionVars#PreparedStmts 獲取到到 Prepare 階段的 ast.Prepared 信息。

  3. 使用上一節第 2 步中準備的 prepared.Params 來快速查找並填充參數值;同時會保存一份參數到 sessionVars.PreparedParams 中,這個主要用於支持 PreparePlanCache 延遲獲取參數。

  4. 判斷對比判斷 Prepare 和 Execute 之間 schema 是否有變化,若是有變化則從新 Preprocess。

  5. 以後調用 Execute#getPhysicalPlan 獲取物理計劃,實現中首先會根據是否啓用 PreparedPlanCache 來查找已緩存的 Plan,本文後面咱們也會專門介紹這個。

  6. 在沒有開啓 PreparedPlanCache 或者開啓了但沒命中 cache 時,會對 AST 進行一次正常的 Optimize。

在獲取到 PhysicalPlan 後就是正常的 Executing 執行

COM_STMT_CLOSE

在客戶再也不須要執行以前的 Prepared 的語句時,能夠經過 COM_STMT_CLOSE 來釋放服務器資源,TiDB 收到後會進入 clientConn#handleStmtClose,會經過 stmtIDTiDBContext#stmts 中找到對應的 TiDBStatement,而且執行 Close 清理以前的保存的 TiDBContext#stmtsSessionVars#PrepareStmts,不過經過代碼咱們看到,對於前者的確直接進行了清理,對於後者不會刪除而是加入到 RetryInfo#DroppedPreparedStmtIDs 中,等待當前事務提交或回滾纔會從 SessionVars#PrepareStmts 中清理,之因此延遲刪除是因爲 TiDB 在事務提交階段遇到衝突會根據配置決定是否重試事務,參與重試的語句可能只有 Execute 和 Deallocate,爲了保證重試還能經過 stmtID 找到 prepared 的語句 TiDB 目前使用延遲到事務執行完成後才作清理。

其餘 COM_STMT

除了上面介紹的 3 個 COM_STMT,還有另外幾個 COM_STMT_SEND_LONG_DATACOM_STMT_FETCHCOM_STMT_RESET 也會在 Prepare 中使用到。

COM_STMT_SEND_LONG_DATA

某些場景咱們 SQL 中的參數是 TEXTTINYTEXTMEDIUMTEXTLONGTEXT and BLOBTINYBLOBMEDIUMBLOBLONGBLOB 列時,客戶端一般不會在一次 Execute 中帶大量的參數,而是單獨經過 COM_SEND_LONG_DATA 預先發到 TiDB,最後再進行 Execute。

TiDB 的處理在 client#handleStmtSendLongData,經過 stmtIDTiDBContext#stmts 中找到 TiDBStatement 並提早放置 paramID 對應的參數信息,進行追加參數到 boundParams(因此客戶端其實能夠屢次 send 數據並追加到一個參數上),Execute 時會經過 stmt.BoundParams() 獲取到提早傳過來的參數並和 Execute 命令帶的參數 一塊兒執行,在每次執行完成後會重置 boundParams

COM_STMT_FETCH

一般的 Execute 執行後,TiDB 會向客戶端持續返回結果,返回速率受 max_chunk_size 控制(見《TiDB 源碼閱讀系列文章(十)Chunk 和執行框架簡介》), 但實際中返回的結果集可能很是大。客戶端受限於資源(通常是內存)沒法一次處理那麼多數據,就但願服務端一批批返回,COM_STMT_FETCH 正好解決這個問題。

它的使用首先要和 COM_STMT_EXECUTE 配合(也就是必須使用 Prepared 語句執行), handleStmtExeucte 請求協議 flag 中有標記要使用 cursor,execute 在完成 plan 拿到結果集後並不當即執行而是把它緩存到 TiDBStatement 中,並馬上向客戶端回包中帶上列信息並標記 ServerStatusCursorExists,這部分邏輯能夠參看 handleStmtExecute

客戶端看到 ServerStatusCursorExists 後,會用 COM_STMT_FETCH 向 TiDB 拉去指定 fetchSize 大小的結果集,在 connClient#handleStmtFetch 中,會經過 session 找到 TiDBStatement 進而找到以前緩存的結果集,開始實際調用執行器的 Next 獲取知足 fetchSize 的數據並返回客戶端,若是執行器一次 Next 超過了 fetchSize 會只返回 fetchSize 大小的數據並把剩下的數據留着下次再給客戶端,最後對於結果集最後一次返回會標記 ServerStatusLastRowSend 的 flag 通知客戶端沒有後續數據。

COM_STMT_RESET

主要用於客戶端主動重置 COM_SEND_LONG_DATA 發來的數據,正常 COM_STMT_EXECUTE 後會自動重置,主要針對客戶端但願主動廢棄以前數據的狀況,由於 COM_STMT_SEND_LONG_DATA 是一直追加的操做,客戶端某些場景須要主動放棄以前預存的參數,這部分邏輯主要位於 connClient#handleStmtReset 中。

Prepared Plan Cache

經過前面的解析過程咱們看到在 Prepare 時完成了 AST 轉換,在以後的 Execute 會經過 stmtID 找以前的 AST 來進行 Plan 跳過每次都進行 Parse SQL 的開銷。若是開啓了 Prepare Plan Cache,可進一步在 Execute 處理中重用上次的 PhysicalPlan 結果,省掉查詢優化過程的開銷。

TiDB 能夠經過 修改配置文件 開啓 Prepare Plan Cache, 開啓後每一個新 Session 建立時會初始化一個 SimpleLRUCache 類型的 preparedPlanCache 用於保存用於緩存 Plan 結果,緩存的 key 是 pstmtPlanCacheKey(由當前 DB,鏈接 ID,statementIDschemaVersionsnapshotTssqlModetimezone 組成,因此要命中 plan cache 這以上元素必須都和上次緩存的一致),並根據配置的緩存大小和內存大小作 LRU。

在 Execute 的處理邏輯 PrepareExec 中除了檢查 PreparePlanCache 是否開啓外,還會判斷當前的語句是否能使用 PreparePlanCache

  1. 只有 SELECTINSERTUPDATEDELETE 有可能可使用 PreparedPlanCache

  2. 並進一步經過 cacheableChecker visitor 檢查 AST 中是否有變量表達式,子查詢,"order by ?","limit ?,?" 和 UnCacheableFunctions 的函數調用等不可使用 PlanCache 的狀況。

若是檢查都經過則在 Execute#getPhysicalPlan 中會用當前環境構建 cache key 查找 preparePlanCache

未命中 Cache

咱們首先來看下沒有命中 Cache 的狀況。發現沒有命中後會用 stmtID 找到的 AST 執行 Optimize,但和正常執行 Optimize 不一樣對於 Cache 的 Plan, 我須要對 「?」 作延遲求值處理, 即將佔位符轉換爲一個 function 作 Plan 並 Cache, 後續從 Cache 獲取後 function 在執行時再從具體執行上下文中實際獲取執行參數。

回顧下構建 LogicPlan 的過程當中會經過 expressionRewriter 將 AST 轉換爲各種 expression.Expression,一般對於 ParamMarkerExpr 會重寫爲 Constant 類型的 expression,但若是該條 stmt 支持 Cache 的話會重寫爲 Constant 並帶上一個特殊的 DeferredExpr 指向一個 GetParam 的函數表達式,而這個函數會在執行時實際從前面 Execute 保存到 sessionVars.PreparedParams 中獲取,這樣就作到了 Plan 並 Cache 一個參數無關的 Plan,而後實際執行的時填充參數。

新獲取 Plan 後會保存到 preparedPlanCache 供後續使用。

命中 Cache

讓咱們回到 getPhysicalPlan,若是 Cache 命中在獲取 Plan 後咱們須要從新 build plan 的 range,由於前面咱們保存的 Plan 是一個帶 GetParam 的函數表達式,而再次獲取後,當前參數值已經變化,咱們須要根據當前 Execute 的參數來從新修正 range,這部分邏輯代碼位於 Execute#rebuildRange 中,以後就是正常的執行過程了。

文本協議的 Prepared

前面主要介紹了二進制協議的 Prepared 執行流程,還有一種執行方式是經過二進制協議來執行。

客戶端能夠經過 COM_QUREY 發送:

PREPARE stmt_name FROM prepareable_stmt;
EXECUTE stmt_name USING @var_name1, @var_name2,...
DEALLOCTE PREPARE stmt_name

來進行 Prepared,TiDB 會走正常 文本 Query 處理流程,將 SQL 轉換 Prepare,Execute,Deallocate 的 Plan, 並最終轉換爲和二進制協議同樣的 PrepareExecExecuteExecDealocateExec 的執行器進行執行。

寫在最後

Prepared 是提升程序 SQL 執行效率的有效手段之一。熟悉 TiDB 的 Prepared 實現,能夠幫助各位讀者在未來使用 Prepared 時更加駕輕就熟。另外,若是有興趣向 TiDB 貢獻代碼的讀者,也能夠經過本文更快的理解這部分的實現。

相關文章
相關標籤/搜索