原創: 鮑鳳奇mysql
本文摘要:git
DBLE 是一款企業級的開源分佈式中間件,江湖人送外號 「MyCat Plus」。Prepared Statement 協議是 MySQL 5.1 版本新加入的功能。MyCat 從1.6版本實現了 Prepared Statement 協議,但 MyCat 存在一些至今仍未修復的Bug。github
本文將從兩名 DBLE 用戶提交的Bug開始提及,詳細解讀 DBLE 是如何實現 Prepared Statement 協議的sql
2019年4月12日下午,GitHub獲得舉報,兩名 DBLE 用戶各發現了一個極爲兇殘的Bug。DBLE 社區片兒警立刻趕到案發現場進行取證並對Bug們開始展開調查。舉報信息以下:數據庫
BugOne(https://github.com/actiontech/dble/issues/1122)後端
error message: Dble has an error message 'unknown pStmtId when executing' when the client set useServerPrepStmts=true #1122 dble version: dble-9.9.9.9-884fc6b612d64cc22101226536f8fd1d24580857-20190221182143緩存
BugTwo(https://github.com/actiontech/dble/issues/1124) error message: use PreparedStatement with JDBC and MySQL J Connector will get wrong result when useCursorFetch=true #1124 dble version: dble 2.18.10.5 and before (not yet test on later version)安全
兩個Bug的錯誤信息雖然不一樣,但相同點是都涉及到了 Prepared Statement 協議,也就是MySQL 的預處理功能。想要完整了解兩個Bug背後的隱情,咱們先要回顧一下 MySQL Prepared Statement 協議以及 DBLE 是如何實現 Prepared Statement 協議的。服務器
MySQL Prepared Statement 協議併發
先看看 MySQL 官方的介紹:
MySQL 5.1 版本開始爲服務器端預處理語句提供支持。此支持利用了高效的客戶端/服務器二進制協議。將帶有佔位符的預準備語句用於參數值具備如下好處:
每次執行時解析語句的開銷更少。一般,數據庫應用程序處理大量幾乎相同的語句,只更改子句中的文字或變量值,例如 WHERE 查詢和刪除,SET 更新和 VALUES 插入。
防止 SQL 注入攻擊。參數值能夠包含未轉義的 SQL 引號和分隔符。
MySQL Prepared Statement 的二進制協議交互過程如圖:
注意:
1. COMSTMTSENDLONGDATA 必須在 COMSTMTEXECUTE 前發送。 2. COMSTMTFETCH 必須在 COMSTMTEXECUTE 後發送。 3. COMSTMTRESET 是專爲重置 COMSTMTSENDLONGDATA ,不能單獨使用。
經過一圖一表,咱們對 MySQL Prepared Statement 協議交互中的各類行爲作了一個回顧。下面讓咱們看看 DBLE 是如何實現的。
DBLE 對 prepare statement 協議的實現
對於客戶端的預編譯 請求,DBLE緩存了 SQL 並模擬返回報文,對於服務端改用 COM_QUERY 命令執行,並將各節點返回數據整合轉換格式返回客戶端。
說明:
1. 在 COMSTMTPREPARE 階段,DBLE 此時接收的 SQL 不完整,不能肯定下發節點,但MySQL Prepared Statement 協議要求此處返回一個 response 報文,所以 DBLE 會假裝COMSTMTPREPAREOK 報文返回。如有些 MySQL 驅動(如 JDBC )想從 response 報文中獲取信息,這些信息會不許確。
2. 在 COMSTMTEXECUTE 階段,DBLE 會根據客戶端傳輸的參數將預編譯SQL替換爲具體的SQL,並使用 COMQUERY 命令下發至後端節點,由於 COMQUERY 再也不使用二進制協議傳輸,所以DBLE須要對後端返回的數據進行轉換後再返回客戶端。
3. DBLE 不支持 COMSTMT_FETCH 命令。
好了,當咱們瞭解完 MySQL 和 DBLE 對 Prepared Statement 協議實現過程後,咱們再回過頭來看那個兩個Bug究竟是怎麼來的。
通過分析,在同一鏈接中,當發起 COMSTMTCLOSE 銷燬當前 prepare statement 後,緊接着又建立一個 prepare statement。這兩個操做是在 DBLE 中是異步進行,存在線程安全的問題。知道問題的根源以後,解決方案是 DBLE 將兩個操做變成同步操做,避免線程安全的問題。
使用 JDBC 時,在URL中設置 useCursorFetch=true 的參數,但願開啓 MySQL Server Side 遊標功能。可是要使用 MySQL Server Side 遊標須要知足下面條件:
必須是SELECT語句
設置了fetchSize > 0
設置了useCursorFetch = true
數據集類型爲ResultSet.TYPEFORWARDONLY
數據集併發設置爲ResultSet.CONCURREADONLY
Server versions 5.0.5 or newer
這是由於 JDBC 在代碼層面作了以下限制:
// we only create cursor-backed result sets if // a) The query is a SELECT // b) The server supports it // c) We know it is forward-only (note this doesn't preclude updatable result sets) // d) The user has set a fetch size if (this.resultFields != null && this.useCursorFetch && getResultSetType() == ResultSet.TYPE_FORWARD_ONLY && getResultSetConcurrency() == ResultSet.CONCUR_READ_ONLY && getFetchSize() > 0) { packet.writeByte(OPEN_CURSOR_FLAG); } else { packet.writeByte((byte) 0); // placeholder for flags }
那如何開啓遊標功能呢?如下是 JDBC 部分功能在進行預處理是開啓遊標的示例:
public static void testPrepareStmt() { Connection conn = null; PreparedStatement stmt = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:8066/poc?useCursorFetch=true", "root", "123456"); stmt = conn.prepareStatement("select long_col_1, long_col_2 from problemTable where to_days(create_time) <= to_days(now()) and id = ?"); stmt.setFetchSize(10); stmt.setInt(1, 2); ResultSet rs = stmt.executeQuery(); int count = 0; while(rs.next()){ ++count; System.out.println("########### row " + count + " ###################"); System.out.println("long_col_1 : " + rs.getString(1)); System.out.println("long_col_2 : " + rs.getString(2)); System.out.println(); } } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException ce) { ce.printStackTrace(); } finally { try { if (stmt != null) { stmt.close(); } if (conn != null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }
1122 和1124 兩個Bug從被定位到抓獲認罪只用了5天,隨後 DBLE 社區發佈 DBLE 2.19.03.0 版本,將真相大白於天下。 咱們始終相信:真相只有一個!至此DBLE又踩平了一個 MyCat 的坑。