*************************************優雅的分割線 **********************************
分享一波:程序員賺外快-必看的巔峰干貨
如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程
請關注微信公眾號:HB荷包
一個能讓你學習技術和賺錢方法的公眾號,持續更新
*************************************優雅的分割線 **********************************
SimpleExecutor
接上一章博客繼續。
SImpleExecutor繼承了BaseExecutor類,是最簡單的Executor實現。Executor使用了模板方法模式,所以SimpleExecutor不必在關心一級緩存等操作,只需要實現基本的4個方法。
首先看doQuery
/*** 執行查詢操作* @param ms* @param parameter* @param rowBounds* @param resultHandler* @param boundSql* @param <E>* @return* @throws SQLException*/
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {// 獲取配置對象Configuration configuration = ms.getConfiguration();// 創建RoutingStatementHandler,根據MappedStatement.statementType決定選擇具體的StatementHandlerStatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 完成Statement的初始化。先創建對應的StatementHandler,再調用parameterize方法處理占位符stmt = prepareStatement(handler, ms.getStatementLog());// 調用StatementHandler.query方法執行SQLreturn handler.query(stmt, resultHandler);} finally {closeStatement(stmt);}
}/*** 創建Statement并處理占位符* @param handler* @param statementLog* @return* @throws SQLException*/
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;Connection connection = getConnection(statementLog);// 創建Statementstmt = handler.prepare(connection, transaction.getTimeout());// 處理占位符handler.parameterize(stmt);return stmt;
}
代碼很簡單,先獲取配置對象,再創建RoutingStatementHandler對象,對Statement進行初始化,最后調用query方法完成查詢操作。doQueryCursor、update方法與之類似,不進行介紹。SimpleExecutor不提供批量處理SQL的功能所以doFlushStatement方法直接返回空集合。
ReuseExecutor
傳統的JDBC編程中,Statement對象重用是最常見的一種優化手段,這樣可以減少SQL的預編譯以及創建、銷毀Statement對象的開銷,從而提高性能。
ReuseExecutor提供了Statement重用的功能,通過statementMap字段緩存使用過的Statement對象,key是sql,value是statement。
該類中的doQuery、doQueryCursor、doUpdate與SimpleExecutor中的實現一樣, 不同的是PrepareStatement方法。SimpleExecutor每次都會通過JDBC的Connection創建新的Statement,而ReuseExecutor則先嘗試從statementMap中查找緩存的對象。
/*** 默認會從緩存中查找Statement* @param handler* @param statementLog* @return* @throws SQLException*/
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;BoundSql boundSql = handler.getBoundSql();// 獲取SQLString sql = boundSql.getSql();if (hasStatementFor(sql)) {stmt = getStatement(sql);// 修改事務超時時間applyTransactionTimeout(stmt);} else {// 獲取數據庫連接Connection connection = getConnection(statementLog);// 創建新的Statement放到statementMapstmt = handler.prepare(connection, transaction.getTimeout());putStatement(sql, stmt);}handler.parameterize(stmt);return stmt;
}
當事務提交、回滾、連接關閉時,需要銷毀這些緩存的Statement對象。在BaseExecutor中commit、rollback、close方法中,都會調用doFlushStatement方法,所以在doFlushStatement方法中關閉Statement比較合適。方法實現如下。
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) {for (Statement stmt : statementMap.values()) {closeStatement(stmt);}statementMap.clear();return Collections.emptyList();
}
BatchExecutor
系統在執行一條sql語句的時候,會將SQQL語句以及相關參數通過網絡發送到數據庫。對于頻繁操作數據庫的系統,如果執行一條SQl就向數據庫發送一次請求,在網絡通信上會有很多性能折損。因此使用批量處理的方式進行優化,緩存多條SQL語句,在合適的時機將多條SQL打包發送給數據庫執行,從而減少網絡方面的開銷,提高性能。
需要注意,批量執行SQL的時候,每次向數據庫發送的SQL語句條數是有上限的,如果超出了這個上限,數據庫會拒絕執行這些SQL并拋出異常。
BatchExecutor實現了批量處理SQL的功能,核心字段如下。
/*** 緩存多個Statement,每個Statement都緩存了多條SQL*/
private final List<Statement> statementList = new ArrayList<>();
/*** 記錄批處理的結果*/
private final List<BatchResult> batchResultList = new ArrayList<>();
/*** 記錄當前執行的SQL*/
private String currentSql;
/*** 記錄當前執行的MappedStatement*/
private MappedStatement currentStatement;
JDBC只支持insert、update、delete的批處理,select不存在批處理一說,因此這里主要分析doUpdate方法。
doUpdate方法在添加一條SQL的時候,會先將currentSql字段記錄的SQl以及currentStatement記錄的MappedStatement對象與當前添加的SQL對比,如果相同則添加到同一個Statement對象中等待執行,不同則創建新的Statement對象并緩存到statementList集合中等待執行,代碼如下。
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {final Configuration configuration = ms.getConfiguration();final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);final BoundSql boundSql = handler.getBoundSql();final String sql = boundSql.getSql();final Statement stmt;// 如果sql和當前sql相同,這里的sql還有問號占位符,并且MappedStatement和當前Statement相同,就添加到同一個Statement對象中等待執行if (sql.equals(currentSql) && ms.equals(currentStatement)) {// 獲取最后一個Statement對象int last = statementList.size() - 1;stmt = statementList.get(last);applyTransactionTimeout(stmt);// 綁定實參,處理?占位符handler.parameterize(stmt);//fix Issues 322// 查找對應的BatchResult,記錄用戶傳入的實參BatchResult batchResult = batchResultList.get(last);batchResult.addParameterObject(parameterObject);} else {// 否則創建新的Statement緩存到statementList中等待執行Connection connection = getConnection(ms.getStatementLog());// 創建Statementstmt = handler.prepare(connection, transaction.getTimeout());handler.parameterize(stmt); //fix Issues 322currentSql = sql;currentStatement = ms;// 添加到statementListstatementList.add(stmt);// 添加新的BatchResultbatchResultList.add(new BatchResult(ms, sql, parameterObject));}handler.batch(stmt);return BATCH_UPDATE_RETURN_VALUE;
}
JDBC的Statement可以添加不同模式的SQL,每添加一個新模式的SQl就會觸發一次編譯操作。而PrepareStatement中只能添加統一模式的SQL語句,只觸發一次編譯操作,但是可以通過綁定多組不同的參數實現批處理。而BatchExecutor做的就是這件事,將連續添加的、相同模式的SQL語句添加到同一個Statement對象中,從而有效地減少編譯次數。
在添加完待執行的SQL之后,doFlushStatement方法會處理這些SQL語句。
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {try {// 記錄批處理的結果List<BatchResult> results = new ArrayList<>();// 如果明確指定了要回滾事務,則直接返回空集合,忽略statementList中記錄的sqlif (isRollback) {return Collections.emptyList();}// 遍歷StatementListfor (int i = 0, n = statementList.size(); i < n; i++) {Statement stmt = statementList.get(i);applyTransactionTimeout(stmt);BatchResult batchResult = batchResultList.get(i);try {// 調用批量執行方法,返回int數組,每個元素都表示每條sql影響的記錄條數batchResult.setUpdateCounts(stmt.executeBatch());MappedStatement ms = batchResult.getMappedStatement();List<Object> parameterObjects = batchResult.getParameterObjects();// 獲取配置的KeyGeneratorKeyGenerator keyGenerator = ms.getKeyGenerator();if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;// 獲取數據庫生成的主鍵,并配置到parameterObjectsjdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141// 其他類型的KeyGenerator,調用其processAfterfor (Object parameter : parameterObjects) {keyGenerator.processAfter(this, ms, stmt, parameter);}}// Close statement to close cursor #1109closeStatement(stmt);} catch (BatchUpdateException e) {StringBuilder message = new StringBuilder();message.append(batchResult.getMappedStatement().getId()).append(" (batch index #").append(i + 1).append(")").append(" failed.");if (i > 0) {message.append(" ").append(i).append(" prior sub executor(s) completed successfully, but will be rolled back.");}throw new BatchExecutorException(message.toString(), e, results, batchResult);}// 將BatchResult添加到resultsresults.add(batchResult);}return results;} finally {for (Statement stmt : statementList) {closeStatement(stmt);}currentSql = null;statementList.clear();batchResultList.clear();}
}
結語
Executor接口的內容有點多,因此就分成了兩篇博客進行介紹,而最后的CachingExecutor是為Mybatis實現二級緩存功能,其中使用了裝飾器模式。Mybatis的二級緩存功能在實際開發中很少會使用,因此這里就不進行介紹,感興趣的朋友可以自己摸索。
*************************************優雅的分割線 **********************************
分享一波:程序員賺外快-必看的巔峰干貨
如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程
請關注微信公眾號:HB荷包
一個能讓你學習技術和賺錢方法的公眾號,持續更新
*************************************優雅的分割線 **********************************