JDBC使用PrepareStatement對性能的提升分析

下文均基于mysql-connector-java-5.1.43, mysql server version 5.6版本進行分析。

從剛開始接觸JDBC開始,就學到使用PrepareStatement對sql進行預編譯,不用每次語句都進行一次重新sql解析和編譯,相較于使用Statement能夠提高程序的性能,那么到底是用PrepareStatement對性能的提升有多大呢?

通過示例代碼:

import java.sql.*;

/**
 * Created by ZHUKE on 2017/8/18.
 */
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1/test", "root", "root");
        String prepareSql = "select * from user_info where firstName = ?";
        PreparedStatement preparedStatement = conn.prepareStatement(prepareSql);

        Statement statement = conn.createStatement();
        String statementSql = "select * from user_info where firstName= 'zhuke'";

        long nowTime = System.currentTimeMillis();

        int count = 100000;
        for (int i = 0; i < count; i++) {
            preparedStatement.setString(1, "zhuke");
            preparedStatement.execute();
        }
        long nowTime1 = System.currentTimeMillis();
        System.out.println("preparedStatement execute " + count + " times consume " + (nowTime1 - nowTime) + " ms");

        long nowTime2 = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            statement.execute(statementSql);
        }
        long nowTime3 = System.currentTimeMillis();
        System.out.println("statement execute " + count + " times consume " + (nowTime3 - nowTime2) + " ms");

    }
}

執行同樣的語句100000次,得到的結果如下:

測試結果

14588 : 14477,這就是我一直深信的性能提升???

一定是哪里出了問題,通過查找資料知道,PrepareStatement會將帶有參數占位符?的sql語句提交到mysql服務器,服務器會對sql語句進行解析和編譯,將編譯后的sql id返回給客戶端,客戶端下次值需要將參數值和sql id發送到服務器即可。以此節省了服務器多次重復編譯同一sql語句的開銷,而且因為不用每次都發送完整sql內容,也一定程度上節省了網絡開銷。

那么為什么以上代碼中,PrepareStatement沒有實現性能提升呢?
通過開啟mysql的詳細日志,對PrepareStatement的執行來一探究竟。

preparedStatement.setString(1, "zhuke");
preparedStatement.execute();

mysql日志如下:

PrepareStatement執行mysql日志

通過mysql日志我們可以看到,通過PrepareStatement的方式,每次執行發送給mysql服務器的依然是完整的參數拼接完成后的sql語句,并沒有利用到上述的服務器預編譯的特性。

通過mysql-connector-java(5.1.43版本)連接驅動的源碼來查找原因。

public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
    synchronized (getConnectionMutex()) {
        ……

        if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {
            canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
        }
        //如果useServerPreparedStmts配置為true,且服務器支持sql預編譯優化,則執行服務器sql優化
        if (this.useServerPreparedStmts && canServerPrepare) {
            if (this.getCachePreparedStatements()) {
                synchronized (this.serverSideStatementCache) {
                    ……
        } else {//否則執行本地預編譯
            ……
        }

        return pStmt;
    }
}

服務器支持預編譯的情況下,那么就只由useServerPreparedStmts 控制是否進行服務器預編譯了。而從源碼中又知道其默認值為false。那么如果不顯式配置useServerPreparedStmts =true,就不會進行服務器預編譯,而只執行本地預編譯。

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).
通過查找MySQL官網發現,驅動文件在版本 5.0.5后將設為了false,所以需要手動指定和開啟服務器預編譯功能。
https://dev.mysql.com/doc/relnotes/connector-j/5.1/en/news-5-0-5.html

通過在url鏈接中添加參數useServerPreparedStmts =true開啟服務器預編譯。
現在我們看到mysql日志信息如下:

useServerPreparedStmts =true時mysql日志信息

此時我們看到,開啟了服務器預編譯后,mysql服務器會首先prepare
預編譯

select * from user_info where firstName = ?

語句。

再次實驗以上代碼,看看性能提升了多少:

開啟useServerPreparedStmts 后執行結果

13312 : 14535,性能提升了8.4%.

與之對應的還有一個參數:cachePrepStmts表示服務器是否需要緩存prepare預編譯對象。

// 關閉cachePrepStmts時新建兩個preparedStatement 
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1/test?useServerPrepStmts=true", "root", "root");
String prepareSql = "select * from user_info where firstName = ?";
PreparedStatement preparedStatement = conn.prepareStatement(prepareSql);

preparedStatement.setString(1, "zhuke");
preparedStatement.execute();
preparedStatement.close();

preparedStatement = conn.prepareStatement(prepareSql);
preparedStatement.setString(1, "zhuke1");
preparedStatement.execute();
preparedStatement.close();
關閉cachePrepStmts時新建兩個preparedStatement

可以看到此時,針對完全相同的sql語句,服務器進行了兩次預編譯過程。

那么當我們開啟cachePrepStmts的時候呢?

// 關閉cachePrepStmts時新建兩個preparedStatement 
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1/test?useServerPrepStmts=true&cachePrepStmts=true", "root", "root");
String prepareSql = "select * from user_info where firstName = ?";
PreparedStatement preparedStatement = conn.prepareStatement(prepareSql);

preparedStatement.setString(1, "zhuke");
preparedStatement.execute();
preparedStatement.close();

preparedStatement = conn.prepareStatement(prepareSql);
preparedStatement.setString(1, "zhuke1");
preparedStatement.execute();
preparedStatement.close();
開啟開啟cachePrepStmts時的mysql日志

可以看到,開啟cachePrepStmts時,mysql服務器只進行了一次預編譯過程。

通過閱讀源碼發現,當開啟cachePrepStmts時,客戶端會以sql語句作為鍵,預編譯完成后的對象PrepareStatement作為值,保存在Map中,以便下次可以重復利用和緩存。

//prepareStatement關閉時,將對象存入緩存中
public void close() throws SQLException {
        MySQLConnection locallyScopedConn = this.connection;

        if (locallyScopedConn == null) {
            return; // already closed
        }

        synchronized (locallyScopedConn.getConnectionMutex()) {
            if (this.isCached && isPoolable() && !this.isClosed) {
                clearParameters();
                this.isClosed = true;
                //緩存預編譯對象
                this.connection.recachePreparedStatement(this);
                return;
            }

            realClose(true, true);
        }
    }


public void recachePreparedStatement(ServerPreparedStatement pstmt) throws SQLException {
        synchronized (getConnectionMutex()) {
            if (getCachePreparedStatements() && pstmt.isPoolable()) {
                synchronized (this.serverSideStatementCache) {
                    Object oldServerPrepStmt = this.serverSideStatementCache.put(makePreparedStatementCacheKey(pstmt.currentCatalog, pstmt.originalSql), pstmt);
                    if (oldServerPrepStmt != null) {
                        ((ServerPreparedStatement) oldServerPrepStmt).isCached = false;
                        ((ServerPreparedStatement) oldServerPrepStmt).realClose(true, true);
                    }
                }
            }
        }
    }


結論

使用mysql的預編譯對象PrepateStatement時,一定需要設置useServerPrepStmts=true開啟服務器預編譯功能,設置cachePrepStmts=true開啟客戶端對預編譯對象的緩存。

參考資料:
https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html
http://www.cnblogs.com/justfortaste/p/3920140.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容