Spring Boot+MyBatis+Druid批處理SqlSessionTemplate事務(wù)無(wú)法回滾

問(wèn)題描述:

  軟件環(huán)境:Spring Boot版本: 2.0.2.RELEASE、MyBatis版本: 3.4.6、Druid版本: 1.1.8籍嘹、: mysql-connector-java-5.1.46   
 
  Spring Boot + MyBatis + Druid  整合中使用批處理的 SqlSessionTemplate 當(dāng)在Service層拋出RuntimeException(或子類(lèi))時(shí),
Druid代理的連接類(lèi)com.alibaba.druid.pool.DruidPooledConnection的rollback并不會(huì)調(diào)用目標(biāo)數(shù)據(jù)庫(kù)連接(比如MySql的數(shù)據(jù)庫(kù)連接)rollback方法,
而Druid代理的DruidPooledConnection的close方法的邏輯并不是關(guān)閉實(shí)際的數(shù)據(jù)庫(kù)連接,而是將其引用計(jì)數(shù)器減一,然后將連接重新添加到空閑連接池中,
這個(gè)時(shí)候因?yàn)闆](méi)有調(diào)用rollback, 當(dāng)其它線(xiàn)程獲取到這個(gè)連接的時(shí)候不是一個(gè)純凈的Connection,它里面有上次未提交的sql語(yǔ)句,如果此時(shí)提交將會(huì)把上次回滾的sql和當(dāng)前操作的sql一起提交,這個(gè)操作很危險(xiǎn)的。

定位問(wèn)題,分析過(guò)程如下:

MyBatis默認(rèn)的SqlSession是SIMPLE, 當(dāng)配置了:
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    //return new SqlSessionTemplate(sqlSessionFactory);   // 不使用批處理 SqlSession, 推薦用這種方式
    return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH); // 使用批處理 SqlSession, 不推薦用這種方式, 如果主鍵自增, 批處理方式無(wú)法獲取到自增的id
}
這段代碼后, 內(nèi)部使用的 Executor 是 org.apache.ibatis.executor.BatchExecutor, 而不是默認(rèn)的  org.apache.ibatis.executor.SimpleExecutor
對(duì)數(shù)據(jù)庫(kù)進(jìn)行DML之后, 最終都會(huì)轉(zhuǎn)到 BatchExecutor 或 SimpleExecutor 的 doUpdate(MappedStatement ms, Object parameterObject) 方法 [注: 根據(jù)配置選取是BatchExecutor還是SimpleExecutor]

1余蟹、先看能正呈芳颍回滾數(shù)據(jù)的情況,也就是 SimpleExecutor, 它的doUpdate(MappedStatement ms, Object parameterObject) 方法, 內(nèi)部包含如下代碼:
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
   // 省略其它代碼
   int count = handler.update(stmt);   // handler 的具體類(lèi)型是: org.apache.ibatis.executor.statement.RoutingStatementHandler , 我們跟蹤進(jìn)入這個(gè)方法
   // 省略其它代碼
}
 
RoutingStatementHandler.java 的 update 方法如下: 
public int update(Statement statement) throws SQLException {
    return delegate.update(statement);  // delegate 的具體類(lèi)型是: org.apache.ibatis.executor.statement.PreparedStatementHandler , 我們跟蹤進(jìn)入這個(gè)方法
}

PreparedStatementHandler.java 的 update 方法如下: 
@Override
public int update(Statement statement) throws SQLException {
   PreparedStatement ps = (PreparedStatement) statement;
    // ps.execute() 這行代碼是關(guān)鍵: ps的具體實(shí)現(xiàn)類(lèi)是: com.alibaba.druid.pool.DruidPooledPreparedStatement , 進(jìn)入這個(gè)方法
    ps.execute();
    // 省略其它代碼
}

DruidPooledPreparedStatement.java 的 execute() 方法如下: 
 @Override
  public boolean execute() throws SQLException {
      // 省略其它代碼
      transactionRecord(sql); // 這行代碼是關(guān)鍵, 跟進(jìn)去
      // 省略其它代碼
 }

DruidPooledPreparedStatement.java 的 transactionRecord 方法如下: 
 protected void transactionRecord(String sql) throws SQLException {
      // 觸發(fā)Connection創(chuàng)建事務(wù)
      conn.transactionRecord(sql);  // conn 的具體實(shí)現(xiàn)類(lèi)是: com.alibaba.druid.pool.DruidPooledConnection , 跟進(jìn)去查看這個(gè)方法
  }

DruidPooledConnection.java
protected void transactionRecord(String sql) throws SQLException {
    // 這個(gè)非空判斷是關(guān)鍵: 實(shí)際調(diào)試的結(jié)果是進(jìn)入了這個(gè)判斷, 也就是 new 了一個(gè) TransactionInfo      
    if (transactionInfo == null && (!conn.getAutoCommit())) {
       DruidAbstractDataSource dataSource = holder.getDataSource();
       dataSource.incrementStartTransactionCount();
       transactionInfo = new TransactionInfo(dataSource.createTransactionId()); // 進(jìn)入了這里, 創(chuàng)建了 TransactionInfo 
   }
    // 省略其它代碼
}

以上流程結(jié)束后, 當(dāng)Service拋出RuntimeException 時(shí)需要回滾事務(wù),進(jìn)入 DruidPooledConnection 的 rollback 方法,

public void rollback() throws SQLException {
      // 此時(shí) transactionInfo 和 holder 都不為空, 沒(méi)有進(jìn)入下面的兩個(gè)中斷判斷中, 事務(wù)正程海回滾
      if (transactionInfo == null) { // 中斷判斷, SimpleExecutor 沒(méi)有進(jìn)入此判斷
          return;
      }
      if (holder == null) { // 中斷判斷, SimpleExecutor 沒(méi)有進(jìn)入此判斷
          return;
     }
     // 省略其它代碼

     conn.rollback();  // 這里調(diào)用實(shí)際的數(shù)據(jù)庫(kù)連接回滾事務(wù), 事務(wù)正趁姿撸回滾

     // 省略其它代碼
}
SimpleExecutor 分析完畢

2菱蔬、再看 BatchExecutor 不能正常回滾數(shù)據(jù)的情況,
BatchExecutor.java 的 update 方法如下: 
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
     // 節(jié)省篇幅省略了部分代碼
     final Statement stmt;
     if (sql.equals(currentSql) && ms.equals(currentStatement)) {
          // 省略其它代碼
     } else { // 進(jìn)入了 esle 分支
         Connection connection = getConnection(ms.getStatementLog());
         stmt = handler.prepare(connection, transaction.getTimeout());
         handler.parameterize(stmt);    //fix Issues 322
         currentSql = sql;
         currentStatement = ms;
         statementList.add(stmt); // 這里把 Statement 存在集合里面了, 后面一定會(huì)有清除集合的代碼, 否則將導(dǎo)致內(nèi)存泄漏  
         batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
   
   handler.batch(stmt);  // handler 的具體實(shí)現(xiàn)類(lèi)型是: org.apache.ibatis.executor.statement.RoutingStatementHandler  , 進(jìn)入這個(gè)方法
   return BATCH_UPDATE_RETURN_VALUE;   
}

 RoutingStatementHandler.java 的 batch 方法如下:
 public void batch(Statement statement) throws SQLException {
     delegate.batch(statement);  // delegate 的具體實(shí)現(xiàn)類(lèi)是: org.apache.ibatis.executor.statement.PreparedStatementHandler , 進(jìn)入這個(gè)方法
}

PreparedStatementHandler.java 的 batch 方法如下:
public void batch(Statement statement) throws SQLException {
      PreparedStatement ps = (PreparedStatement) statement;    
      ps.addBatch();// ps 的實(shí)現(xiàn)類(lèi)是: com.alibaba.druid.pool.DruidPooledPreparedStatement , 進(jìn)入這個(gè)方法
}

DruidPooledPreparedStatement.java 的 addBatch 方法如下:
public void addBatch() throws SQLException {
     // 省略其它代碼   
    
     // 特別注意: 這里的addBatch()和 SimpleExecutor 最終調(diào)用 PreparedStatement 的 execute() 方法不同, 這個(gè)沒(méi)有創(chuàng)建 TransactionInfo
     // 當(dāng)以上流程結(jié)束后, 當(dāng)Service拋出RuntimeException 時(shí)需要回滾事務(wù),進(jìn)入 DruidPooledConnection 的 rollback 方法,
     stmt.addBatch();  
}

DruidPooledConnection.java 的  rollback 方法如下:
public void rollback() throws SQLException {
      // 此時(shí) transactionInfo 為空, 進(jìn)入下面的中斷判斷中, 從而導(dǎo)致后面的 conn.rollback(); 沒(méi)有執(zhí)行
      if (transactionInfo == null) { // 中斷判斷, BatchExecutor 進(jìn)入了此判斷, 從而導(dǎo)致后面的 conn.rollback(); 沒(méi)有執(zhí)行
          return;
      }
      if (holder == null) { 
          return;
     }
     // 省略其它代碼

     conn.rollback();  // 使用 BatchExecutor 時(shí)不能執(zhí)行此操作

     // 省略其它代碼
}

至此: 我們提出的問(wèn)題產(chǎn)生的原因分析完畢.

嘗試解決方案如下(在如下兩個(gè)類(lèi)新增了兩處代碼):

org.apache.ibatis.executor.BatchExecutor.java 修改如下: 

public class BatchExecutor extends BaseExecutor {

  ... ...

  @Override
  public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException { // 為 true 是回滾數(shù)據(jù), line: 111 
    try {
      List<BatchResult> results = new ArrayList<BatchResult>();
      if (isRollback) {
// 調(diào)試后新增: 2017-07-02 ======================> 開(kāi)始  ==================================================================>
          // 批處理方式需要清除加入的 Statement 的
          if(statementList!=null) {
              // 缺一個(gè)清除 Statement 中的批處理腳本的邏輯
              for(Statement stmt : statementList) {
                    closeStatement(stmt);
                   // 清除批處理緩存
                   stmt.clearBatch();
              }
          }
          
          // 清除批處理的緩存信息
          currentSql = null;
          statementList.clear();
          batchResultList.clear();
//        Connection conn = transaction.getConnection();  // 最好不要再這里關(guān)閉數(shù)據(jù)庫(kù)連接
//        if(conn!=null) {
//           conn.close();
//        }
// 調(diào)試后新增: 2017-07-02 ======================>結(jié)束  ==================================================================>
         
          // 返回空集合
         return Collections.emptyList();
      }
  }   
    
  ... ...
}

com.alibaba.druid.pool.DruidPooledPreparedStatement.java 修改如下: 

public class DruidPooledPreparedStatement extends DruidPooledStatement implements PreparedStatement {
    
    ... ...
    
    @Override
    public void addBatch() throws SQLException {  // line: 549
        checkOpen();
        try {
// 調(diào)試后新增: 2017-07-02 ======================> 開(kāi)始  ==================================================================> 
            if(null == conn.getTransactionInfo()) {
               conn.createTransactionInfo(); // 創(chuàng)建 TransactionInfo , 當(dāng)執(zhí)行 DruidPooledConnection 的 rollback 時(shí)不會(huì)進(jìn)入中斷方法
            }
// 調(diào)試后新增: 2017-07-02 ======================>結(jié)束  ==================================================================>
            stmt.addBatch();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    ... ...
}

后記:

   整體來(lái)看,是因?yàn)镈ruidPooledPreparedStatement.java 的 TransactionInfo 沒(méi)有在 addBatch() 方法中創(chuàng)建,而是延遲到了 executeBatch() 方法調(diào)用時(shí)才創(chuàng)建,
 而 BaseExecutor.java 當(dāng)出現(xiàn)異常時(shí)不執(zhí)行DruidPooledPreparedStatement的executeBatch()方法,進(jìn)而引發(fā)事務(wù)沒(méi)有被回滾。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拴泌,一起剝皮案震驚了整個(gè)濱河市魏身,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蚪腐,老刑警劉巖箭昵,帶你破解...
    沈念sama閱讀 212,222評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異回季,居然都是意外死亡家制,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)泡一,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)颤殴,“玉大人,你說(shuō)我怎么就攤上這事瘾杭∽绮。” “怎么了哪亿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,720評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵粥烁,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我蝇棉,道長(zhǎng)讨阻,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,568評(píng)論 1 284
  • 正文 為了忘掉前任篡殷,我火速辦了婚禮钝吮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘板辽。我一直安慰自己奇瘦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,696評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布劲弦。 她就那樣靜靜地躺著耳标,像睡著了一般。 火紅的嫁衣襯著肌膚如雪邑跪。 梳的紋絲不亂的頭發(fā)上次坡,一...
    開(kāi)封第一講書(shū)人閱讀 49,879評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音画畅,去河邊找鬼砸琅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛轴踱,可吹牛的內(nèi)容都是我干的症脂。 我是一名探鬼主播,決...
    沈念sama閱讀 39,028評(píng)論 3 409
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼诱篷!你這毒婦竟也來(lái)了沸版?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,773評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤兴蒸,失蹤者是張志新(化名)和其女友劉穎视粮,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體橙凳,經(jīng)...
    沈念sama閱讀 44,220評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蕾殴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,550評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了岛啸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钓觉。...
    茶點(diǎn)故事閱讀 38,697評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖坚踩,靈堂內(nèi)的尸體忽然破棺而出荡灾,到底是詐尸還是另有隱情,我是刑警寧澤瞬铸,帶...
    沈念sama閱讀 34,360評(píng)論 4 332
  • 正文 年R本政府宣布批幌,位于F島的核電站,受9級(jí)特大地震影響嗓节,放射性物質(zhì)發(fā)生泄漏荧缘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,002評(píng)論 3 315
  • 文/蒙蒙 一拦宣、第九天 我趴在偏房一處隱蔽的房頂上張望截粗。 院中可真熱鬧,春花似錦鸵隧、人聲如沸绸罗。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,782評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)珊蟀。三九已至,卻和暖如春靡羡,著一層夾襖步出監(jiān)牢的瞬間系洛,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,010評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工略步, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留描扯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,433評(píng)論 2 360
  • 正文 我出身青樓趟薄,卻偏偏與公主長(zhǎng)得像绽诚,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,587評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容