大家都知道,Mybatis內置參數,形如#{xxx}的,均采用了sql預編譯的形式,大致知道mybatis底層使用PreparedStatement,過程是先將帶有占位符(即”?”)的sql模板發送至mysql服務器,由服務器對此無參數的sql進行編譯后,將編譯結果緩存,然后直接執行帶有真實參數的sql。如果你的基本結論也是如此,那你就大錯特錯了。
1. mysql是否默認開啟了預編譯功能?
mysql是否支持預編譯有兩層意思:
- db是否支持預編譯
- 連接數據庫的url是否指定了需要預編譯,比如:jdbc:mysql://127.0.0.1:3306/user?useServerPrepStmts=true,useServerPrepStmts=true是非常非常重要的參數。如果不配置PreparedStatement 實際是個假的 PreparedStatement
SELECT VERSION(); // 5.6.24-log
SHOW GLOBAL STATUS LIKE ‘%prepare%’; //Com_stmt_prepare 4 代表被執行預編譯次數
//開啟server日志
SHOW VARIABLES LIKE ‘%general_log%’;
SHOW VARIABLES LIKE ‘log_output’;
SET GLOBAL general_log = ON;
SET GLOBAL log_output=’table’;
TRUNCATE TABLE mysql.general_log;
SELECT * FROM mysql.general_log; // 有Prepare命令
注意:mysql預編譯功能有版本要求,包括server版本和mysql.jar包版本。以前的版本默認useServerPrepStmts=true,5.0.5以后的版本默認useServerPrepStmts=false
2. 預編譯緩存是服務端還是客戶端緩存?
開啟緩存:useServerPrepStmts=true&cachePrepStmts=true,設置了useServerPrepStmts=true,雖然可以一次編譯,多次執行
它可以提高性能,但緩存是針對連接的,即每個連接的緩存都是獨立的,并且緩存主要是由mysql-connector-java.jar實現的。
當手動調用prepareStatement.close()時PrepareStatement對象只會將關閉狀態置為關閉,并不會向mysql發送關閉請求,prepareStatement對象會被緩存起來,等下次使用的時候直接從緩存中取出來使用。沒有開啟緩存,則會向mysql發送closeStmt的請求。
3. 開啟預編譯性能更高?
也就是說預編譯比非預編譯更好?其實不然,不行自己可試試看。
public class PreparedStatement_test {
private String url = “jdbc:mysql://localhost:3306/batch”;
private String sql = “SELECT * FROM export_request WHERE id = ?”;
private int maxTimes = 100000;
@Test
public void go_driver() throws SQLException, ClassNotFoundException {
Class.forName(“com.mysql.jdbc.Driver”);
Connection conn = (Connection) DriverManager.getConnection(url, “root”, “123456”);
// PreparedStatement
Stopwatch stopwatch = Stopwatch.createStarted();
for (int i = 0; i < maxTimes; i++) {
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setLong(1, Math.abs(new Random().nextLong()));
// execute
stmt.executeQuery();
}
System.out.println(“go_driver:” + stopwatch);
}
@Test
public void go_setPre() throws SQLException, ClassNotFoundException {
Class.forName(“com.mysql.jdbc.Driver”);
Connection conn = (Connection) DriverManager.getConnection(url + “?useServerPrepStmts=true”, “root”, “123456”);
// PreparedStatement
Stopwatch stopwatch = Stopwatch.createStarted();
for (int i = 0; i < maxTimes; i++) {
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setLong(1, Math.abs(new Random().nextLong()));
// execute
stmt.executeQuery();
}
System.out.println(“go_setPre:” + stopwatch);
}
@Test
public void go_setPreCache() throws SQLException, ClassNotFoundException {
Class.forName(“com.mysql.jdbc.Driver”);
Connection conn = (Connection) DriverManager.getConnection(url + “?useServerPrepStmts=true&cachePrepStmts=true”, “root”, “123456”);
// PreparedStatement
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setLong(1, Math.abs(new Random().nextLong()));
// execute
stmt.executeQuery();
stmt.close();//非常重要的,一定要調用才會緩存
Stopwatch stopwatch = Stopwatch.createStarted();
for (int i = 0; i < maxTimes; i++) {
stmt = conn.prepareStatement(sql);
stmt.setLong(1, Math.abs(new Random().nextLong()));
// execute
stmt.executeQuery();
}
System.out.println(“go_setPreCache:” + stopwatch);
}
}
基準為10w次單線程:
- 非預編譯::23.78 s
- 預編譯:41.86 s
- 預編譯緩存:20.55 s
經過實踐測試,對于頻繁適用的語句,使用預編譯+緩存確實能夠得到可觀的提升,但對于不頻繁適用的語句,服務端編譯會增加額外的round-trip。開發實踐中要視情況而定。
4. 從源碼中驗證
預編譯原理(connection -> prepareStatement )
預編譯:JDBC42ServerPreparedStatement(需將對應占位符)
非預編譯:JDBC42PreparedStatement(完整的SQL)
//com.mysql.jdbc.ConnectionImpl中的代碼片段
/**
* JDBC 2.0 Same as prepareStatement() above, but allows the default result
* set type and result set concurrency type to be overridden.
* @param sql
* the SQL query containing place holders
* @param resultSetType
* a result set type, see ResultSet.TYPE_XXX
* @param resultSetConcurrency
* a concurrency type, see ResultSet.CONCUR_XXX
* @return a new PreparedStatement object containing the pre-compiled SQL
* statement
* @exception SQLException
* if a database-access error occurs.
*/
public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
synchronized (getConnectionMutex()) {
checkClosed();
//
// FIXME: Create warnings if can’t create results of the given type or concurrency
//當Client開啟 useServerPreparedStmts 并且Server支持 ServerPrepare
PreparedStatement pStmt = null;
boolean canServerPrepare = true;
String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql) : sql;
if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {
canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
}
if (this.useServerPreparedStmts && canServerPrepare) {// 從緩存中獲取 pStmt
if (this.getCachePreparedStatements()) {
synchronized (this.serverSideStatementCache) {
pStmt = (com.mysql.jdbc.ServerPreparedStatement) this.serverSideStatementCache
.remove(makePreparedStatementCacheKey(this.database, sql));
if (pStmt != null) {
((com.mysql.jdbc.ServerPreparedStatement) pStmt).setClosed(false);
pStmt.clearParameters();// 清理上次留下的參數
}
if (pStmt == null) {
try {// 向Server提交 SQL 預編譯,實例是JDBC42ServerPreparedStatement
pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType,
resultSetConcurrency);
if (sql.length() < getPreparedStatementCacheSqlLimit()) {
((com.mysql.jdbc.ServerPreparedStatement) pStmt).isCached = true;
}
pStmt.setResultSetType(resultSetType);
pStmt.setResultSetConcurrency(resultSetConcurrency);
} catch (SQLException sqlEx) {
// Punt, if necessary
if (getEmulateUnsupportedPstmts()) {
pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
if (sql.length() < getPreparedStatementCacheSqlLimit()) {
this.serverSideStatementCheckCache.put(sql, Boolean.FALSE);
}
} else {
throw sqlEx;
}
}
}
}
} else {
try { // 向Server提交 SQL 預編譯。
pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);
pStmt.setResultSetType(resultSetType);
pStmt.setResultSetConcurrency(resultSetConcurrency);
} catch (SQLException sqlEx) {
// Punt, if necessary
if (getEmulateUnsupportedPstmts()) {
pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
} else {
throw sqlEx;
}
}
}
} else {// Server不支持 ServerPrepare,實例是JDBC42PreparedStatement
pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
}
return pStmt;
}
}
JDBC42ServerPreparedStatement->close,緩存
//com.mysql.jdbc.ServerPreparedStatement中選取代碼
@Override
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;
}
//沒有開啟緩存,則會向mysql發送closeStmt的請求
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) {// 將sql語句作為key,reparedStatement對象作為value存放到緩存中
((ServerPreparedStatement) oldServerPrepStmt).isCached = false;
((ServerPreparedStatement) oldServerPrepStmt).realClose(true, true);
}
}
}
}
}
5. 總結
- 預編譯顯式開啟(在url中指定useServerPrepStmts=true),否則PreparedStatement不會向mysql發送預編譯(Prepare命令)的請求;
- 每次向mysql發送預編譯請求,不管之前有沒有執行過此SQL語句,只要請求的命令是Prepare或Query,mysql就會重新編譯一次SQL語句,并返回此鏈接當前唯一的Statement ID,后續執行SQL語句的時候,程序只需拿著Statement ID和參數就可以了;
- 當預編譯的SQL語句有語法錯誤,則mysql的響應會攜帶錯誤信息,但此錯誤信息JDBC感知不到(或者說mysql-connetor-java.jar包里的實現將其忽略掉了),此時還會繼續往下執行代碼,當執行到executeXxx()方法時,由于沒有Statement ID(所以就會將拼接完整的SQL語句值已經將占位符(?)替換掉再次發給mysql請求執行,此時mysql響應有語法錯誤,這時JDBC就會拋出語法錯誤異常),所以檢查語法那一步實在mysql-server中做的(通過抓包可以看到);
- PreparedStatement對性能的提高是利用緩存實現的,需要顯式開啟(在url中指定cachePrepStmts=true),此緩存是mysql-connetor-java.jar包里實現的(非mysql-server中的緩存),緩存的key是完整的sql語句,value是PreparedStatement對象。放入緩存是PreparedStatement.close()觸發的,所以只要緩存PreparedStatement對象沒有關閉,你不管調用多少次connection.prapareStatement(sql)對相同的sql語句進行預編譯,都會將預編譯的請求發給mysql,mysql也會對每一個sql語句不管是否相同進行預編譯,并生成一個唯一的Statement ID并返回;
- 緩存是針對鏈接的,每個鏈接都是獨立的,不共享緩存
上一篇: 尚硅谷九章云臺項目視頻發布
下一篇: java程序員培訓學習中自身需要什么怎么做