java培訓技術中Mybatis 的 PreparedStatement 預編譯

發布時間:2023年02月07日作者:atguigu瀏覽次數:142

大家都知道,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培訓 大數據培訓 前端培訓

關于尚硅谷
教育理念
名師團隊
學員心聲
資源下載
視頻下載
資料下載
工具下載
加入我們
招聘崗位
崗位介紹
招賢納師
聯系我們
全國統一咨詢電話:010-56253825
地址:北京市昌平區宏??萍紙@2號樓3層(北京校區)

深圳市寶安區西部硅谷大廈B座C區一層(深圳校區)

上海市松江區谷陽北路166號大江商廈3層(上海校區)

武漢市東湖高新開發區東湖網谷(武漢校區)

西安市雁塔區和發智能大廈B座3層(西安校區)

成都市成華區北辰星拱青創園綜合樓3層(成都校區)

夜玩亲女小妍夏令营