下文均基于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日志如下:
通過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日志信息如下:
此時我們看到,開啟了服務器預編譯后,mysql服務器會首先prepare
預編譯
select * from user_info where firstName = ?
語句。
再次實驗以上代碼,看看性能提升了多少:
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();
可以看到此時,針對完全相同的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時,客戶端會以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