摘要:作者蘇立在之前的一篇文章源碼閱讀系列文章三的一生中,我們介紹了在收到客戶端請求包時,最常見的的請求處理流程。通常的執行后,會向客戶端持續返回結果,返回速率受控制見源碼閱讀系列文章十和執行框架簡介,但實際中返回的結果集可能非常大。
作者:蘇立
在之前的一篇文章《TiDB 源碼閱讀系列文章(三)SQL 的一生》中,我們介紹了 TiDB 在收到客戶端請求包時,最常見的 Command --- COM_QUERY 的請求處理流程。本文我們將介紹另外一種大家經常使用的 Command --- Prepare/Execute 請求在 TiDB 中的處理過程。
Prepare/Execute Statement 簡介首先我們先簡單回顧下客戶端使用 Prepare 請求過程:
客戶端發起 Prepare 命令將帶 “?” 參數占位符的 SQL 語句發送到數據庫,成功后返回 stmtID。
具體執行 SQL 時,客戶端使用之前返回的 stmtID,并帶上請求參數發起 Execute 命令來執行 SQL。
不再需要 Prepare 的語句時,關閉 stmtID 對應的 Prepare 語句。
相比普通請求,Prepare 帶來的好處是:
減少每次執行經過 Parser 帶來的負擔,因為很多場景,線上運行的 SQL 多是相同的內容,僅是參數部分不同,通過 Prepare 可以通過首次準備好帶占位符的 SQL,后續只需要填充參數執行就好,可以做到“一次 Parse,多次使用”。
在開啟 PreparePlanCache 后可以達到“一次優化,多次使用”,不用進行重復的邏輯和物理優化過程。
更少的網絡傳輸,因為多次執行只用傳輸參數部分,并且返回結果 Binary 協議。
因為是在執行的同時填充參數,可以防止 SQL 注入風險。
某些特性比如 serverSideCursor 需要是通過 Prepare statement 才能使用。
TiDB 和 MySQL 協議 一樣,對于發起 Prepare/Execute 這種使用訪問模式提供兩種方式:
Binary 協議:即上述的使用 COM_STMT_PREPARE,COM_STMT_EXECUTE,COM_STMT_CLOSE 命令并且通過 Binary 協議獲取返回結果,這是目前各種應用開發常使用的方式。
文本協議:使用 COM_QUERY,并且用 PREPARE,EXECUTE,DEALLOCATE PREPARE 使用文本協議獲取結果,這個效率不如上一種,多用于非程序調用場景,比如在 MySQL 客戶端中手工執行。
下面我們主要以 Binary 協議來看下 TiDB 的處理過程。文本協議的處理與 Binary 協議處理過程比較類似,我們會在后面簡要介紹一下它們的差異點。
COM_STMT_PREPARE首先,客戶端發起 COM_STMT_PREPARE,在 TiDB 收到后會進入 clientConn#handleStmtPrepare,這個函數會通過調用 TiDBContext#Prepare 來進行實際 Prepare 操作并返回 結果 給客戶端,實際的 Prepare 處理主要在 session#PrepareStmt 和 PrepareExec 中完成:
調用 Parser 完成文本到 AST 的轉換,這部分可以參考《TiDB 源碼閱讀系列文章(五)TiDB SQL Parser 的實現》。
使用名為 paramMarkerExtractor 的 visitor 從 AST 中提取 “?” 表達式,并根據出現位置(offset)構建排序 Slice,后面我們會看到在 Execute 時會通過這個 Slice 值來快速定位并替換 “?” 占位符。
檢查參數個數是否超過 Uint16 最大值(這個是 協議限制,對于參數只提供 2 個 Byte)。
進行 Preprocess, 并且創建 LogicPlan, 這部分實現可以參考之前關于 邏輯優化的介紹,這里生成 LogicPlan 主要為了獲取并檢查組成 Prepare 響應中需要的列信息。
生成 stmtID,生成的方式是當前會話中的遞增 int。
保存 stmtID 到?ast.Prepared (由 AST,參數類型信息,schema 版本,是否使用 PreparedPlanCache 標記組成) 的映射信息到 SessionVars#PreparedStmts 中供 Execute 部分使用。
保存 stmtID 到 TiDBStatement (由 stmtID,參數個數,SQL 返回列類型信息,sendLongData 預 BoundParams 組成)的映射信息保存到 TiDBContext#stmts。
在處理完成之后客戶端會收到并持有 stmtID 和參數類型信息,返回列類型信息,后續即可通過 stmtID 進行執行時,server 可以通過 6、7 步保存映射找到已經 Prepare 的信息。
COM_STMT_EXECUTEPrepare 成功之后,客戶端會通過 COM_STMT_EXECUTE 命令請求執行,TiDB 會進入 clientConn#handleStmtExecute,首先會通過 stmtID 在上節介紹中保存的 TiDBContext#stmts 中獲取前面保存的 TiDBStatement,并解析出是否使用 userCursor 和請求參數信息,并且調用對應 TiDBStatement 的 Execute 進行實際的 Execute 邏輯:
生成 ast.ExecuteStmt 并調用 planer.Optimize 生成 plancore.Execute,和普通優化過程不同的是會執行 Exeucte#OptimizePreparedPlan。
使用 stmtID 通過 SessionVars#PreparedStmts 獲取到到 Prepare 階段的 ast.Prepared 信息。
使用上一節第 2 步中準備的 prepared.Params 來快速查找并填充參數值;同時會保存一份參數到 sessionVars.PreparedParams 中,這個主要用于支持 PreparePlanCache 延遲獲取參數。
判斷對比判斷 Prepare 和 Execute 之間 schema 是否有變化,如果有變化則重新 Preprocess。
之后調用 Execute#getPhysicalPlan 獲取物理計劃,實現中首先會根據是否啟用 PreparedPlanCache 來查找已緩存的 Plan,本文后面我們也會專門介紹這個。
在沒有開啟 PreparedPlanCache 或者開啟了但沒命中 cache 時,會對 AST 進行一次正常的 Optimize。
在獲取到 PhysicalPlan 后就是正常的 Executing 執行。
COM_STMT_CLOSE在客戶不再需要執行之前的 Prepared 的語句時,可以通過 COM_STMT_CLOSE 來釋放服務器資源,TiDB 收到后會進入 clientConn#handleStmtClose,會通過 stmtID 在 TiDBContext#stmts 中找到對應的 TiDBStatement,并且執行 Close 清理之前的保存的 TiDBContext#stmts 和 SessionVars#PrepareStmts,不過通過代碼我們看到,對于前者的確直接進行了清理,對于后者不會刪除而是加入到 RetryInfo#DroppedPreparedStmtIDs 中,等待當前事務提交或回滾才會從 SessionVars#PrepareStmts 中清理,之所以延遲刪除是由于 TiDB 在事務提交階段遇到沖突會根據配置決定是否重試事務,參與重試的語句可能只有 Execute 和 Deallocate,為了保證重試還能通過 stmtID 找到 prepared 的語句 TiDB 目前使用延遲到事務執行完成后才做清理。
其他 COM_STMT除了上面介紹的 3 個 COM_STMT,還有另外幾個 COM_STMT_SEND_LONG_DATA,COM_STMT_FETCH,COM_STMT_RESET 也會在 Prepare 中使用到。
COM_STMT_SEND_LONG_DATA某些場景我們 SQL 中的參數是 TEXT,TINYTEXT,MEDIUMTEXT,LONGTEXT and BLOB,TINYBLOB,MEDIUMBLOB,LONGBLOB 列時,客戶端通常不會在一次 Execute 中帶大量的參數,而是多帶帶通過 COM_SEND_LONG_DATA 預先發到 TiDB,最后再進行 Execute。
TiDB 的處理在 client#handleStmtSendLongData,通過 stmtID 在 TiDBContext#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,statementID,schemaVersion, snapshotTs,sqlMode,timezone 組成,所以要命中 plan cache 這以上元素必須都和上次緩存的一致),并根據配置的緩存大小和內存大小做 LRU。
在 Execute 的處理邏輯 PrepareExec 中除了檢查 PreparePlanCache 是否開啟外,還會判斷當前的語句是否能使用 PreparePlanCache。
只有 SELECT,INSERT,UPDATE,DELETE 有可能可以使用 PreparedPlanCache 。
并進一步通過 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, 并最終轉換為和二進制協議一樣的 PrepareExec,ExecuteExec,DealocateExec 的執行器進行執行。
寫在最后Prepared 是提高程序 SQL 執行效率的有效手段之一。熟悉 TiDB 的 Prepared 實現,可以幫助各位讀者在將來使用 Prepared 時更加得心應手。另外,如果有興趣向 TiDB 貢獻代碼的讀者,也可以通過本文更快的理解這部分的實現。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/17888.html
摘要:在事務提交結束之后,事務可能提交成功,也可能提交失敗。需要把這個狀態告知如果發生了,那么輸出的類型就為,如果成功提交,那么輸出的類型就為。,當完成自己所有的狀態變更之后,會把的狀態改為。 作者:姚維 TiDB Binlog Overview 這篇文章不是講 TiDB Binlog 組件的源碼,而是講 TiDB 在執行 DML/DDL 語句過程中,如何將 Binlog 數據 發送給 Ti...
閱讀 2473·2021-11-16 11:45
閱讀 2442·2021-10-11 10:59
閱讀 2250·2021-10-08 10:05
閱讀 3813·2021-09-23 11:30
閱讀 2369·2021-09-07 09:58
閱讀 789·2019-08-30 15:55
閱讀 772·2019-08-30 15:53
閱讀 1922·2019-08-29 17:00