Spring 與 Mybatis 的事務(wù)

1、前言

mybatis 是有事務(wù)模塊的乙各,mybatis 與 spring 結(jié)合的時候杖狼,spring 實(shí)現(xiàn)了 mybatis 的事務(wù)接口 Transaction披蕉,實(shí)現(xiàn)類為 SpringManagedTransaction。


SpringManagedTransaction 類的注釋

此類的注釋如下:

  • 如果它主要掌管 jdbc 連接的整個生命周期伞访,包括獲取釋放膝藕。
  • 如果 spring 的事務(wù)管理器被激活的話,那么它不會做事務(wù)的操作咐扭,他的 commit/rollback/close 不會做任何操作。
  • 如果 spring 事務(wù)不激活滑废,那么它會掌管 jdbc 的事務(wù)蝗肪,事實(shí)上,我們一般都用 spring 自己的事務(wù)蠕趁,所以它其實(shí)就拿個連接而已薛闪。

我們可以點(diǎn)進(jìn)去看他的方法,事實(shí)上也是這樣:

/**
 *    Copyright 2010-2016 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.spring.transaction;

import static org.springframework.util.Assert.notNull;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.transaction.Transaction;
import org.springframework.jdbc.datasource.ConnectionHolder;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * {@code SpringManagedTransaction} handles the lifecycle of a JDBC connection.
 * It retrieves a connection from Spring's transaction manager and returns it back to it
 * when it is no longer needed.
 * <p>
 * If Spring's transaction handling is active it will no-op all commit/rollback/close calls
 * assuming that the Spring transaction manager will do the job.
 * <p>
 * If it is not it will behave like {@code JdbcTransaction}.
 *
 * @author Hunter Presnall
 * @author Eduardo Macarron
 * 
 * @version $Id$
 */
public class SpringManagedTransaction implements Transaction {

  private static final Log LOGGER = LogFactory.getLog(SpringManagedTransaction.class);

  private final DataSource dataSource;

  private Connection connection;

  private boolean isConnectionTransactional;

  private boolean autoCommit;

  public SpringManagedTransaction(DataSource dataSource) {
    notNull(dataSource, "No DataSource specified");
    this.dataSource = dataSource;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }

  /**
   * Gets a connection from Spring transaction manager and discovers if this
   * {@code Transaction} should manage connection or let it to Spring.
   * <p>
   * It also reads autocommit setting because when using Spring Transaction MyBatis
   * thinks that autocommit is always false and will always call commit/rollback
   * so we need to no-op that calls.
   */
  private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug(
          "JDBC Connection ["
              + this.connection
              + "] will"
              + (this.isConnectionTransactional ? " " : " not ")
              + "be managed by Spring");
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void commit() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
      }
      this.connection.commit();
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void rollback() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Rolling back JDBC Connection [" + this.connection + "]");
      }
      this.connection.rollback();
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void close() throws SQLException {
    DataSourceUtils.releaseConnection(this.connection, this.dataSource);
  }
    
  /**
   * {@inheritDoc}
   */
  @Override
  public Integer getTimeout() throws SQLException {
    ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    if (holder != null && holder.hasTimeout()) {
      return holder.getTimeToLiveInSeconds();
    } 
    return null;
  }

}

綜上而言俺陋,mybatis 就像個工具人豁延,spring 只需要它解析 mapper 接口、mapper.xml 文件之類的功能(mybatis 主體功能)腊状,對于事務(wù)這種東西诱咏,spring 選擇了自己實(shí)現(xiàn)。那么缴挖,spring 事務(wù)是怎么實(shí)現(xiàn)的呢袋狞?

2、關(guān)于事務(wù)實(shí)現(xiàn)

首先映屋,不管是 spring苟鸯、mybatis 抑或是其他的任何框架類,對于事務(wù)的實(shí)現(xiàn)本質(zhì)上還是利用數(shù)據(jù)源的事務(wù)實(shí)現(xiàn)棚点。就比如 mybatis早处,在最后提交事務(wù)的時候,還是使用 connection.commit()瘫析、connection.rollback()砌梆。可以試著點(diǎn)進(jìn)去 commit 等方法颁股,發(fā)現(xiàn)調(diào)用的是各個數(shù)據(jù)源的實(shí)現(xiàn)(而數(shù)據(jù)源的實(shí)現(xiàn)么库,最后都是數(shù)據(jù)庫 sql 語句的實(shí)現(xiàn),比如一個很簡單的事務(wù)操作語句:
set autocommit = 0甘有;
update xxx set xx = yy ...诉儒;
commit;
如果有人能講清楚這個問題亏掀,而不是說一堆廢話忱反,我想可能更好理解把)泛释。代碼如下:

/**
 *    Copyright 2009-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.transaction.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;

import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.TransactionException;

/**
 * {@link Transaction} that makes use of the JDBC commit and rollback facilities directly.
 * It relies on the connection retrieved from the dataSource to manage the scope of the transaction.
 * Delays connection retrieval until getConnection() is called.
 * Ignores commit or rollback requests when autocommit is on.
 *
 * @author Clinton Begin
 *
 * @see JdbcTransactionFactory
 */
public class JdbcTransaction implements Transaction {

  private static final Log log = LogFactory.getLog(JdbcTransaction.class);

  protected Connection connection;
  protected DataSource dataSource;
  protected TransactionIsolationLevel level;
  // MEMO: We are aware of the typo. See #941
  protected boolean autoCommmit;

  public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
    dataSource = ds;
    level = desiredLevel;
    autoCommmit = desiredAutoCommit;
  }

  public JdbcTransaction(Connection connection) {
    this.connection = connection;
  }

  @Override
  public Connection getConnection() throws SQLException {
    if (connection == null) {
      openConnection();
    }
    return connection;
  }

  @Override
  public void commit() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Committing JDBC Connection [" + connection + "]");
      }
      connection.commit();
    }
  }

  @Override
  public void rollback() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Rolling back JDBC Connection [" + connection + "]");
      }
      connection.rollback();
    }
  }

  @Override
  public void close() throws SQLException {
    if (connection != null) {
      resetAutoCommit();
      if (log.isDebugEnabled()) {
        log.debug("Closing JDBC Connection [" + connection + "]");
      }
      connection.close();
    }
  }

  protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
    try {
      if (connection.getAutoCommit() != desiredAutoCommit) {
        if (log.isDebugEnabled()) {
          log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");
        }
        connection.setAutoCommit(desiredAutoCommit);
      }
    } catch (SQLException e) {
      // Only a very poorly implemented driver would fail here,
      // and there's not much we can do about that.
      throw new TransactionException("Error configuring AutoCommit.  "
          + "Your driver may not support getAutoCommit() or setAutoCommit(). "
          + "Requested setting: " + desiredAutoCommit + ".  Cause: " + e, e);
    }
  }

  protected void resetAutoCommit() {
    try {
      if (!connection.getAutoCommit()) {
        // MyBatis does not call commit/rollback on a connection if just selects were performed.
        // Some databases start transactions with select statements
        // and they mandate a commit/rollback before closing the connection.
        // A workaround is setting the autocommit to true before closing the connection.
        // Sybase throws an exception here.
        if (log.isDebugEnabled()) {
          log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]");
        }
        connection.setAutoCommit(true);
      }
    } catch (SQLException e) {
      if (log.isDebugEnabled()) {
        log.debug("Error resetting autocommit to true "
          + "before closing the connection.  Cause: " + e);
      }
    }
  }

  protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
      log.debug("Opening JDBC Connection");
    }
    connection = dataSource.getConnection();
    if (level != null) {
      connection.setTransactionIsolation(level.getLevel());
    }
    setDesiredAutoCommit(autoCommmit);
  }

  @Override
  public Integer getTimeout() throws SQLException {
    return null;
  }
  
}

3、spring 事務(wù)實(shí)現(xiàn)

實(shí)際上温算,spring 的源碼異常龐大復(fù)雜怜校,所以基本上是不可能看完的,光 transaction 都有一個模塊注竿,這邊頂多講講他的實(shí)現(xiàn)思路茄茁。

spring中的事務(wù)實(shí)現(xiàn)從原理上說比較簡單,通過aop在方法執(zhí)行前后增加數(shù)據(jù)庫事務(wù)的操作巩割。

  • 1.在方法開始時判斷是否開啟新事務(wù)裙顽,需要開啟事務(wù)則設(shè)置事務(wù)手動提交 set autocommit=0;
  • 2.在方法執(zhí)行完成后手動提交事務(wù) commit;
  • 3.在方法拋出指定異常后調(diào)用rollback回滾事務(wù)

具體邏輯看 TransactionIntercepto 的 invoke 方法,會發(fā)現(xiàn)有創(chuàng)建事務(wù)宣谈,提交事務(wù)愈犹,出錯會滾的代碼,我也就看了一下闻丑,沒具體細(xì)看漩怎。不過我們使用 @Transactional 開啟事務(wù)的時候,
我們先使用:sudo tcpdump -i lo0 -s 0 -l -w - port 3306 | strings
抓包 mysql 的 sql 打印代碼嗦嗡,會發(fā)現(xiàn)有如下的代碼打友浮:


mysql 的 tcp 通信

當(dāng)然,我在實(shí)際操作的過程中發(fā)現(xiàn)侥祭,我不好抓這個包怪得,可能是不熟悉這個軟件把催什。所以我直接去 mysql 的日志查看爆哑,事實(shí)也確實(shí)是類似的(記得開啟 mysql 日志):


mysql 日志

4殊者、感想

我覺得大部分人都不會好好講話鲸沮,至少都不會完整的把一個事情講明白往扔。spring 的事務(wù)編程模型確實(shí)很復(fù)雜逾礁,但是我有時候其實(shí)想知道他為啥有效移宅,而不是整個模型是怎樣做的鲫售,因?yàn)槿绻浪麨樯队行Ш竺婢吐芯烤托辛讼跖 R苍S

5径筏、參考資料

https://xie.infoq.cn/article/52f38883e28821c9cf0a608ea

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市障陶,隨后出現(xiàn)的幾起案子滋恬,更是在濱河造成了極大的恐慌,老刑警劉巖抱究,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恢氯,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)勋拟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門勋磕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人敢靡,你說我怎么就攤上這事挂滓。” “怎么了啸胧?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵赶站,是天一觀的道長。 經(jīng)常有香客問我纺念,道長亲怠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任柠辞,我火速辦了婚禮,結(jié)果婚禮上主胧,老公的妹妹穿的比我還像新娘叭首。我一直安慰自己,他們只是感情好踪栋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布焙格。 她就那樣靜靜地躺著,像睡著了一般夷都。 火紅的嫁衣襯著肌膚如雪眷唉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天囤官,我揣著相機(jī)與錄音冬阳,去河邊找鬼。 笑死党饮,一個胖子當(dāng)著我的面吹牛肝陪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刑顺,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼氯窍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蹲堂?” 一聲冷哼從身側(cè)響起狼讨,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎柒竞,沒想到半個月后政供,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年鲫骗,在試婚紗的時候發(fā)現(xiàn)自己被綠了犬耻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡执泰,死狀恐怖枕磁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情术吝,我是刑警寧澤计济,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站排苍,受9級特大地震影響沦寂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜淘衙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一传藏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧彤守,春花似錦毯侦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至筝蚕,卻和暖如春卦碾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背起宽。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工洲胖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坯沪。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓宾濒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親屏箍。 傳聞我的和親對象是個殘疾皇子绘梦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355

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