1、背景java
MySQL是一箇中小型關係型數據庫管理系統,目前咱們淘寶也使用的也很是普遍。爲了對開發中間DAO持久層的問題能有更深的理解以及最近在使用的phoenix on Hbase的SQL也是實現的JDBC規範,在遇到問題的時候可以有更多的思路,因而研究了一下MySQL_JDBC驅動的源碼,你們都知道JDBC是Java訪問數據庫的一套規範,具體訪問數據庫的細節有各個數據庫廠商本身實現,看驅動實現也有助有咱們更好的理解JDBC規範,而且在這過程當中也發現了一直以來對於PreparedStatement常識理解上的錯誤,與你們分享(MySQl版本5.1.39,JDBC驅動版本5.1.7,JDK版本1.6)。 mysql
2、JDBC典型應用
下面是個最簡單的使用JDBC取得數據的應用。主要能分紅幾個步驟,分別是①加載數據庫驅動,②獲取數據庫鏈接,③建立PreparedStatement,而且設置參數 ④ 執行查詢 ,來分步分析這個過程。基本上每一個步驟的源碼分析我都畫了時序圖,若是不想看文字的話,能夠對着時序圖看。最後我還會分析關於PreparedStatement預編譯的話題,有興趣的同窗能夠仔細看下。sql
Java代碼 數據庫
1. public class PreparedStatement_Select { 編程
2. private Connection conn = null; 數組
3. private PreparedStatement pstmt = null; 緩存
4. private ResultSet rs = null; 服務器
5. private String sql = "SELECT * FROM user WHERE id = ?";網絡
7. public void selectStudent(int id) { 框架
8. try {
9. // step1:加載數據庫廠商提供的驅動程序
10. Class.forName(「 com.mysql.jdbc.Driver 」);
11. } catch (ClassNotFoundException e) {
12. e.printStackTrace();
13. }
15. String url = "jdbc:mysql://localhost:3306/studb";
16. try {
17. // step2:提供數據庫鏈接的URL,經過DriverManager得到數據庫的一個鏈接對象
18. conn = DriverManager.getConnection(url, "root", "root");
19. } catch (SQLException e) {
20. e.printStackTrace();
21. }
23. try {
24. // step3:建立Statement(SQL的執行環境)
25. pstmt = conn.prepareStatement(sql);
26. pstmt.setInt(1, id);
28. // step4: 執行SQL語句
29. rs = pstmt.executeQuery();
31. // step5: 處理結果
32. while (rs.next()) {
33. int i = 1;
34. System.out.print(" 學員編號: " + rs.getInt(i++));
35. System.out.print(", 學員用戶名: " + rs.getString(i++));
36. System.out.print(", 學員密碼: " + rs.getString(i++));
37. System.out.println(", 學員年齡: " + rs.getInt(i++));
38. }
39. } catch (SQLException e) {
40. e.printStackTrace();
41. } finally {
42. // step6: 關閉數據庫鏈接
43. DbClose.close(rs, pstmt, conn);
44. }
45. }
46.}
3、JDBC驅動源碼解析
Java數據庫鏈接(JDBC)由一組用 Java 編程語言編寫的類和接口組成。JDBC 爲工具/數據庫開發人員提供了一個標準的 API,使他們可以用純Java API 來編寫數據庫應用程序。說白了一套Java訪問數據庫的統一規範,以下圖,具體與數據庫交互的仍是由驅動實現,JDBC規範之於驅動的關係,也相似於Servlet規範與Servlet容器(Tomcat)的關係,本質就是一套接口和一套實現的關係。以下類圖所示,咱們平時開發JDBC時熟悉的Connection接口在Mysql驅動中的實現類是com.mysql.jdbc.JDBC4Connection類,PreparedStatement接口在Mysql驅動中的實現類是com.mysql.jdbc.JDBC4Connection, ResultSet接口在Mysql驅動中的實現類是 com.mysql.jdbc.JDBC4ResultSet,下面的源碼解析也是經過這幾個類展開。
1:加載數據庫廠商提供的驅動程序
首先咱們經過Class.forName("com.mysql.jdbc.Driver")來加載mysql的jdbc驅動。 Mysql的com.mysql.jdbc.Driver類實現了java.sql.Driver接口,任何數據庫提供商的驅動類都必須實現這個接口。在DriverManager類中使用的都是接口Driver類型的驅動,也就是說驅動的使用不依賴於具體的實現,這無疑給咱們的使用帶來很大的方便。若是須要換用其餘的數據庫的話,只須要把Class.forName()中的參數換掉就能夠了,能夠說是很是方便的,com.mysql.jdbc.Driver類也是驅動實現JDBC規範的第一步。
Java代碼
1. public class Driver extends NonRegisteringDriver implements java.sql.Driver {
2. static {
3. try {
4. //往DriverManager中註冊自身驅動
5. java.sql.DriverManager.registerDriver(new Driver());
6. } catch (SQLException E) {
7. throw new RuntimeException("Can't register driver!");
8. }
9. }
10. public Driver() throws SQLException {
11. }
12. }
在com.mysql.jdbc.Driver類的靜態初始化塊中會向java.sql.DriverManager註冊它本身 ,註冊驅動首先就是初始化,而後把驅動的信息封裝一下放進一個叫作DriverInfo的驅動信息類中,最後放入一個驅動的集合中, 到此Mysql的驅動類com.mysql.jdbc.Driver也就已經註冊到DriverManager中了。
Java代碼
1. public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
2. if (!initialized) {
3. initialize();
4. }
6. DriverInfo di = new DriverInfo();
8. //把driver的信息封裝一下,組成一個DriverInfo對象
9. di.driver = driver;
10. di.driverClass = driver.getClass();
11. di.driverClassName = di.driverClass.getName();
13. writeDrivers.addElement(di);
14. println("registerDriver: " + di);
16. readDrivers = (java.util.Vector) writeDrivers.clone();
17. }
註冊驅動的具體過程序列圖以下:
2.獲取數據庫鏈接
數據庫鏈接的本質其實就是客戶端維持了一個和遠程MySQL服務器的一個TCP長鏈接,而且在此鏈接上維護了一些信息。
經過 DriverManager.getConnection(url, "root", "root")獲取數據庫鏈接對象時,因爲以前已經在 DriverManager中註冊了驅動類 ,全部會找到那個驅動類來鏈接數據庫com.mysql.jdbc.Driver.connect
Java代碼
1. private static Connection getConnection(
2. String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
3. java.util.Vector drivers = null;
5. if (!initialized) {
6. initialize();
7. }
8. //取得鏈接使用的driver從readDrivers中取
9. synchronized (DriverManager.class){
10. drivers = readDrivers;
11. }
13. SQLException reason = null;
14. for (int i = 0; i < drivers.size(); i++) {
15. DriverInfo di = (DriverInfo)drivers.elementAt(i);
17. if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
18. continue;
19. }
20. try {
21. // 找到可供使用的驅動,鏈接數據庫server
22. Connection result = di.driver.connect(url, info);
23. if (result != null) {
24. return (result);
25. }
26. } catch (SQLException ex) {
27. if (reason == null) {
28. reason = ex;
29. }
}
接着看com.mysql.jdbc.Driver.connect是如何創建鏈接返回數據庫鏈接對象的, 寫法很簡潔,就是建立了一個MySQL的數據庫鏈接對象, 傳入host, port, database等鏈接信息,在com.mysql.jdbc.Connection的構造方法裏面有個createNewIO()方法,主要會作兩件事情,1、創建和MysqlServer的Socket鏈接,2、鏈接成功後,進行登陸校驗, 發送用戶名、密碼、當前數據庫鏈接默認選擇的數據庫名。
Java代碼
1. public java.sql.Connection connect(String url, Properties info)
2. throws SQLException {
3. Properties props = null;
4. try {
5. // 傳入host,port,database等鏈接信息建立數據庫鏈接對象
6. Connection newConn = new com.mysql.jdbc.ConnectionImpl(host(props),
7. port(props), props, database(props), url);
9. return newConn;
10. } catch (SQLException sqlEx) {
11. throw sqlEx;
12. } catch (Exception ex) {
13. throw SQLError.createSQLException(...);
14. }
15. }
繼續往下看ConnectionImpl構造器中的實現,會調用 createNewIO()方法來建立一個MysqlIO對象,維護在Connection中。
Java代碼
1. protected ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info,
2. String databaseToConnectTo, String url)
3. throws SQLException {
4. try {
5. this.dbmd = getMetaData(false, false);
6. //建立MysqlIO對象,創建和MySql服務端的鏈接,而且進行登陸校驗工做
7. createNewIO(false);
8. initializeStatementInterceptors();
9. this.io.setStatementInterceptors(this.statementInterceptors);
10. } catch (SQLException ex) {
11. cleanup(ex);
13. throw ex;
14. }
15. }
16. }
緊接着createNewIO()會建了一個com.mysql.jdbc.MysqlIO,利用com.mysql.jdbc.StandardSocketFactory來建立一個Socket創建與MySQL服務器的鏈接,而後就由這個mySqlIO來與MySql服務器進行握手(doHandshake()),這個doHandshake主要用來初始化與MySQL server的鏈接,負責登錄服務器和處理鏈接錯誤。在其中會分析所鏈接的mysql server的版本,根據不一樣的版本以及是否使用SSL加密數據都有不一樣的處理方式,並把要傳輸給數據庫server的數據都放在一個叫作packet的buffer中,調用send()方法往outputStream中寫入要發送的數據。
Java代碼
1. protected void createNewIO(boolean isForReconnect)
2. throws SQLException {
4. // 建立一個MysqlIO對象,創建與Mysql服務器的Socket鏈接
5. this.io = new MysqlIO(newHost, newPort, mergedProps,
6. getSocketFactoryClassName(), this,
7. getSocketTimeout(),
8. this.largeRowSizeThreshold.getValueAsInt());
10. // 登陸校驗MySql Server, 發送用戶名、密碼、當前數據庫鏈接默認選擇的數據庫名
11. this.io.doHandshake(this.user, this.password,
12. this.database);
14. // 獲取MySql數據庫鏈接的鏈接ID
15. this.connectionId = this.io.getThreadId();
16. this.isClosed = false;
17. }
具體的跟Mysql Server創建鏈接的代碼以下:
Java代碼
1. public MysqlIO(String host, int port, Properties props,
2. String socketFactoryClassName, ConnectionImpl conn,
3. int socketTimeout, int useBufferRowSizeThreshold) throws IOException, SQLException {
4. this.connection = conn;
6. try {
7. // 建立Socket對象,和MySql服務器創建鏈接
8. this.mysqlConnection = this.socketFactory.connect(this.host,
9. this.port, props);
11. // 獲取Socket對象
12. this.mysqlConnection = this.socketFactory.beforeHandshake();
14. //封裝SocketInputStream輸入流
15. if (this.connection.getUseReadAheadInput()) {
16. this.mysqlInput = new ReadAheadInputStream(this.mysqlConnection.getInputStream(), 16384,
17. this.connection.getTraceProtocol(),
18. this.connection.getLog());
19. } else {
20. this.mysqlInput = new BufferedInputStream(this.mysqlConnection.getInputStream(),
21. 16384);
22. }
23. //封裝ScoketOutputStream輸出流
24. this.mysqlOutput = new BufferedOutputStream(this.mysqlConnection.getOutputStream(),
25. 16384);
26. }
具體的跟MySQL Server交互登陸校驗的代碼以下:
Java代碼
1. void secureAuth411(Buffer packet, int packLength, String user,
2. String password, String database, boolean writeClientParams)
3. throws SQLException {
5. // 設置用戶名
6. packet.writeString(user, "utf-8", this.connection);
8. if (password.length() != 0) {
9. packet.writeByte((byte) 0x14);
10. try {
11. // 設置密碼
12. packet.writeBytesNoNull(Security.scramble411(password, this.seed, this.connection));
13. } catch (NoSuchAlgorithmException nse) {
14. }
16. if (this.useConnectWithDb) {
17. // 設置鏈接數據庫名
18. packet.writeString(database, "utf-8", this.connection);
19. }
21. // 向Mysql服務器發送登陸信息包(用戶名、密碼、此Socket鏈接默認選擇的數據庫)
22. send(packet, packet.getPosition());
24. byte savePacketSequence = this.packetSequence++;
26. // 讀取Mysql服務器登陸檢驗後發送的狀態信息,若是成功就返回,若是登陸失敗則拋出異常
27. Buffer reply = checkErrorPacket();
28. }
最終由SocketOutputStream通過一次RPC發送給MySQLServer進行驗證。
Java代碼
1. private final void send(Buffer packet, int packetLen)
2. throws SQLException {
3. try {
4. //把登陸信息的字節流發送給MySQL Server
5. this.mysqlOutput.write(packetToSend.getByteBuffer(), 0,
6. packetLen);
7. this.mysqlOutput.flush();
8. }
9. } catch (IOException ioEx) {
10. throw SQLError.createCommunicationsException(this.connection,
11. this.lastPacketSentTimeMs, this.lastPacketReceivedTimeMs, ioEx);
12. }
13. }
具體的獲取數據庫鏈接的序列圖以下:
3.建立PreparedStatement,並設置參數
當建立完數據庫鏈接以後,就能夠經過conn.prepareStatement(sql) 來獲取SQL執行環境PreparedStatement了,獲取PreparedStatement的邏輯很是簡單,會根據須要編譯的SQL語句和Connection鏈接對象來建立一個JDBC4PreparedStatement對象,也就是相應SQL的執行環境了,具體代碼以下:
Java代碼
1. public java.sql.PreparedStatement prepareStatement(String sql,
2. int resultSetType, int resultSetConcurrency) throws SQLException {
3. checkClosed();
4. PreparedStatement pStmt = null;
6. //須要預編譯的SQL語句
7. String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql): sql;
9. if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {
10. canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
11. }
12. // 建立JDBC4PreapareStatement對象,這個SQL環境中持有預編譯SQL語句及對應的數據庫鏈接對象
13. pStmt = com.mysql.jdbc.PreparedStatement.getInstance(this, nativeSql,
14. this.database);
15. return pStmt;
16. }
當建立完SQL執行環境PreparedStatement對象以後,就能夠設置一些自定義的參數了,最終會把參數值保存在JDBC4PreapareStatement的parameterValues字段,參數類型保存在parameterTypes中,以下代碼:
Java代碼
1. public void setInt(int parameterIndex, int x) throws SQLException {
2. int parameterIndexOffset = getParameterIndexOffset();
4. checkBounds(paramIndex, parameterIndexOffset);
5. byte[] parameterAsBytes = StringUtils.getBytes(String.value(x), this.charConverter,
6. this.charEncoding, this.connection
7. .getServerCharacterEncoding(), this.connection
8. .parserKnowsUnicode());
9. this.parameterStreams[paramIndex - 1 + parameterIndexOffset] = null;
10. //設置參數值
11. this.parameterValues[paramIndex - 1 + parameterIndexOffset] = parameterAsBytes;
12. //設置參數類型
13. this.parameterTypes[parameterIndex - 1 + getParameterIndexOffset()] = Types.INTEGER;
14. }
具體的建立PreparedStatement的序列圖以下:
3.執行查詢
建立完PreparedStatement以後,就一切準備就緒了,就能夠經過 pstmt.executeQuery()來執行查詢了。主要思路是根據SQL模板和設置的參數,解析成一條完整的SQL語句,最後根據MySQL協議,序列化成字節流,RPC發送給MySQL服務端。主要的處理過程以下:
Java代碼
1. public java.sql.ResultSet executeQuery() throws SQLException {
2. checkClosed();
3. ConnectionImpl locallyScopedConn = this.connection;
4. CachedResultSetMetaData cachedMetadata = null;
5. synchronized (locallyScopedConn.getMutex()) {
6. if (doStreaming
7. && this.connection.getNetTimeoutForStreamingResults() > 0) {
8. locallyScopedConn.execSQL(this, "SET net_write_timeout="
9. + this.connection.getNetTimeoutForStreamingResults(),
10. -1, null, ResultSet.TYPE_FORWARD_ONLY,
11. ResultSet.CONCUR_READ_ONLY, false, this.currentCatalog,
12. null, false);
13. }
14. //解析封裝須要發送的sql語句,序列化成MySQL協議對應的字節流
15. Buffer sendPacket = fillSendPacket();
17. if (locallyScopedConn.getCacheResultSetMetadata()) {
18. cachedMetadata = locallyScopedConn.getCachedMetaData(this.originalSql);
19. }
21. Field[] metadataFromCache = null;
23. // 執行sql語句,並獲取MySQL發送過來的結果字節流,根據MySQL協議反序列化成ResultSet
24. this.results = executeInternal(-1, sendPacket,
25. doStreaming, true,
26. metadataFromCache, false);
27.
28. if (oldCatalog != null) {
29. locallyScopedConn.setCatalog(oldCatalog);
30. }
32. }
33. this.lastInsertId = this.results.getUpdateID();
34. return this.results;
35. }
接下來看下 fillSendPacket() 方法怎麼來序列化成二進制字節流的,請看下面的代碼分析
Java代碼
1. protected Buffer fillSendPacket(byte[][] batchedParameterStrings,
2. InputStream[] batchedParameterStreams, boolean[] batchedIsStream,
3. int[] batchedStreamLengths) throws SQLException {
4. // 從connection的IO中獲得發送數據包,首先清空其中的數據
5. Buffer sendPacket = this.connection.getIO().getSharedSendPacket();
6. sendPacket.clear();
8. //數據包的第一位爲一個操做標識符(MysqlDefs.QUERY),表示驅動向服務器發送的鏈接的操做信號,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,這個操做信號並非針 對sql語句操做而言的CRUD操做,從提供的幾種參數來看,這個操做是針對服務器的一個操做。通常而言,使用到的都是MysqlDefs.QUERY,表示發送的是要執行sql語句的操做。
9. sendPacket.writeByte((byte) MysqlDefs.QUERY);
11. boolean useStreamLengths = this.connection
12. .getUseStreamLengthsInPrepStmts();
14. int ensurePacketSize = 0;
15. for (int i = 0; i < batchedParameterStrings.length; i++) {
16. if (batchedIsStream[i] && useStreamLengths) {
17. ensurePacketSize += batchedStreamLengths[i];
18. }
19. }
21. // 判斷這個sendPacket的byte buffer夠不夠大,不夠大的話,按照1.25倍來擴充buffer
22. if (ensurePacketSize != 0) {
23. sendPacket.ensureCapacity(ensurePacketSize);
24. }
26. //遍歷全部的參數。在prepareStatement階段的new ParseInfo()中,驅動曾經對sql語句進行過度割,若是含有以問號標識的參數佔位符的話,就記錄下這個佔位符的位置,依據這個位置把sql分割成多段,放 在了一個名爲staticSql的字符串中。這裏就開始把sql語句進行拼裝,把staticSql和parameterValues進行組合,放在操做符的後面
27. for (int i = 0; i < batchedParameterStrings.length; i++) {
28. if ((batchedParameterStrings[i] == null)
29. && (batchedParameterStreams[i] == null)) {
30. throw SQLError.createSQLException(Messages
31. .getString("PreparedStatement.40") //$NON-NLS-1$
32. + (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS);
33. }
35. //在sendPacket中加入staticSql數組中的元素,就是分割出來的沒有」?」的sql語句,並把字符串轉換成byte
36. sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);
38. if (batchedIsStream[i]) {
39. streamToBytes(sendPacket, batchedParameterStreams[i], true,
40. batchedStreamLengths[i], useStreamLengths);
41. } else {
43. //用batchedParameterStrings,也就是parameterValues來填充參數位置。在循環中,這個操做是跟在staticSql後面的,所以就把第i個參數加到了第i個staticSql段中。參考前面的staticSql的例 子,發現當循環結束的時候,原始sql語句最後一個」?」以前的sql語句就拼成了正確的語句了
44. sendPacket.writeBytesNoNull(batchedParameterStrings[i]);
45. }
46. }
48. // 因爲在原始的包含問號的sql語句中,在最後一個」?」後面可能還有order by等語句,所以staticSql數組中的元素個數必定比參數的個數多1,因此這裏把staticSqlString中最後一段sql語句放sendPacket中
49. sendPacket.writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]);
50. return sendPacket;
51. }
準備好須要發送的MySQL協議的字節流後,就能夠一路經過ConnectionImpl.execSQL--> MysqlIO.sqlQueryDirect --> MysqlIO.send -- >OutPutStram.write把字節流數據經過Socket發送給MySQL服務器,而後線程阻塞等待服務端返回結果數據,拿到數據後再根據MySQL協議反序列化成咱們熟悉的ResultSet對象。
具體執行SQL的序列圖以下:
4、探究MyQL預編譯
一.背景:
如今咱們淘寶持久化大多數是採用iBatis+MySQL作開發的,你們都知道,iBatis內置參數,形如#xxx#的,均採用了sql預編譯的形式,舉例以下:
Xml代碼
1. <select id=」queryUserById」 returnType=」userResult」>
2. SELECT * FROM user WHERE id =#id#
3. </select>
查看日誌後,會發現這個sql執行時被記錄以下,SELECT * FROM user WHERE id = ?
看過iBatis源碼發現底層使用的就是JDBC的PreparedStatement,過程是先將帶有佔位符(即」?」)的sql模板發送至mysql服務器,由服務器對此無參數的sql進行編譯後,將編譯結果緩存,而後直接執行帶有真實參數的sql。查詢了相關文檔及資料後, 基本結論都是,使用預編譯,能夠提升sql的執行效率,而且有效地防止了sql注入。可是一直沒有親自去測試下,趁着最近看MySQL_JDBC的源碼的契機,好好研究測試了下。測試結果出乎意料,發現原來一直以來我對PreparedStatement的理解是有誤的。咱們平時使用的無論是JDBC仍是ORM框架iBatis默認都沒有真正開啓預編譯,形如PreparedStatement( SELECT * FROMuser WHERE id = ? ),每次都是驅動拼好完整帶參數的SQL( SELECT * FROM user WHERE id = 5 ),而後再發送給MySQL服務端,壓根就沒用到如PreparedStatement名字的功能。諮詢了淘寶相關DBA 和相關TDDL同窗,確認瞭如今咱們線上使用的TDDL(JDBC)默認都是沒有打開預編譯的,可是通過測試確實預編譯會快一點,DBA那邊以後會詳細測試並推廣到線上。
接下來我會把探究過程跟你們分享並記錄下。
二.問題:
個人疑問有兩點:1.MySQL是否默認開啓了預編譯功能?若沒有,將如何開啓? 2.預編譯是否能有效提高執行SQL的性能?
三.探究一:MySQL是否默認開啓了預編譯?
首先針對第一個問題。個人電腦上已經安裝了MySQL,版本是5.1.9,打開配置文件my.ini,在"port=3306" 這一行下面加了配置:log=d:/logs/mysql_log.txt,這樣就開啓了MySQL日誌功能,該日誌主要記錄MySQL執行sql的過程。重啓MySQL,並創建一個庫studb,在該庫下建一個叫user的表,有id(主鍵)和username和password三個字段。
接着,我創建了一個簡單的Java工程,引入JDBC驅動包mysql-connector-java-5.0.3-bin.jar。而後寫了以下的代碼:
Java代碼
1. public static void main(String[] args) throws Exception{
2. String sql = "select * from userwhere id= ?";
3. Class.forName("com.mysql.jdbc.Driver");
4. Connection conn = null;
5. try{
6. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/studb?user=root&password=root");
7. PreparedStatement stmt = conn.prepareStatement(sql);
8. stmt.setString(1,5);
9. ResultSet rs = stmt.executeQuery();
10. rs.close();
11. stmt.close();
12. }catch(Exception e){
13. e.printStackTrace();
14. }finally{
15. conn.close();
16. }
17. }
執行這些代碼後,打開剛纔配置的mysql日誌文件mysql_log.txt,日誌記錄以下:
1 Query SET NAMES utf8
1 Query SET character_set_results = NULL
1 Query SHOW VARIABLES
1 Query SHOW WARNINGS
1 Query SHOW COLLATION
1 Query SET autocommit=1
1 Prepare select *from user where id = ?
1 Execute select * from user where id= 5
1 Close stmt
1 Quit
從MySQL日誌能夠清晰看到,server端執行了一次預編譯Prepare及執行了一次Execute,預編譯sql模板爲「select * from user where id= ?」,說明MySQL5.1.19+ mysql-connector-java-5.0.3是默認開啓預編譯的。但仍是有不少疑惑,爲何以前查閱資料,都說開啓預編譯是跟 useServerPrepStmts 參數有關的,因而將剛纔代碼裏的JDBC鏈接修改以下:
Java代碼
1. DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=false")
執行代碼後,再次查看mysql日誌:
1Query SET NAMES utf8
1 Query SET character_set_results = NULL
1 Query SHOW VARIABLES
1 Query SHOW WARNINGS
1 Query SHOW COLLATION
1 Query SET autocommit=1
1 Query select * from user where id= 5
1Quit
果真,日誌沒有了prepare這一行,說明MySQL沒有進行預編譯。這意味着useServerPrepStmts這個參數是起效的,且默認值爲true。
最後意識到useServerPrepStmts這個參數是JDBC的鏈接參數,這說明此問題與JDBC驅動程序可能有關係。打開MySQL官網,發如今線的官方文檔很強大,支持全文檢索,因而我將「useServerPrepStmts」作爲關鍵字,搜索出了一些信息,原文以下:
Important change: Due to a number ofissues with the use of server-side prepared statements, Connector/J5.0.5 has disabled their use by default. The disabling of server-sideprepared statements does not affect the operation of the connector in any way.
To enable server-sideprepared statements, add the following configuration property to your connectorstring:
useServerPrepStmts=true
The default value of thisproperty is false (that is,Connector/J does not use server-side prepared statements)
這段文字說,Connector/J在5.0.5之後的版本,默認useServerPrepStmts參數爲false,Connector/J就是咱們熟知的JDBC驅動程序。看來,若是咱們的驅動程序爲5.0.5或以後的版本,想啓用mysql預編譯,就必須設置useServerPrepStmts=true。個人JDBC驅動用的是5.0.3,這個版本的useServerPrepStmts參數默認值是true。因而我將Java工程中的JDBC驅動程序替換爲5.0.8的版本,去掉代碼裏JDBC鏈接中的useServerPrepStmts參數,再執行,發現mysql_log.txt的日誌打印以下:
2 Query SHOW SESSIONVARIABLES
2 Query SHOW WARNINGS
2 Query SHOW COLLATION
2 Query SET NAMES utf8
2 Query SET character_set_results = NULL
2 Query SET autocommit=1
2 Query select * from user where id= 5
2 Quit
果真,在mysql_log.txt日誌裏,prepare關鍵字沒有了,說明 useServerPrepStmts 參數確實跟JDBC驅動版本有關。另外還查閱了相關MySQL的官方文檔後,發現MySQL服務端是在4.1版本纔開始支持預編譯的,以後的版本都默認支持預編譯。
第一個問題解決了,結論就是:要打開預編譯功能跟MySQL版本及 MySQL Connector/J(JDBC驅動)版本都有關,首先MySQL服務端是在4.1版本以後纔開始支持預編譯的,以後的版本都默認支持預編譯,而且預編譯還與 MySQL Connector/J(JDBC驅動)的版本有關, Connector/J 5.0.5以前的版本默認支持預編譯, Connector/J 5.0.5以後的版本默認不支持預編譯, 因此咱們用的Connector/J 5.0.5驅動之後版本的話默認都是沒有打開預編譯的 (若是須要打開預編譯,須要配置 useServerPrepStmts 參數)
四.探究二:預編譯是否能有效提高執行SQL的性能?
首先,咱們要明白MySQL執行一個sql語句的過程。查了一些資料後,我得知,mysql執行腳本的大體過程以下:prepare(準備)-> optimize(優化)-> exec(物理執行),其中,prepare也就是咱們所說的編譯。前面已經說過,對於同一個sql模板,若是能將prepare的結果緩存,之後若是再執行相同模板而參數不一樣的sql,就能夠節省掉prepare(準備)的環節,從而節省sql執行的成本。明白這一點後,我寫了以下測試程序:
Java代碼
1. public static void main(String []a) throws Exception{
2. String sql = "select * from user whereid = ?";
3. Class.forName("com.mysql.jdbc.Driver");
4. Connection conn = null;
5. try{
6. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/studb?user=root&password=root&useServerPrepStmts=true");
7. PreparedStatement stmt = conn.prepareStatement(sql);
8. stmt.setString(1,5);
9. ResultSet rs1 = stmt.executeQuery(); //第一次執行
10. s1.close();
11. stmt.setString(1,9);
12. ResultSet rs2 = stmt.executeQuery(); //第二次執行
13. rs2.close();
14. stmt.close();
15. }catch(Exception e){
16. e.printStackTrace();
17. }finally{
18. conn.close();
19. }
20. }
執行該程序後,查看mysql日誌:
1Query SHOW SESSION VARIABLES
1 Query SHOW WARNINGS
1 Query SHOW COLLATION
1 Query SET NAMES utf8
1 Query SET character_set_results = NULL
1 Query SET autocommit=1
1 Prepare select * from userwhere id = ?
1 Execute select * from user where id = 5
1 Execute select * from user where id = 9
1 Close stmt
1 Quit
按照日誌看來,PreparedStatement從新設置sql參數後,並無從新prepare,看來預編譯起到了效果。但剛纔咱們使用的是同一個stmt,若是將stmt關閉,從新獲取一個stmt呢?
Java代碼
1. public static void main(String []a) throws Exception{
2. String sql = "select * from userwhere id = ?";
3. Class.forName("com.mysql.jdbc.Driver");
4. Connection conn = null;
5. try{
6. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/studb?user=root&password=root&useServerPrepStmts=true");
7. PreparedStatement stmt = conn.prepareStatement(sql);
8. stmt.setString(1,5);
9. ResultSet rs1 = stmt.executeQuery(); //第一次執行
10. rs1.close();
11. stmt.close();
12. stmt = conn.prepareStatement(sql); //從新獲取一個statement
13. stmt.setString(1,9);
14. ResultSet rs2 = stmt.executeQuery(); //第二次執行
15. rs2.close();
16. stmt.close();
17. }catch(Exception e){
18. e.printStackTrace();
19. }finally{
20. conn.close();
21. }
22. }
mysql日誌打印以下:
1Query SHOW SESSION VARIABLES
1 Query SHOW WARNINGS
1 Query SHOW COLLATION
1 Query SET NAMES utf8
1 Query SET character_set_results = NULL
1 Query SET autocommit=1
1 Prepare select * from user where id=?
1 Execute select * from user where id= 5
1 Close stmt
1 Prepare select *from user where id = ?
1 Execute select * from user where id = 9
1 Close stmt
1 Quit
很明顯,關閉stmt後再執行第二個sql,mysql就從新進行了一次預編譯,這樣是沒法提升sql執行效率的。而在實際的應用場景中,咱們不可能保持同一個statement。那麼,mysql如何緩存預編譯結果呢?
搜索一些資料後得知,JDBC鏈接參數中有另一個重要的參數:cachePrepStmts ,設置爲true後能夠緩存預編譯結果。因而我將測試代碼中JDBC鏈接串改成了這樣:
Java代碼
1. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true&cachePrepStmts=true");
再執行代碼後,發現mysql日誌記錄又變成了這樣:
1 Prepare select * from user where id = ?
1 Execute select * from user where id = 5
1 Execute select * from user where id = 9
OK,如今咱們才正式開啓了預編譯,並開啓了緩存預編譯的功能。那麼接下來咱們對預編譯語句("select * from userwhere id = ?")進行性能測試,測試數據以下:
當不開啓預編譯功能時(String url ="jdbc:mysql://localhost:3306/studb"),作10次測試,100000個select總時間爲(單位毫秒)
12321,12173,12159,12132,12604,12349,12621,12356,12899,12287
(每次查詢一個RPC,每個查詢,都會在mysql server端作一次編譯及一次執行)
Mysql協議:xx xx xx xx QUERY .. .. .. .. .. ..
Mysql協議:xx xx xx xx QUERY .. .. .. .. .. ..
開啓預編譯,但不開啓預編譯緩存時(String url= "jdbc:mysql://localhost:3306/studb?useServerPrepStmts=true"),作10次測試,100000個select總時間爲(單位毫秒)
21349,22860,27237,26848,27772,28100,23114,22897,20010,23211
(每次查詢須要兩個RPC,第一個RPC是編譯,第二個RPC是執行,進測試數據能夠看到這種其實與不打開預編譯相比竟然還慢,由於多了一次RPC,網絡開銷在那裏)
Mysql協議:xx xx xx xx PREPARE .. .. .. .. .. ..
Mysql協議:xx xx xx xx EXECUTE PS_ID ..
Mysql協議:xx xx xx xx PREPARE .. .. .. .. .. ..
Mysql協議:xx xx xx xx EXECUTE PS_ID ..
開啓預編譯,並開啓預編譯緩存時(String url ="jdbc:mysql://localhost:3306/studb?useServerPrepStmts=true&cachePrepStmts=true"),作10次測試,100000個select總時間爲
8732,8655,8678,9693,8624,9874,8444,9660,8607,8780
(第一次兩個RPC,以後都是一個RPC,第一次會由於編譯sql模板走一次RPC,後面都只須要執行一次RPC,在mysql server端不須要編譯,只須要執行)
Mysql協議:xx xx xx xx PREPARE .. .. .. .. .. ..
Mysql協議:xx xx xx xx EXECUTE PS_ID ..
Mysql協議:xx xx xx xx EXECUTE PS_ID ..
從測試結果看來,若開啓預編譯,但不開啓預編譯緩存,查詢效率會有明顯降低,由於須要走屢次RPC,且每一個查詢都須要編譯及執行;開啓預編譯而且打開預編譯緩存的明顯比不打開預編譯的查詢性能好30%左右(這個是本機測試,還須要更多驗證)。
結論:對於Connector/J5.0.5之後的版本,若使用useServerPrepStmts=true開啓預編譯,則必定須要同時使用cachePrepStmts=true 開啓預編譯緩存,不然性能會降低,只有兩者都開啓,纔算是真正開啓了預編譯功能,性能會比不開啓預編譯提高30%左右(這個多是我測試程序的緣由,有待進一步研究)
五.預編譯JDBC驅動源碼剖析
首先對於打開預編譯的URL(String url ="jdbc:mysql://localhost:3306/studb?useServerPrepStmts=true&cachePrepStmts=true")獲取數據庫鏈接以後,本質是獲取預編譯語句pstmt = conn.prepareStatement(sql)時會向MySQL服務端發送一個RPC,發送一個預編譯的SQL模板(驅動會拼接mysql預編譯語句prepare s1 from 'select * fromuser where id = ?'),然會MySQL服務端會編譯好收到的SQL模板,再會爲此預編譯模板語句分配一個serverStatementId發送給JDBC驅動,這樣之後PreparedStatement就會持有當前預編譯語句的服務端的serverStatementId,而且會把此 PreparedStatement緩存在當前數據庫鏈接中,之後對於相同SQL模板的操做pstmt.executeUpdate(),都用相同的PreparedStatement,執行SQL時只須要發送serverStatementId和參數,節省一次SQL編譯, 直接執行。而且對於每個鏈接(驅動端及Mysql服務端)都有本身的preparecache,具體的源碼實現是在com.mysql.jdbc.ServerPreparedStatement中實現。