摘要:作者蘇立在之前的一篇文章源碼閱讀系列文章三的一生中,我們介紹了在收到客戶端請(qǐng)求包時(shí),最常見(jiàn)的的請(qǐng)求處理流程。通常的執(zhí)行后,會(huì)向客戶端持續(xù)返回結(jié)果,返回速率受控制見(jiàn)源碼閱讀系列文章十和執(zhí)行框架簡(jiǎn)介,但實(shí)際中返回的結(jié)果集可能非常大。
作者:蘇立
在之前的一篇文章《TiDB 源碼閱讀系列文章(三)SQL 的一生》中,我們介紹了 TiDB 在收到客戶端請(qǐng)求包時(shí),最常見(jiàn)的 Command --- COM_QUERY 的請(qǐng)求處理流程。本文我們將介紹另外一種大家經(jīng)常使用的 Command --- Prepare/Execute 請(qǐng)求在 TiDB 中的處理過(guò)程。
Prepare/Execute Statement 簡(jiǎn)介首先我們先簡(jiǎn)單回顧下客戶端使用 Prepare 請(qǐng)求過(guò)程:
客戶端發(fā)起 Prepare 命令將帶 “?” 參數(shù)占位符的 SQL 語(yǔ)句發(fā)送到數(shù)據(jù)庫(kù),成功后返回 stmtID。
具體執(zhí)行 SQL 時(shí),客戶端使用之前返回的 stmtID,并帶上請(qǐng)求參數(shù)發(fā)起 Execute 命令來(lái)執(zhí)行 SQL。
不再需要 Prepare 的語(yǔ)句時(shí),關(guān)閉 stmtID 對(duì)應(yīng)的 Prepare 語(yǔ)句。
相比普通請(qǐng)求,Prepare 帶來(lái)的好處是:
減少每次執(zhí)行經(jīng)過(guò) Parser 帶來(lái)的負(fù)擔(dān),因?yàn)楹芏鄨?chǎng)景,線上運(yùn)行的 SQL 多是相同的內(nèi)容,僅是參數(shù)部分不同,通過(guò) Prepare 可以通過(guò)首次準(zhǔn)備好帶占位符的 SQL,后續(xù)只需要填充參數(shù)執(zhí)行就好,可以做到“一次 Parse,多次使用”。
在開(kāi)啟 PreparePlanCache 后可以達(dá)到“一次優(yōu)化,多次使用”,不用進(jìn)行重復(fù)的邏輯和物理優(yōu)化過(guò)程。
更少的網(wǎng)絡(luò)傳輸,因?yàn)槎啻螆?zhí)行只用傳輸參數(shù)部分,并且返回結(jié)果 Binary 協(xié)議。
因?yàn)槭窃趫?zhí)行的同時(shí)填充參數(shù),可以防止 SQL 注入風(fēng)險(xiǎn)。
某些特性比如 serverSideCursor 需要是通過(guò) Prepare statement 才能使用。
TiDB 和 MySQL 協(xié)議 一樣,對(duì)于發(fā)起 Prepare/Execute 這種使用訪問(wèn)模式提供兩種方式:
Binary 協(xié)議:即上述的使用 COM_STMT_PREPARE,COM_STMT_EXECUTE,COM_STMT_CLOSE 命令并且通過(guò) Binary 協(xié)議獲取返回結(jié)果,這是目前各種應(yīng)用開(kāi)發(fā)常使用的方式。
文本協(xié)議:使用 COM_QUERY,并且用 PREPARE,EXECUTE,DEALLOCATE PREPARE 使用文本協(xié)議獲取結(jié)果,這個(gè)效率不如上一種,多用于非程序調(diào)用場(chǎng)景,比如在 MySQL 客戶端中手工執(zhí)行。
下面我們主要以 Binary 協(xié)議來(lái)看下 TiDB 的處理過(guò)程。文本協(xié)議的處理與 Binary 協(xié)議處理過(guò)程比較類(lèi)似,我們會(huì)在后面簡(jiǎn)要介紹一下它們的差異點(diǎn)。
COM_STMT_PREPARE首先,客戶端發(fā)起 COM_STMT_PREPARE,在 TiDB 收到后會(huì)進(jìn)入 clientConn#handleStmtPrepare,這個(gè)函數(shù)會(huì)通過(guò)調(diào)用 TiDBContext#Prepare 來(lái)進(jìn)行實(shí)際 Prepare 操作并返回 結(jié)果 給客戶端,實(shí)際的 Prepare 處理主要在 session#PrepareStmt 和 PrepareExec 中完成:
調(diào)用 Parser 完成文本到 AST 的轉(zhuǎn)換,這部分可以參考《TiDB 源碼閱讀系列文章(五)TiDB SQL Parser 的實(shí)現(xiàn)》。
使用名為 paramMarkerExtractor 的 visitor 從 AST 中提取 “?” 表達(dá)式,并根據(jù)出現(xiàn)位置(offset)構(gòu)建排序 Slice,后面我們會(huì)看到在 Execute 時(shí)會(huì)通過(guò)這個(gè) Slice 值來(lái)快速定位并替換 “?” 占位符。
檢查參數(shù)個(gè)數(shù)是否超過(guò) Uint16 最大值(這個(gè)是 協(xié)議限制,對(duì)于參數(shù)只提供 2 個(gè) Byte)。
進(jìn)行 Preprocess, 并且創(chuàng)建 LogicPlan, 這部分實(shí)現(xiàn)可以參考之前關(guān)于 邏輯優(yōu)化的介紹,這里生成 LogicPlan 主要為了獲取并檢查組成 Prepare 響應(yīng)中需要的列信息。
生成 stmtID,生成的方式是當(dāng)前會(huì)話中的遞增 int。
保存 stmtID 到?ast.Prepared (由 AST,參數(shù)類(lèi)型信息,schema 版本,是否使用 PreparedPlanCache 標(biāo)記組成) 的映射信息到 SessionVars#PreparedStmts 中供 Execute 部分使用。
保存 stmtID 到 TiDBStatement (由 stmtID,參數(shù)個(gè)數(shù),SQL 返回列類(lèi)型信息,sendLongData 預(yù) BoundParams 組成)的映射信息保存到 TiDBContext#stmts。
在處理完成之后客戶端會(huì)收到并持有 stmtID 和參數(shù)類(lèi)型信息,返回列類(lèi)型信息,后續(xù)即可通過(guò) stmtID 進(jìn)行執(zhí)行時(shí),server 可以通過(guò) 6、7 步保存映射找到已經(jīng) Prepare 的信息。
COM_STMT_EXECUTEPrepare 成功之后,客戶端會(huì)通過(guò) COM_STMT_EXECUTE 命令請(qǐng)求執(zhí)行,TiDB 會(huì)進(jìn)入 clientConn#handleStmtExecute,首先會(huì)通過(guò) stmtID 在上節(jié)介紹中保存的 TiDBContext#stmts 中獲取前面保存的 TiDBStatement,并解析出是否使用 userCursor 和請(qǐng)求參數(shù)信息,并且調(diào)用對(duì)應(yīng) TiDBStatement 的 Execute 進(jìn)行實(shí)際的 Execute 邏輯:
生成 ast.ExecuteStmt 并調(diào)用 planer.Optimize 生成 plancore.Execute,和普通優(yōu)化過(guò)程不同的是會(huì)執(zhí)行 Exeucte#OptimizePreparedPlan。
使用 stmtID 通過(guò) SessionVars#PreparedStmts 獲取到到 Prepare 階段的 ast.Prepared 信息。
使用上一節(jié)第 2 步中準(zhǔn)備的 prepared.Params 來(lái)快速查找并填充參數(shù)值;同時(shí)會(huì)保存一份參數(shù)到 sessionVars.PreparedParams 中,這個(gè)主要用于支持 PreparePlanCache 延遲獲取參數(shù)。
判斷對(duì)比判斷 Prepare 和 Execute 之間 schema 是否有變化,如果有變化則重新 Preprocess。
之后調(diào)用 Execute#getPhysicalPlan 獲取物理計(jì)劃,實(shí)現(xiàn)中首先會(huì)根據(jù)是否啟用 PreparedPlanCache 來(lái)查找已緩存的 Plan,本文后面我們也會(huì)專(zhuān)門(mén)介紹這個(gè)。
在沒(méi)有開(kāi)啟 PreparedPlanCache 或者開(kāi)啟了但沒(méi)命中 cache 時(shí),會(huì)對(duì) AST 進(jìn)行一次正常的 Optimize。
在獲取到 PhysicalPlan 后就是正常的 Executing 執(zhí)行。
COM_STMT_CLOSE在客戶不再需要執(zhí)行之前的 Prepared 的語(yǔ)句時(shí),可以通過(guò) COM_STMT_CLOSE 來(lái)釋放服務(wù)器資源,TiDB 收到后會(huì)進(jìn)入 clientConn#handleStmtClose,會(huì)通過(guò) stmtID 在 TiDBContext#stmts 中找到對(duì)應(yīng)的 TiDBStatement,并且執(zhí)行 Close 清理之前的保存的 TiDBContext#stmts 和 SessionVars#PrepareStmts,不過(guò)通過(guò)代碼我們看到,對(duì)于前者的確直接進(jìn)行了清理,對(duì)于后者不會(huì)刪除而是加入到 RetryInfo#DroppedPreparedStmtIDs 中,等待當(dāng)前事務(wù)提交或回滾才會(huì)從 SessionVars#PrepareStmts 中清理,之所以延遲刪除是由于 TiDB 在事務(wù)提交階段遇到?jīng)_突會(huì)根據(jù)配置決定是否重試事務(wù),參與重試的語(yǔ)句可能只有 Execute 和 Deallocate,為了保證重試還能通過(guò) stmtID 找到 prepared 的語(yǔ)句 TiDB 目前使用延遲到事務(wù)執(zhí)行完成后才做清理。
其他 COM_STMT除了上面介紹的 3 個(gè) COM_STMT,還有另外幾個(gè) COM_STMT_SEND_LONG_DATA,COM_STMT_FETCH,COM_STMT_RESET 也會(huì)在 Prepare 中使用到。
COM_STMT_SEND_LONG_DATA某些場(chǎng)景我們 SQL 中的參數(shù)是 TEXT,TINYTEXT,MEDIUMTEXT,LONGTEXT and BLOB,TINYBLOB,MEDIUMBLOB,LONGBLOB 列時(shí),客戶端通常不會(huì)在一次 Execute 中帶大量的參數(shù),而是多帶帶通過(guò) COM_SEND_LONG_DATA 預(yù)先發(fā)到 TiDB,最后再進(jìn)行 Execute。
TiDB 的處理在 client#handleStmtSendLongData,通過(guò) stmtID 在 TiDBContext#stmts 中找到 TiDBStatement 并提前放置 paramID 對(duì)應(yīng)的參數(shù)信息,進(jìn)行追加參數(shù)到 boundParams(所以客戶端其實(shí)可以多次 send 數(shù)據(jù)并追加到一個(gè)參數(shù)上),Execute 時(shí)會(huì)通過(guò) stmt.BoundParams() 獲取到提前傳過(guò)來(lái)的參數(shù)并和 Execute 命令帶的參數(shù) 一起執(zhí)行,在每次執(zhí)行完成后會(huì)重置 boundParams。
COM_STMT_FETCH通常的 Execute 執(zhí)行后,TiDB 會(huì)向客戶端持續(xù)返回結(jié)果,返回速率受 max_chunk_size 控制(見(jiàn)《TiDB 源碼閱讀系列文章(十)Chunk 和執(zhí)行框架簡(jiǎn)介》), 但實(shí)際中返回的結(jié)果集可能非常大??蛻舳耸芟抻谫Y源(一般是內(nèi)存)無(wú)法一次處理那么多數(shù)據(jù),就希望服務(wù)端一批批返回,COM_STMT_FETCH 正好解決這個(gè)問(wèn)題。
它的使用首先要和 COM_STMT_EXECUTE 配合(也就是必須使用 Prepared 語(yǔ)句執(zhí)行), handleStmtExeucte 請(qǐng)求協(xié)議 flag 中有標(biāo)記要使用 cursor,execute 在完成 plan 拿到結(jié)果集后并不立即執(zhí)行而是把它緩存到 TiDBStatement 中,并立刻向客戶端回包中帶上列信息并標(biāo)記 ServerStatusCursorExists,這部分邏輯可以參看 handleStmtExecute。
客戶端看到 ServerStatusCursorExists 后,會(huì)用 COM_STMT_FETCH 向 TiDB 拉去指定 fetchSize 大小的結(jié)果集,在 connClient#handleStmtFetch 中,會(huì)通過(guò) session 找到 TiDBStatement 進(jìn)而找到之前緩存的結(jié)果集,開(kāi)始實(shí)際調(diào)用執(zhí)行器的 Next 獲取滿足 fetchSize 的數(shù)據(jù)并返回客戶端,如果執(zhí)行器一次 Next 超過(guò)了 fetchSize 會(huì)只返回 fetchSize 大小的數(shù)據(jù)并把剩下的數(shù)據(jù)留著下次再給客戶端,最后對(duì)于結(jié)果集最后一次返回會(huì)標(biāo)記 ServerStatusLastRowSend 的 flag 通知客戶端沒(méi)有后續(xù)數(shù)據(jù)。
COM_STMT_RESET主要用于客戶端主動(dòng)重置 COM_SEND_LONG_DATA 發(fā)來(lái)的數(shù)據(jù),正常 COM_STMT_EXECUTE 后會(huì)自動(dòng)重置,主要針對(duì)客戶端希望主動(dòng)廢棄之前數(shù)據(jù)的情況,因?yàn)?COM_STMT_SEND_LONG_DATA 是一直追加的操作,客戶端某些場(chǎng)景需要主動(dòng)放棄之前預(yù)存的參數(shù),這部分邏輯主要位于 connClient#handleStmtReset 中。
Prepared Plan Cache通過(guò)前面的解析過(guò)程我們看到在 Prepare 時(shí)完成了 AST 轉(zhuǎn)換,在之后的 Execute 會(huì)通過(guò) stmtID 找之前的 AST 來(lái)進(jìn)行 Plan 跳過(guò)每次都進(jìn)行 Parse SQL 的開(kāi)銷(xiāo)。如果開(kāi)啟了 Prepare Plan Cache,可進(jìn)一步在 Execute 處理中重用上次的 PhysicalPlan 結(jié)果,省掉查詢優(yōu)化過(guò)程的開(kāi)銷(xiāo)。
TiDB 可以通過(guò) 修改配置文件 開(kāi)啟 Prepare Plan Cache, 開(kāi)啟后每個(gè)新 Session 創(chuàng)建時(shí)會(huì)初始化一個(gè) SimpleLRUCache 類(lèi)型的 preparedPlanCache 用于保存用于緩存 Plan 結(jié)果,緩存的 key 是 pstmtPlanCacheKey(由當(dāng)前 DB,連接 ID,statementID,schemaVersion, snapshotTs,sqlMode,timezone 組成,所以要命中 plan cache 這以上元素必須都和上次緩存的一致),并根據(jù)配置的緩存大小和內(nèi)存大小做 LRU。
在 Execute 的處理邏輯 PrepareExec 中除了檢查 PreparePlanCache 是否開(kāi)啟外,還會(huì)判斷當(dāng)前的語(yǔ)句是否能使用 PreparePlanCache。
只有 SELECT,INSERT,UPDATE,DELETE 有可能可以使用 PreparedPlanCache 。
并進(jìn)一步通過(guò) cacheableChecker visitor 檢查 AST 中是否有變量表達(dá)式,子查詢,"order by ?","limit ?,?" 和 UnCacheableFunctions 的函數(shù)調(diào)用等不可以使用 PlanCache 的情況。
如果檢查都通過(guò)則在 Execute#getPhysicalPlan 中會(huì)用當(dāng)前環(huán)境構(gòu)建 cache key 查找 preparePlanCache。
未命中 Cache我們首先來(lái)看下沒(méi)有命中 Cache 的情況。發(fā)現(xiàn)沒(méi)有命中后會(huì)用 stmtID 找到的 AST 執(zhí)行 Optimize,但和正常執(zhí)行 Optimize 不同對(duì)于 Cache 的 Plan, 我需要對(duì) “?” 做延遲求值處理, 即將占位符轉(zhuǎn)換為一個(gè) function 做 Plan 并 Cache, 后續(xù)從 Cache 獲取后 function 在執(zhí)行時(shí)再?gòu)木唧w執(zhí)行上下文中實(shí)際獲取執(zhí)行參數(shù)。
回顧下構(gòu)建 LogicPlan 的過(guò)程中會(huì)通過(guò) expressionRewriter 將 AST 轉(zhuǎn)換為各類(lèi) expression.Expression,通常對(duì)于 ParamMarkerExpr 會(huì)重寫(xiě)為 Constant 類(lèi)型的 expression,但如果該條 stmt 支持 Cache 的話會(huì)重寫(xiě)為 Constant 并帶上一個(gè)特殊的 DeferredExpr 指向一個(gè) GetParam 的函數(shù)表達(dá)式,而這個(gè)函數(shù)會(huì)在執(zhí)行時(shí)實(shí)際從前面 Execute 保存到 sessionVars.PreparedParams 中獲取,這樣就做到了 Plan 并 Cache 一個(gè)參數(shù)無(wú)關(guān)的 Plan,然后實(shí)際執(zhí)行的時(shí)填充參數(shù)。
新獲取 Plan 后會(huì)保存到 preparedPlanCache 供后續(xù)使用。
命中 Cache讓我們回到 getPhysicalPlan,如果 Cache 命中在獲取 Plan 后我們需要重新 build plan 的 range,因?yàn)榍懊嫖覀儽4娴?Plan 是一個(gè)帶 GetParam 的函數(shù)表達(dá)式,而再次獲取后,當(dāng)前參數(shù)值已經(jīng)變化,我們需要根據(jù)當(dāng)前 Execute 的參數(shù)來(lái)重新修正 range,這部分邏輯代碼位于 Execute#rebuildRange 中,之后就是正常的執(zhí)行過(guò)程了。
文本協(xié)議的 Prepared前面主要介紹了二進(jìn)制協(xié)議的 Prepared 執(zhí)行流程,還有一種執(zhí)行方式是通過(guò)二進(jìn)制協(xié)議來(lái)執(zhí)行。
客戶端可以通過(guò) COM_QUREY 發(fā)送:
PREPARE stmt_name FROM prepareable_stmt; EXECUTE stmt_name USING @var_name1, @var_name2,... DEALLOCTE PREPARE stmt_name
來(lái)進(jìn)行 Prepared,TiDB 會(huì)走正常 文本 Query 處理流程,將 SQL 轉(zhuǎn)換 Prepare,Execute,Deallocate 的 Plan, 并最終轉(zhuǎn)換為和二進(jìn)制協(xié)議一樣的 PrepareExec,ExecuteExec,DealocateExec 的執(zhí)行器進(jìn)行執(zhí)行。
寫(xiě)在最后Prepared 是提高程序 SQL 執(zhí)行效率的有效手段之一。熟悉 TiDB 的 Prepared 實(shí)現(xiàn),可以幫助各位讀者在將來(lái)使用 Prepared 時(shí)更加得心應(yīng)手。另外,如果有興趣向 TiDB 貢獻(xiàn)代碼的讀者,也可以通過(guò)本文更快的理解這部分的實(shí)現(xiàn)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/17888.html
摘要:在事務(wù)提交結(jié)束之后,事務(wù)可能提交成功,也可能提交失敗。需要把這個(gè)狀態(tài)告知如果發(fā)生了,那么輸出的類(lèi)型就為,如果成功提交,那么輸出的類(lèi)型就為。,當(dāng)完成自己所有的狀態(tài)變更之后,會(huì)把的狀態(tài)改為。 作者:姚維 TiDB Binlog Overview 這篇文章不是講 TiDB Binlog 組件的源碼,而是講 TiDB 在執(zhí)行 DML/DDL 語(yǔ)句過(guò)程中,如何將 Binlog 數(shù)據(jù) 發(fā)送給 Ti...
閱讀 2487·2021-11-16 11:45
閱讀 2461·2021-10-11 10:59
閱讀 2264·2021-10-08 10:05
閱讀 3860·2021-09-23 11:30
閱讀 2384·2021-09-07 09:58
閱讀 826·2019-08-30 15:55
閱讀 786·2019-08-30 15:53
閱讀 1932·2019-08-29 17:00