您好,歡迎來到賦能網(wǎng)!

java培訓(xùn) Mybati 中的 PreparedStatement 預(yù)編譯

賦能網(wǎng) 2023-06-13 192

大家都知道,Mybatis內(nèi)置參數(shù),形如#{xxx}的,均采用了sql預(yù)編譯的形式,大致知道m(xù)ybatis底層使用PreparedStatement,過程是先將帶有占位符(即”?”)的sql模板發(fā)送至mysql服務(wù)器,由服務(wù)器對此無參數(shù)的sql進(jìn)行編譯后,將編譯結(jié)果緩存,然后直接執(zhí)行帶有真實(shí)參數(shù)的sql。如果你的基本結(jié)論也是如此,那你就大錯(cuò)特錯(cuò)了。

1. mysql是否默認(rèn)開啟了預(yù)編譯功能?

mysql是否支持預(yù)編譯有兩層意思:

  • db是否支持預(yù)編譯
  • 連接數(shù)據(jù)庫的url是否指定了需要預(yù)編譯,比如:jdbc:mysql://127.0.0.1:3306/user?useServerPrepStmts=true,useServerPrepStmts=true是非常非常重要的參數(shù)。如果不配置PreparedStatement 實(shí)際是個(gè)假的 PreparedStatement

SELECt VERSION(); // 5.6.24-log

SHOW GLOBAL STATUS LIKE ‘%prepare%’; //Com_stmt_prepare 4 代表被執(zhí)行預(yù)編譯次數(shù)

//開啟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預(yù)編譯功能有版本要求,包括server版本和mysql.jar包版本。以前的版本默認(rèn)useServerPrepStmts=true,5.0.5以后的版本默認(rèn)useServerPrepStmts=false

2. 預(yù)編譯緩存是服務(wù)端還是客戶端緩存?

開啟緩存:useServerPrepStmts=true&cachePrepStmts=true,設(shè)置了useServerPrepStmts=true,雖然可以一次編譯,多次執(zhí)行

它可以提高性能,但緩存是針對連接的,即每個(gè)連接的緩存都是獨(dú)立的,并且緩存主要是由mysql-connector-java.jar實(shí)現(xiàn)的。

當(dāng)手動調(diào)用prepareStatement.close()時(shí)PrepareStatement對象只會將關(guān)閉狀態(tài)置為關(guān)閉,并不會向mysql發(fā)送關(guān)閉請求,prepareStatement對象會被緩存起來,等下次使用的時(shí)候直接從緩存中取出來使用。沒有開啟緩存,則會向mysql發(fā)送closeStmt的請求。

3. 開啟預(yù)編譯性能更高?

也就是說預(yù)編譯比非預(yù)編譯更好?其實(shí)不然,不行自己可試試看。

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();//非常重要的,一定要調(diào)用才會緩存

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);

}

}

基準(zhǔn)為10w次單線程:

  • 非預(yù)編譯::23.78 s
  • 預(yù)編譯:41.86 s
  • 預(yù)編譯緩存:20.55 s

經(jīng)過實(shí)踐測試,對于頻繁適用的語句,使用預(yù)編譯+緩存確實(shí)能夠得到可觀的提升,但對于不頻繁適用的語句,服務(wù)端編譯會增加額外的round-trip。開發(fā)實(shí)踐中要視情況而定。

4. 從源碼中驗(yàn)證

預(yù)編譯原理(connection -> prepareStatement )

預(yù)編譯:JDBC42ServerPreparedStatement(需將對應(yīng)占位符)

非預(yù)編譯:JDBC42PreparedStatement(完整的SQL)

//com.mysql.jdbc.ConnectionImpl中的代碼片段

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

//當(dāng)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();// 清理上次留下的參數(shù)

}

if (pStmt == null) {

try {// 向Server提交 SQL 預(yù)編譯,實(shí)例是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 預(yù)編譯。

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,實(shí)例是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();// 若開啟緩存,則只會將狀態(tài)位設(shè)為已關(guān)閉,并且刷新緩存

this.isClosed = true;

this.connection.recachePreparedStatement(this);

return;

}

//沒有開啟緩存,則會向mysql發(fā)送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);

}

}

}

java培訓(xùn)  Mybati 中的 PreparedStatement 預(yù)編譯

}

}

5. 總結(jié)

  • 預(yù)編譯顯式開啟(在url中指定useServerPrepStmts=true),否則PreparedStatement不會向mysql發(fā)送預(yù)編譯(Prepare命令)的請求;
  • 每次向mysql發(fā)送預(yù)編譯請求,不管之前有沒有執(zhí)行過此SQL語句,只要請求的命令是Prepare或Query,mysql就會重新編譯一次SQL語句,并返回此鏈接當(dāng)前唯一的Statement ID,后續(xù)執(zhí)行SQL語句的時(shí)候,程序只需拿著Statement ID和參數(shù)就可以了;
  • 當(dāng)預(yù)編譯的SQL語句有語法錯(cuò)誤,則mysql的響應(yīng)會攜帶錯(cuò)誤信息,但此錯(cuò)誤信息JDBC感知不到(或者說mysql-connetor-java.jar包里的實(shí)現(xiàn)將其忽略掉了),此時(shí)還會繼續(xù)往下執(zhí)行代碼,當(dāng)執(zhí)行到executeXxx()方法時(shí),由于沒有Statement ID(所以就會將拼接完整的SQL語句值已經(jīng)將占位符(?)替換掉再次發(fā)給mysql請求執(zhí)行,此時(shí)mysql響應(yīng)有語法錯(cuò)誤,這時(shí)JDBC就會拋出語法錯(cuò)誤異常),所以檢查語法那一步實(shí)在mysql-server中做的(通過抓包可以看到);
  • PreparedStatement對性能的提高是利用緩存實(shí)現(xiàn)的,需要顯式開啟(在url中指定cachePrepStmts=true),此緩存是mysql-connetor-java.jar包里實(shí)現(xiàn)的(非mysql-server中的緩存),緩存的key是完整的sql語句,value是PreparedStatement對象。放入緩存是PreparedStatement.close()觸發(fā)的,所以只要緩存PreparedStatement對象沒有關(guān)閉,你不管調(diào)用多少次connection.prapareStatement(sql)對相同的sql語句進(jìn)行預(yù)編譯,都會將預(yù)編譯的請求發(fā)給mysql,mysql也會對每一個(gè)sql語句不管是否相同進(jìn)行預(yù)編譯,并生成一個(gè)唯一的Statement ID并返回;
  • 緩存是針對鏈接的,每個(gè)鏈接都是獨(dú)立的,不共享緩存

想要了解跟多關(guān)于java培訓(xùn)課程內(nèi)容歡迎關(guān)注賦能網(wǎng)java培訓(xùn),賦能網(wǎng)除了這些技術(shù)文章外還有免費(fèi)的高質(zhì)量大 java培訓(xùn)課程視頻供廣大學(xué)員下載學(xué)習(xí)。


相關(guān): java培訓(xùn)班學(xué)習(xí)后就業(yè)怎么樣——java培訓(xùn)中jvm 的主要組成部分

本文鏈接:

本文章“java培訓(xùn) Mybati 中的 PreparedStatement 預(yù)編譯”已幫助 192 人

免責(zé)聲明:本信息由用戶發(fā)布,本站不承擔(dān)本信息引起的任何交易及知識產(chǎn)權(quán)侵權(quán)的法律責(zé)任!

本文由賦能網(wǎng) 整理發(fā)布。了解更多培訓(xùn)機(jī)構(gòu)》培訓(xùn)課程》學(xué)習(xí)資訊》課程優(yōu)惠》課程開班》學(xué)校地址等機(jī)構(gòu)信息,可以留下您的聯(lián)系方式,讓課程老師跟你詳細(xì)解答:
咨詢熱線:4008-569-579

如果本頁不是您要找的課程,您也可以百度查找一下: