大家都知道,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);
}
}
}
}
}
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