1.概述
prepared statement在MySQL4.1中引進並且增加了一些新的命令:
- COM_STMT_PREPARE
- COM_STMT_EXECUTE
- COM_STMT_CLOSE
- COM_STMT_RESET
- COM_STMT_SEND_LONG_DATA
它還定義了一個更緊湊簡潔的結果集格式代替ProtocolText::Resultset來返回結果集。
記住不是所有的語句都是可以預處理的:
1.1 預處理說明
sql預處理首先要求客戶端提交需要執行的sql,這時提交的sql不傳遞真實的參數值,參數以問號的形式傳遞過去。
eg:
insert into user(id, name) values(?, ?); select * from user where id = ?;
在mysql服務端完成預編譯解析,這里的預編譯解析包括解析參數的個數、類型等,然后響應給客戶端。接下來客戶端第二次交互只需傳遞參數過去就可以完成一個完整的sql的執行。相比傳統的sql執行,預處理需要兩次交互,才能完成一次sql執行。
預處理的優勢:
(1)預處理sql能一定程度上防止sql注入
(2)sql預編譯效率更高
(3)二進制包協議讓sql預處理更加高效。
mysql預處理命令參數的封裝以及結果集的返回,均采用二進制格式封裝數據,體積更小,面向底層,能直接被mysql服務端利用。相比普通sql文本協議傳輸的數據,二進制協議傳輸數據更加高效。
2.二進制協議結果集
二進制協議結果集類似ProtocolText::Resultset.它僅包含二進制協議結果集行格式。
ProtocolBinary::Resultset:
Packet:
- lenenc_int column_cout > 0
- column_count * Protocol::ColumnDefinition
- 沒有或者很多ProtocolBinary::ResultsetRow
- EOF_Packet
注意:如果CLIENT_DEPRECATE_EOF客戶端性能標志被設置,發送OK_Packet,否則發送EOF_Packet。
例如:
3. 二進制協議結果集行
3.1 NULL-Bitmap
二進制協議結果集行由NULL位圖組成,該位圖包含與結果集+ 2中的列一樣多的位以及二進制協議值格式中非NULL的列的值。
ProtocolBinary::ResultsetRow:
3.2 二進制結果集的行(COM_STMT_EXECUTE)
payload:
1 packet header[00]
string[$len] NULL-bitmap,length: (列數 + 7 + 2 )/8
string[$len] values
例子:
3.2.1 NULL-Bitmap
二進制協議將NULL值作為位發送到位圖內而不是像ProtocolText :: ResultsetRow那樣發送完整字節。 如果發送了許多NULL值,則它比舊方法更有效。
警告:
對於二進制協議結果集行,num-fields和field-pos需要添加2的偏移量。對於COM_STMT_EXECUTE,此偏移量為0。
NULL位圖需要足夠的空間來為發送的每個列存儲可能的NULL位。 其空間計算如下:
NULL-bitmap-bytes = (num-fields + 7 + offset) / 8
導致:
4. 預處理命令說明
4.1 COM_STMT_PREPARE
COM_STMT_PREPARE命令用於客戶端往服務端提交一個預處理的sql,如上面提到的:
1 insert into user(id, name) values(?, ?);
4.2 COM_STMT_EXECUTE
COM_STMT_EXECUTE用於執行預處理sql,正如前面說到的,如果預處理sql需要傳遞參數,這個命令會發送預處理語句所需要的參數到服務端。如上面的例子,需要傳遞兩個參數id和user的具體值到服務端。
4.3 COM_STMT_CLOSE
COM_STMT_CLOSE用於關閉服務端預處理sql,每一個預處理預處理的sql提交后都保存在mysql服務端的內存當中,每個預處理sql都有一個唯一的id標識,這個命令將發送需要關閉的sql的id,通知服務端可以將所有該預處理sql的資源釋放掉(過多的預處理sql保留在服務端會占用較多的內存,因此有必要執行該命令清理無用的預處理sql)。
4.4 COM_STMT_RESET
COM_STMT_RESET命令用於重置COM_STMT_SEND_LONG_DATA命令發送的blob數據。
4.5 COM_STMT_SEND_LONG_DATA
COM_STMT_SEND_LONG_DATA用於往服務端發送字節流數據,通常來說只有在發送blob字段數據才用到該命令。可以多次調用該命令連續傳同一個字段的字節的數據,這個命令必須在COM_STMT_EXECUTE命令發送之前執行。
5. 預處理協議結果包說明
mysql預處理結果集采用了二進制協議包進行封裝,與普通的查詢結果集格式不同。(普通的結果集包采用文本協議包進行封裝)。
5.1 普通查詢結果集協議包
普通sql查詢(相比預處理sql查詢)返回的結果集包用文本協議(官方稱為Text Protocol)封裝。文本協議的結果集包格式根據官網的一個圖來說明:
一個結果集包主要包括以下部分(順序傳輸):
- one pakcet show field count(第一個packet用於表示返回結果集列數)
- column defines packets(一個列就是一個packet, 格式參考Column Define Pakcet)
- EOF Packet
- row packets(一行數據就是一個packet, 格式參考ResultsetRow Packet)
- EOF Packet
5.2 預處理結果集協議包
預處理結果集包的組成和普通協議包類似,區別只在於row packet(數據以二進制協議格式存放)。
- one packet show field count(第一個packet用於表示返回結果集列數)
- column define packets(一列就是一個packet,格式參考普通協議包的Column Define Packet)
- EOF Packet
- binary row packets(一行數據一個packet,格式參考Binary Row Pakcet)
- EOF Packet
說明:
Binary Row Packet的第一個字節恆為0, 表示paket header, 接下來,由NULL-Bitmap標識那些值為NULL的列,NULL-Bitmap的長度計算方式為(column-count +7 + 2)/8,其中column-count表示列數,而非空的列值以二進制協議格式(協議格式參考Binary Protocol Value)順序存儲在NULL-Bitmap的后面。
提示:
返回相同結果行,預處理協議包所占字節比普通協議包小,在列數越多,列越長的情況下,相差的大小越明顯。
5.3 mysql jdbc 預處理
java.sql.preparestatement可以執行預處理sql,mysql jdbc實現了該接口,並且將預處理分為客戶端和服務端預處理
5.3.1 jdbc客戶端預處理
mysql jdbc默認情況下采用的就是客戶端預處理。客戶端預處理的意思是,所有預處理參數都將被緩存在mysql jdbc層,而不是緩存在mysql server。在PrepareStatement執行的時候,在jdbc端完成sql語句的拼接(主要是使用緩存的參數對sql中問號?進行替換, 最終發送到mysql的就是完整的sql語句)。客戶端預處理走得是普通的查詢協議,而不是真正的mysql預處理協議。
6 Mycat預處理實現機制
Mycat也實現了mysql的預處理協議,可以接收預處理命令的處理。當使用預處理查詢,也可以返回正確的二進制結果集包。Mycat預處理的實現是一種取巧的設計,查詢走到后端mysql實際上不是發送了預處理命令,而是普通的COM_QUERY命令,后端mysql返回給Mycat的結果集包也是文本協議包,只是在Mycat將結果集包發送往客戶端的中間過程,將普通的文本協議結果集包包裝成為二進制協議結果集包,然后再返回給客戶端。
Mycat預處理的處理流程:
(1)Mycat接收到客戶端發送的COM_STMT_PREPARE命令后,解析協議包的內容得到預處理sql語句,eg:insert into user(id, name)value(?, ?),將這些預處理語句緩存在Mycat里面;
(2)當Mycat再次接收到客戶端發送的COM_STMT_EXECUTE命令,就把相應的問號替換為實際傳遞過來的參數值,這時候已經得到了完整的sql語句。
(3)接下來,直接把這個語句丟給Mycat sql查詢處理器去執行,中間會經過sql解析模塊,路由解析模塊以及最后的執行。
(4)最后,當收到后端mysql傳遞給Mycat的數據准備發往客戶端的時候,做一個協議轉換,將普通文本結構集協議包轉換為二進制結果集協議包並發往客戶端。