MySql啓動預編譯功能

在Java編程中,應用代碼絕大多數使用了PreparedStatement,不管你是直接使用JDBC仍是使用框架。
    在Java編程中,絕大多數使用了使用了PreparedStatement鏈接MySQL的應用代碼沒有啓用預編譯,不管你是直接使用JDBC仍是使用框架。

java

    在我所能見到的項目中,幾乎沒有見過啓用MySQL預編譯功能的。網上更有文章說MySQL不支持預編譯,實在是貽害不淺。mysql

    要想知道你的應用是否真正的使用了預編譯,請執行:show global status like '%prepare%';看看曾經編譯過幾條,當前Prepared_stmt_count 是多少。大多數是0吧?sql

    這篇文章分如下幾個方面:
    
    一.MySQL是支持預編譯的

    打開MySQL日誌功能,啓動MySQL,而後 tail -f mysql.log.path(默認:/var/log/mysql/mysql.log).
    
    create table axman_test (ID int(4) auto_increment primary key, name varchar(20),age int(4));
    insert into axman_test (name,age) values ('axman',1000);

    prepare myPreparedStmt from 'select * from axman_test where name = ?';    
    set @name='axman';    
    execute myPreparedStmt using @name ;

    控制檯能夠正確地輸出:

mysql> execute myPreparedStmt using @name ;
+----+-------+------+
| ID | name  | age  |
+----+-------+------+
|  1 | axman | 1000 |
+----+-------+------+
1 row in set (0.00 sec)        

    而log文件中也忠實地記錄以下:
    
111028  9:25:06       51 Query    prepare myPreparedStmt from 'select * from axman_test where name = ?'
           51 Prepare    select * from axman_test where name = ?
           51 Query    set @name='axman'
111028  9:25:08       51 Query    execute myPreparedStmt using @name
           51 Execute    select * from axman_test where name = 'axman'



    二.經過JDBC自己是能夠預編譯的,這個不用多說。至關於咱們把控制檯輸入的命令直接經過JDBC語句來執行:

        Class.forName("org.gjt.mm.mysql.Driver");
        String url = "jdbc:mysql://localhost:3306/mysql";
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(url, "root", "12345678");
            Statement stmt = conn.createStatement();
            /*如下忽略返回值處理*/
            stmt.executeUpdate("prepare mystmt from 'select * from axman_test where name = ?'");
            stmt.execute("set @name='axman'");
            stmt.executeQuery("execute mystmt using @name");
            stmt.close();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }

    看日誌輸出:

111028  9:30:19       52 Connect    root@localhost on mysql
           52 Query    /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
           52 Query    /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SELECT @@session.auto_increment_increment
           52 Query    SHOW COLLATION
           52 Query    SET NAMES latin1
           52 Query    SET character_set_results = NULL
           52 Query    SET autocommit=1
           52 Query    SET sql_mode='STRICT_TRANS_TABLES'
           52 Query    prepare mystmt from 'select * from axman_test where name = ?'
           52 Prepare    select * from axman_test where name = ?
           52 Query    set @name='axman'
           52 Query    execute mystmt using @name
           52 Execute    select * from axman_test where name = 'axman'
           52 Quit    數據庫

 

 

    三.默認的PrearedStatement不能開啓MySQL預編譯功能:
       
       雖然第二節中咱們經過JDBC手工指定MySQL進行預編譯,可是PrearedStatement卻並不自動幫咱們作這件事。
        Class.forName("org.gjt.mm.mysql.Driver");
        String url = "jdbc:mysql://localhost:3306/mysql";
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(url, "root", "12345678");
            PreparedStatement ps = conn.prepareStatement("select * from axman_test where name = ?");
            ps.setString(1, "axman' or 1==1");
            ResultSet rs = ps.executeQuery();
            if (rs.next()) {
                System.out.println(rs.getString(1));
            }
            Thread.sleep(1000);
            rs.close();
            ps.clearParameters();
            ps.setString(1, "axman");
            rs = ps.executeQuery();
            if (rs.next()) {
                System.out.println(rs.getString(1));
            }
            rs.close();
            ps.close();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }

    廢話少說,直接看日誌:
111028  9:54:03       53 Connect    root@localhost on mysql
           53 Query    /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
           53 Query    /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SELECT @@session.auto_increment_increment
           53 Query    SHOW COLLATION
           53 Query    SET NAMES latin1
           53 Query    SET character_set_results = NULL
           53 Query    SET autocommit=1
           53 Query    SET sql_mode='STRICT_TRANS_TABLES'
           53 Query    select * from axman_test where name = 'axman\' or 1==1'
111028  9:54:04       53 Query    select * from axman_test where name = 'axman'
           53 Quit    

    兩條語句都是直接執行,而沒有預編譯。注意個人第一條語句select * from axman_test where name = 'axman\' or 1==1',下面還會說到它。
    接着咱們改變一下jdbc.url的選項:
    String url = "jdbc:mysql://localhost:3306/mysql?cachePrepStmts=true&prepStmtCacheSize=25&prepStmtCacheSqlLimit=256";
    執行上面的代碼仍是沒有開啓Mysql的預編譯。


    四.只有使用了useServerPrepStmts=true才能開啓Mysql的預編譯。

    上面的代碼其它不變,只修改String url = "jdbc:mysql://localhost:3306/mysql?useServerPrepStmts=true";
    查看日誌:
    
111028 10:04:52       54 Connect    root@localhost on mysql
           54 Query    /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
           54 Query    /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SELECT @@session.auto_increment_increment
           54 Query    SHOW COLLATION
           54 Query    SET NAMES latin1
           54 Query    SET character_set_results = NULL
           54 Query    SET autocommit=1
           54 Query    SET sql_mode='STRICT_TRANS_TABLES'
           54 Prepare    select * from axman_test where name = ?
           54 Execute    select * from axman_test where name = 'axman\' or 1==1'
111028 10:04:53       54 Execute    select * from axman_test where name = 'axman'
           54 Close stmt    
           54 Quit    

    若是useServerPrepStmts=true,ConneciontImpl在prepareStatement時會產生一個 ServerPreparedStatement.在這個ServerPreparedStatement對象構造時首先會把當前SQL語句發送給 MySQL進行預編譯,而後將返回的結果緩存起來,其中包含預編譯的名稱(咱們能夠當作是當前SQL語句編譯後的函數名),簽名(參數列表),而後執行的 時候就會直接把參數傳給這個函數請求MySQL執行這個函數。不然返回的是客戶端預編譯語句,它僅作參數化工做,見第五節。
    ServerPreparedStatement在請求預編譯和執行預編譯後的SQL 函數時,雖然和咱們上面手工預編譯工做相同,但它與MySQL交互使用的是壓縮格式,如prepare指令碼是22,這樣能夠減小交互時傳輸的數據量。編程

    
    注意上面的代碼中,兩次執行使用的是同一個PreparedStatement句柄.若是使用個不一樣的PreparedStatement句柄,把代碼改爲:
        Class.forName("org.gjt.mm.mysql.Driver");
        String url = "jdbc:mysql://localhost:3306/mysql?useServerPrepStmts=true";
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(url, "root", "12345678");
            PreparedStatement ps = conn.prepareStatement("select * from axman_test where name = ?");
            ps.setString(1, "axman' or 1==1");
            ResultSet rs = ps.executeQuery();
            if (rs.next()) {
                System.out.println(rs.getString(1));
            }
            Thread.sleep(1000);
            rs.close();
            ps.close();
            ps = conn.prepareStatement("select * from axman_test where name = ?");
            ps.setString(1, "axman");
            rs = ps.executeQuery();
            if (rs.next()) {
                System.out.println(rs.getString(1));
            }
            rs.close();
            ps.close();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }    

    再看日誌輸出:
     Connect    root@localhost on mysql
           55 Query    /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
           55 Query    /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SELECT @@session.auto_increment_increment
           55 Query    SHOW COLLATION
           55 Query    SET NAMES latin1
           55 Query    SET character_set_results = NULL
           55 Query    SET autocommit=1
           55 Query    SET sql_mode='STRICT_TRANS_TABLES'
           55 Prepare    select * from axman_test where name = ?
           55 Execute    select * from axman_test where name = 'axman\' or 1==1'
111028 10:10:24       55 Close stmt    
           55 Prepare    select * from axman_test where name = ?
           55 Execute    select * from axman_test where name = 'axman'
           55 Close stmt    
           55 Quit    
           55 Quit
    同一個SQL語句發生了兩次預編譯。這不是咱們想要的效果,要想對同一SQL語句屢次執行不是每次都預編譯,就要使用 cachePrepStmts=true,這個選項可讓JVM端緩存每一個SQL語句的預編譯結果,說白了就是以SQL語句爲key, 將預編譯結果緩存起來,下次遇到相同的SQL語句時做爲key去get一下看看有沒有這個SQL語句的預編譯結果,有就直接合出來用。咱們仍是以事實來講 明:
    上面的代碼只修改String url = "jdbc:mysql://localhost:3306/mysql?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=25&prepStmtCacheSqlLimit=256";
這行代碼中有其它參數本身去讀文檔,我很少囉嗦,執行的結果:
111028 10:27:23       58 Connect    root@localhost on mysql
           58 Query    /* mysql-connector-java-5.1.18 ( Revision: tonci.grgin@oracle.com-20110930151701-jfj14ddfq48ifkfq ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
           58 Query    /* mysql-connector-java-5.1.18 ( Revision: tonci.grgin@oracle.com-20110930151701-jfj14ddfq48ifkfq ) */SELECT @@session.auto_increment_increment
           58 Query    SHOW COLLATION
           58 Query    SET NAMES latin1
           58 Query    SET character_set_results = NULL
           58 Query    SET autocommit=1
           58 Query    SET sql_mode='STRICT_TRANS_TABLES'
           58 Prepare    select * from axman_test where name = ?
           58 Execute    select * from axman_test where name = 'axman\' or 1==1'
111028 10:27:24       58 Execute    select * from axman_test where name = 'axman'
           58 Quit    

    注意僅發生一次預編譯,儘管代碼自己在第一次執行後關閉了ps.close();但由於使用了cachePrepStmts=true,底層並無真實關閉。緩存

    千萬注意,同一條SQL語句儘可能在一個全局的地方定義,而後在不一樣地方引用,這樣作一是爲了DBA方便地對SQL作統一檢查和優化,就象IBatis把 SQL語句定義在XML文件中同樣。二是同一語句不一樣寫法,即便空格不一樣,大小寫不一樣也會從新預編譯,由於JVM端緩存是直接以SQL自己爲key而不會 對SQL格式化之後再作爲key。安全

    咱們來看下面的輸出:session

           35 Prepare    select * from axman_test where name = ?
           35 Execute    select * from axman_test where name = 'axman\' or 1==1'
111029  9:54:31       35 Prepare    select * FROM axman_test where name = ?
           35 Execute    select * FROM axman_test where name = 'axman' oracle

    第一條語句和第二條語句的差異是FROM在第二條語句中被大寫了,這樣仍是發生了兩次預編譯。app

           37 Prepare    select * from axman_test where name = ?
           37 Execute    select * from axman_test where name = 'axman\' or 1==1'
111029  9:59:00       37 Prepare    select * from    axman_test where name = ?
           37 Execute    select * from    axman_test where name = 'axman'
     這裏兩條語句只是第二條的from後面多了個空格,由於你如今看到是HTML格式,若是不加轉義符,兩個空格也顯示一個空格,因此你能可看不到區別,但你能夠在本身的機器上試一下。

    五.即便沒有開啓MySQL的預編譯,堅持使用PreparedStatement仍然很是必要。    在第三節的最後我說到"注意個人第一條語句select * from axman_test where name = 'axman\' or 1==1',下面還會說到它。",如今咱們回過頭來看,即便沒有開啓MySQL端的預編譯,咱們仍然要堅持使用PreparedStatement,由於 JVM端對PreparedStatement的SQL語句進行了參數化,即用佔位符替換參數,之後任何內容輸入都是字符串或其它類型的值,而不會和原始 的SQL語句拚接產生SQL注入,對字符串中的任何字符都會作檢查,若是多是SQL語句使用的標識符,會進行轉義。而後發送一個合法的安全的SQL語句 給數據庫執行。

相關文章
相關標籤/搜索