探究mysql預編譯

一.背景:

用Mybatis+mysql的架構做開發,大家都知道,Mybatis內置參數,形如#{xxx}的,均採用了sql預編譯的形式,舉例如下:

Xml代碼  
  1. <select id=”aaa” parameterType=”int” returnType=”Blog”>  
  2.                    select * from blog where id = #{id}  
  3.     </select>  

 

         查看日誌後,會發現這個sql執行時被記錄如下:

         

Sql代碼  收藏代碼
  1. select * from blog where id =?  

 

        之前上網查過一些資料,大致知道mybatis底層使用PreparedStatement,過程是先將帶有佔位符(即”?”)的sql模板發送至mysql服務器,由服務器對此無參數的sql進行編譯後,將編譯結果緩存,然後直接執行帶有真實參數的sql。網上關於這個問題的資料較少,但基本結論是,使用預編譯,可以提高sql的執行效率,並且有效地防止了sql注入。我一直對這個結論深信不疑,直到看了一篇名叫Java中連結MySQL啓用預編譯的先決條件是useServerPstmts=true.”的文章,說mysql是默認不開啓預編譯的,大多數持久層框架(例如mybatis)和jdbc代碼都沒有做到真實開啓預編譯,文章鏈接如下:http://blog.csdn.net/axman/article/details/6913527  ,另外還有一些文章說mysql是不支持預編譯的,總之,衆說紛紜,因爲項目中使用的就是mysql,所以我決定花一些時間來探究一下這個問題。

二.問題:

       我的疑問有兩點:1.mysql是否默認開啓了預編譯功能?若沒有,將如何開啓? 2.預編譯是否能有效地節省sql執行的成本

三.探究一:mysql是否默認開啓預編譯?

       首先針對第一個問題。懶得開linux虛擬機了,我電腦上已經安裝了mysql,版本是5.0.18,打開配置文件my.ini,在“port=3306“這一行下面加了配置:log=d:/logs/mysql50_log.txt,這樣就開啓了mysql日誌功能,該日誌主要記錄mysql執行sql的過程。重啓mysql,並建立一個庫prepare_stmt_test,在該庫下建一個叫users的表,有id(主鍵)和name兩個字段。

         接着,我建立了一個簡單的java工程,引入jdbc驅動包mysql-connector-java-5.0.3-bin.jar。然後寫了如下的代碼:

        

Java代碼  收藏代碼
  1. public static void main(String []a) throws Exception{  
  2.        String sql = "select * from users where name = ?";  
  3.        Class.forName("com.mysql.jdbc.Driver");  
  4.        Connection conn = null;  
  5.        try{  
  6.            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root");  
  7.            PreparedStatement stmt = conn.prepareStatement(sql);  
  8.            stmt.setString(1"aaa");  
  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日誌文件mysql50_log.txt,日誌記錄如下:

    1 Query       SET NAMES utf8

                         1 Query       SET character_set_results = NULL

                         1 Query       SHOW VARIABLES

                         1 Query       SHOW COLLATION

                         1 Query       SET autocommit=1

                         1 Prepare     [1]

                         1 Execute     [1] select * from users where name = 'aaa'

                         1 Quit      

         日誌格式有點奇怪,明明打出了prepare關鍵字,但沒有我設定的預編譯的語句“select * from users where name = ?”,更令人疑惑的是,剛纔說的那篇名叫“Java中連結MySQL啓用預編譯的先決條件是useServerPstmts=true.”的文章裏提到的,若jdbc連接沒有加useServerPrepStmts =truemysql日誌裏連prepare關鍵字都不會記錄。而我的測試結果是,不加useServerPrepStmts =trueprepare關鍵字是有的,但沒有預編譯的sql模板“select * from users where name = ?

         可能是我的mysql版本比較老吧,於是我停掉mysql5.0服務,安裝了mysql5.5,依照剛纔那樣建庫建表,並啓用了一個新的mysql日誌文件mysql55_log.txt。一切OK後,我又一次執行了剛纔的測試程序,然後打開日誌文件mysql55_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 users where name = ?

                       1 Execute          select * from users where name = 'aaa'

                       1 Close stmt   

                       1 Quit      

         終於看到sql模板“select * from users where name = ?”了,但仍然有很多疑惑,首先,剛纔的mysql5.0到底開啓預編譯了嗎?其次,我並沒有加useServerPrepStmts =true配置,但mysql5.5的確是做了預編譯的操作的,這與“Java中連結MySQL啓用預編譯的先決條件是useServerPrepStmts =true.<!--[if !supportNestedAnchors]--><!--[endif]-->”這篇文章的測試結果大相徑庭。

         帶着這些問題,又仔細閱讀了一下CSDN上這篇文章,作者的結論是:jdbc連接mysql時配置useServerPrepStmts參數爲true後才能開啓mysql預編譯功能。看來這個useServerPrepStmts參數是很重要的,於是我將剛纔代碼裏的jdbc連接修改如下:

Java代碼  收藏代碼
  1. DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts =false")  

 

執行代碼後,再次查看mysql日誌:

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 Query    select * from users where name = 'aaa'

130410 15:06:48          1 Quit      

果然,日誌了沒有了prepare這一行,說明mysql沒有進行預編譯。這意味着useServerPrepStmts這個參數是起效的,且默認值爲true。那麼,爲什麼在剛纔那篇文章裏,作者得出的結論是useServerPrepStmts默認爲false呢?

繼續思考了一陣,我突然意識到,useServerPrepStmts這個參數是jdbc的連接參數,這說明此問題與jdbc驅動程序可能有關係。打開mysql官網,發現在線的官方文檔很強大,支持全文檢索,於是我將“useServerPrepStmts”做爲關鍵字,搜索出了一些信息,原文如下:

Important change: Due to a number of issues with the use of server-side prepared statements, Connector/J 5.0.5 has disabled their use by default. The disabling of server-side prepared statements does not affect the operation of the connector in any way.

To enable server-side prepared statements, add the following configuration property to your connector string:

useServerPrepStmts=true

The default value of this property is false (that is, Connector/J does not use server-side prepared statements)

       這段文字說,Connector/J5.0.5以後的版本,默認useServerPrepStmts參數爲falseConnector/J就是我們熟知的jdbc驅動程序。看來,如果我們的驅動程序爲5.0.5或之後的版本,想啓用mysql預編譯,就必須設置useServerPrepStmts=true。我的jdbc驅動用的是5.0.3,這個版本的useServerPrepStmts參數默認值是true。於是我將java工程中的jdbc驅動程序替換爲5.0.8的版本,去掉代碼裏jdbc連接中的useServerPrepStmts參數,再執行,發現mysql5.5的日誌打印如下:

           2 Query         SHOW SESSION VARIABLES

                       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 users where name = 'aaa'

                       2 Quit      

         那麼,mysql5.0呢?我停掉mysql5.5服務,開啓mysql5.0,再執行java代碼,查看mysql5.0的日誌:

         1 Query       SHOW SESSION VARIABLES

                         1 Query       SHOW COLLATION

                         1 Query       SET NAMES utf8

                         1 Query       SET character_set_results = NULL

                         1 Query       SET autocommit=1

                         1 Query       select * from users where name = 'aaa'

                         1 Quit 

         果然,在mysql5.0日誌裏,prepare關鍵字沒有了。mysql5.0的日誌格式和mysql5.5的不太一樣,5.0日誌只打印一個“prepare”關鍵字,而不打印預編譯sql模板。

   第一個問題解決了,結論就是:mysql是否默認開啓預編譯,與MySQL server的版本無關,而與 MySQL Connector/J(驅動程序)的版本有關,Connector/J 5.0.5及以後的版本默認不支持預編譯,Connector/J 5.0.5之前的版本默認支持預編譯。

四.探究二:預編譯是否能有效地節省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 users where name = ?";  
  3.       Class.forName("com.mysql.jdbc.Driver");  
  4.       Connection conn = null;  
  5.       try{  
  6.           conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true");  
  7.           PreparedStatement stmt = conn.prepareStatement(sql);  
  8.           stmt.setString(1"aaa");  
  9.           ResultSet rs1 = stmt.executeQuery();//第一次執行  
  10. s1.close();  
  11.           stmt.setString(1"ddd");  
  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日誌:

1 Query    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 users where name = ?

                       1 Execute          select * from users where name = 'aaa'

                       1 Execute          select * from users where name = 'ddd'

                       1 Close stmt   

                       1 Quit      

按照日誌看來,PreparedStatement重新設置sql參數後,並沒有重新prepare,看來預編譯起到了效果。但剛纔我使用的是同一個stmt,如果將stmt關閉呢?

Java代碼  收藏代碼
  1. public static void main(String []a) throws Exception{  
  2.        String sql = "select * from users where name = ?";  
  3.        Class.forName("com.mysql.jdbc.Driver");  
  4.        Connection conn = null;  
  5.        try{  
  6.            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true");  
  7.            PreparedStatement stmt = conn.prepareStatement(sql);  
  8.            stmt.setString(1"aaa");  
  9.            ResultSet rs1 = stmt.executeQuery();//第一次執行  
  10.            rs1.close();  
  11.            stmt.close();  
  12.            stmt = conn.prepareStatement(sql);//重新獲取一個statement  
  13.            stmt.setString(1"ddd");  
  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日誌打印如下:

1 Query    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 users where name = ?

                       1 Execute          select * from users where name = 'aaa'

                       1 Close stmt   

                       1 Prepare          select * from users where name = ?

                       1 Execute          select * from users where name = 'ddd'

                       1 Close stmt   

                       1 Quit

很明顯,關閉stmt後再執行第二個sqlmysql就重新進行了一次預編譯,這樣是無法提高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 users where name = ?

                           1 Execute          select * from users where name = 'aaa'

                       1 Execute          select * from users where name = 'ddd'

OK,現在我們開啓了預編譯,並開啓了緩存預編譯的功能,那麼開始性能測試。我向剛纔的單表中插入了10000條數據,並做10000次同樣sql模板,不同參數的select。記錄結果如下:

當不開啓預編譯功能時,做5次測試,10000select總時間爲(單位毫秒):

49172,49172,49000,49047,48922

開啓預編譯,但不開啓預編譯緩存時,測試數值如下:

50797,50860,50672,50750,50703

開啓預編譯,開啓預編譯緩存,測試數值如下:

49547,49250,49593,49093,49078.

從測試結果看來,若開啓預編譯,但不開啓預編譯緩存,查詢效率會有明顯下降;但開啓預編譯且開啓預編譯緩存時,查詢效率比不開啓緩存有提高,但和不開啓預編譯基本持平。

結論出來了:對於Connector/J5.0.5以上的版本,若使用useServerPrepStmts=true開啓預編譯,則一定要同時使用cachePrepStmts=true 開啓預編譯緩存,否則性能會下降,若二者都開啓,性能並沒有顯著的提高,這個可能是我測試程序的原因,有待進一步研究。

五.總結:

       經過這一系列的探究,能夠得出一些結論:

首先批一下《Java中連結MySQL啓用預編譯的先決條件是useServerPstmts=true.》這篇文章吧,文章寫得很不科學,作者並沒有關注mysqlConnector/J的版本之間的差異,對於mysql,他說mysql一定支持預編譯,事實上,經過我查詢官方文檔後,得知MySQL Server 4.1之前的版本是不支持預編譯的;對於Connector/J,他也沒有關注5.0.5這個版本節點。所以,雖然被瀏覽轉載了很多次,但這篇文章的結論仍然是錯誤的,應該也誤導了不少人;對於開啓預編譯和預編譯緩存後對性能的影響,這篇文章也沒有涉及,事實上經過我測試,若jdbc驅動是5.0.5及之後的版本,同時開啓預編譯和預編譯緩存,sql的執行性能並沒有顯著提高,若jdbc驅動是5.0.5之前的版本,默認開啓了預編譯,則一定要加cachePrepStmts=true,否則mysql的執行效率會比較低。總之,預編譯和預編譯緩存一定要同時開啓或同時關閉,不同Connector/J的版本,useServerPrepStmts的默認值會有所不同。

再談談SQL預編譯這個東西,其實“預編譯”這個叫法不是很準確,官方文檔裏把它叫做“預準備”。經過我測試,對於mysql,開啓了預編譯緩存後,不同connection之間,預編譯的結果是獨立的,是無法共享的,一個connection無法得到另外一個connection的預編譯緩存結果,對於這一點,我想mysql的開發人員應該會在以後逐步改進吧。再一點,關於預編譯緩存的內容,我查了相關的資料後得知,mysql執行一個預編譯操作後,會將sql模板(即帶佔位符“?”的sql),以及參數列表(模板中用各個佔位符表示)緩存,下一次有同樣的sql模板發來的時候,直接將參數傳給這個模板,拼好後execute。雖然mysql的預編譯功能對性能影響不大,但在jdbc中使用PreparedStatement是必要的,可以有效地防止sql注入,這一點大家都明白。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章