国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

由for update引發的血案

roundstones / 1708人閱讀

摘要:微信公眾號后端進階,專注后端技術分享框架分布式中間件服務治理等等。

微信公眾號「后端進階」,專注后端技術分享:Java、Golang、WEB框架、分布式中間件、服務治理等等。 
老司機傾囊相授,帶你一路進階,來不及解釋了快上車!

公司的某些業務用到了數據庫的悲觀鎖 for update,但有些同事沒有把 for update 放在 Spring 事務中執行,在并發場景下發生了嚴重的線程阻塞問題,為了把這個問題吃透,秉承著老司機的職業素養,我決定要給同事們一個交代。

案發現場

最近公司的某些 Dubbo 服務之間的 RPC 調用過程中,偶然性地發生了若干起嚴重的超時問題,導致了某些模塊不能正常提供服務。我們的數據庫用的是 Oracle,經過 DBA 排查,發現了一些 sql 的執行時間特別長,對比發現這些執行時間長的 sql 都帶有 for update 悲觀鎖,于是相關開發人員查看 sql 對應的業務代碼,發現 for update 沒有放在 Spring 事務中執行,但是按照常理來說,如果 for update 沒有加 Spring 事務,每次執行完 Mybatis 都會幫我們 commit 釋放掉資源,并發時出現的問題應該是沒有鎖住對應資源產生臟數據而不是發生阻塞。但是經過代碼的調試,不加 Spring 事務并發執行確實會阻塞。

案例分析

基于案發現場的問題所在,我特地寫了幾個針對問題的案例分析測試代碼,"talk is cheap, show you the code":

加 Spring 事務執行但不提交事務
public void forupdateByTransaction() throws Exception {
  // 主線程獲取獨占鎖
  reentrantLock.lock();
  
  new Thread(() -> transactionTemplate.execute(transactionStatus -> {
    // select * from forupdate where name = #{name} for update
    this.forupdateMapper.findByName("testforupdate");
    System.out.println("==========for update==========");
    countDownLatch.countDown();
    // 阻塞不讓提交事務
    reentrantLock.lock();
    return null;
  })).start();
  
  countDownLatch.await();
  
  System.out.println("==========for update has countdown==========");
  this.forupdateMapper.updateByName("testforupdate");
  System.out.println("==========update success==========");

  reentrantLock.unlock();
}

此時 for update 被包裝在 Spring 事務中,將事務交由 Spring 管理,根據數據事務機制,sql 執行過程中,只有執行了 commit 或者 rollback 操作, 才會提交事務,所以此時每次執行 commit,for update 沒有被釋放,會鎖住對應資源,直到提交事務釋放 for udpate。所以此時的主線程執行更新操作會阻塞。

不加 Spring 事務并發執行
public void forupdateByConcurrent() {
  AtomicInteger atomicInteger = new AtomicInteger();

  for (int i = 0; i < 100; i++) {
    new Thread(() -> {
      // select * from forupdate where name = #{name} for update
      this.forupdateMapper.findByName("testforupdate");
      System.out.println("========ok:" + atomicInteger.getAndIncrement());
    }).start();
  }

}

首先我們先將數據庫連接池的初始化大小調大一點,使該次并發執行至少會獲取 2 個以上 ID 不同的 connection 對象來執行 for update,以下是某一次的執行日志:

得到測試結果,發現如果有 2 個或以上 ID 不同的 connection 對象執行 sql,會發生阻塞,而 Mysql 不會發生阻塞,至于 Mysql 為什么不會發生阻塞,后面我再給大家解釋。

由于我們使用的 druid 連接池,它的 autoCommit 默認為 true,所以我此時將 druid 連接池的 autoCommit 參數設置為 false,再次跑測試代碼,發現此時 oracle 不會發生阻塞,我們先記住這個測試結果,下面我會帶大家走一波源碼,來解釋這個現象。

聰明的你可能會想到,Mybatis 的底層源碼不是給我們封裝了一些重復性操作嗎,比如我們執行一條 sql 語句,mybatis 自動為我們 commit 或者 rollback了,這也是 JDBC 框架的基本要求,那么既然 Mybatis 幫我們 commit 了,for update 應該會被釋放才對,為什么還會發生阻塞問題呢?如果你能想到這個問題,說明你是個認真思考的人,這個問題我們也是先記住,后面會有解釋。

加 Spring 事務并發執行
private void forupdateByConcurrentAndTransaction() {
  AtomicInteger atomicInteger = new AtomicInteger();

  for (int i = 0; i < 100; i++) {
    new Thread(() -> transactionTemplate.execute(transactionStatus -> {
      // select * from forupdate where name = #{name} for update
      this.forupdateMapper.findByName("testforupdate");
      System.out.println("========ok:" + atomicInteger.getAndIncrement());
      return null;
    })).start();
  }
}

這個案例分析主要是為了測試是否跟 Spring 事務有關聯,我將 druid 鏈接池的 autoCommit 參數分別設置為 true 和 false,發現 for update 在 Spring 事務的包裝下并發執行,并不會發生阻塞,從測試結果來看,似乎是跟 Spring 事務有很大的關系。

我們現在總結一下案例分析測試結果:

事務不提交,for update 悲觀鎖不會被釋放;

不加 Spring 事務并發執行 for update 語句,如果有兩個以上的不同 ID 的 connection 執行 for update,會發生阻塞現象,Mysql 則不會阻塞;

不加 Spring 事務并發執行 for update 語句,并且 druid 連接池的 autocommit=false,不會發生阻塞;

加 Spring 事務并發執行 for update 語句,不會發生阻塞。

貼上測試代碼地址:https://github.com/objcoding/...

源碼走一波

基于上述的案例分析,我們源碼走一波,從底層源碼的角度來解析為什么會有這樣的結果。

Mybatis 事務管理器

有沒有發現,我到現在也是一直在強調 Spring 事務,其實在數據庫的角度來說,sql 只要在 START TRANSACTION 與 COMMIT 或者 ROLLBACK 之間執行,就算是一個事務,而我強調的 Spring 事務,指的是在Spring 管理下的事務,而 Mybatis 也有自己的事務管理器,通常我們使用 Mybatis 都是配合 Spring 來使用,而 Spring 整合 Mybatis,在 Mybatis-spring 包中,有一個名叫 SpringManagedTransaction 的類,這個就是 Mybatis 在 Spring 體系下的的 JDBC 事務管理器,Mybatis 用它來管理 JDBC connection 的生命周期,別看它名字是以 Spring 開頭,但它和 Spring 的事務管理器沒有半毛錢關系。

Mybatis 執行 sql 時會創建一個 SqlSession 會話,關于 SqlSession,坐我旁邊的鐘同學之前有向我提問過 SqlSession 的創建機制,我特意寫了一篇文章,感興趣的可以看看,這里就不再重復述說了:

「鐘同學,this is for you!」

在創建 SqlSession 時,相應地會創建一個事務管理器:

org.mybatis.spring.transaction.SpringManagedTransactionFactory#newTransaction:

public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
  return new SpringManagedTransaction(dataSource);
}

創建一個 transaction 時,我們發現傳入的 autoCommit 根本沒有賦值給 SpringManagedTransaction,這里暗藏玄機,我們繼續往下看:

執行 sql 時,Mybatis 會從事務管理器中從數據庫連接池中獲取一個 connection 對象:

org.mybatis.spring.transaction.SpringManagedTransaction#openConnection:

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

這里會從數據庫連接池中獲取 connection 對象,然后將 connection 對象中的 autoCommit 值賦值給 SpringManagedTransaction!可以這么理解,在 Spring 體系下的 Mybatis 事務管理器,autoCommit 的值被數據庫連接池的覆蓋掉了!而后面的 debug 日志也說明了,這個 JDBC connection 對象不歸你 Spring 管理,我 Mybatis 自己就可以管理了,你 Spring 就別瞎參合了。

sql 執行完之后,Mybatis 會自動幫我們 commit,我們來看 SqlSessionTemplate 的 sqlSession 代理:

org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor:

if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
  // force commit even on non-dirty sessions because some databases require
  // a commit/rollback before calling close()
  sqlSession.commit(true);
}

判斷如果不歸 Spring 事務管理,那么會強制執行 commit 操作,我們點進去,發現最終調用的是 Mybatis 的事務管理器的 commit 方法:

org.mybatis.spring.transaction.SpringManagedTransaction#commit:

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

問題就出現在這里,前面我也說了,我們使用的 druid 數據庫連接池的 autoCommit 默認為 true,而事務管理器獲取 connection 對象時,又將 connection 的 autocommit 賦值給事務管理器,如果此時 autoCommit 為 true,Mybatis 認為 connection 已經自動提交事務了,既然這事不歸我管,那么我 Mybatis 自然就不會再去 commit 了。

根據測試結果,將 druid 的 autoCommit 設置為 false 后,不會發生阻塞現象,即 Mybaits 會執行下面的 commit 操作。那么問題來了,connection 的 autocommit = true 時,到底有沒有 commit ?從測試結果來看,很明顯沒有 commit。這里就要從數據庫層來解釋了,由于公司 Oracle 數據庫的 autocommit 使用的是默認的 false 值,即需要顯式提交 commit 事務才會被提交。這也就是為什么當 druid 的 autoCommit=false 時,并發執行不會產生阻塞現象,因為 Mybatis 已經幫我們自動 commit 了。

而為什么當 druid 的 autoCommit=true 時,Mysql 依然不會阻塞呢?我先開啟 Mysql 的日志打印:

set global general_log = 1;

查看日志,發現 Mysql 會為每條執行的 sql 設置 autocommit=1,即自動提交事務,無須顯式提交 commit,每條 sql 就是一個事務。

Spring 事務管理器

上面的案例分析中,加了 Spring 事務的并發執行,并不會產生阻塞現象,顯然肯定是 Spring 事務做了一些不可描述的動作,Spring 的事務管理器有很多個,這里我們用的是數據庫連接池那個管理器,叫 DataSourceTransactionManager,我這里為了靈活控制事務范圍的細粒度,用的是聲明式事務,我們繼續走一波源碼,從事務入口一路跟蹤進來,發現第一步需要調用 doBegin 方法:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin:

// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don"t want to do it unnecessarily (for example if we"ve explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
  txObject.setMustRestoreAutoCommit(true);
  if (logger.isDebugEnabled()) {
    logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
  }
  con.setAutoCommit(false);
}

我們在 doBegin 方法發現了它偷偷地篡改了連接對象 autoCommit 的值,將它設為 false,這里想必大家都會明白其中的原理吧,Spring 管理事務其實就是在 sql 執行前將當前的 connection 對象設置為不自動提交模式,接下來執行的 sql 都不會自動提交,等待事務結束時,Spring 事務管理器會幫我們 commit 提交事務。這也就是為什么加了 Spring 事務的并發執行并不會產生阻塞的原因,原理與上述 Mybatis 所描述的一樣。

org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion:

// Reset connection.
Connection con = txObject.getConnectionHolder().getConnection();
try {
  if (txObject.isMustRestoreAutoCommit()) {
    con.setAutoCommit(true);
  }
  DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
}
catch (Throwable ex) {
  logger.debug("Could not reset JDBC Connection after transaction", ex);
}

在事務完成之后,我們還需要將 connection 對象還原,因為 connection 存在于連接池當中,close 時并不會真正關閉,而是被回收回連接池當中了,如果不對 connection 對象進行還原,那么當下一次會話拿到該 connection 對象,autoCommit 還是上一次會話的值,就會產生一些很隱晦的問題。

寫在最后

其實這個問題從應用層來分析還好,直接擼源碼就完了,主要是這個問題還涉及一些數據庫底層的一些原理,由于我對數據庫還不是那么地專業,所以在這過程中,還特別請教了公司的 DBA 斌哥哥,非常感謝他的協助。

另外,我其實是不太建議使用 for update 這種悲觀鎖的,它太過于依賴數據庫層了,而且當并發量起來了,雖然可以保證數據一致性,但是這樣犧牲了性能,會大大影響效率,嚴重拖垮數據庫資源,而且像這次一樣,有些開發人員使用了 for update 卻忘記 commit 事務了,導致引起很多鎖故障。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/74765.html

相關文章

  • 一個“大頭兒子帶小頭兒子吃飯”引發血案

    摘要:在群里討論,然后得出了這幾種寫法,感覺是層層遞進,想了想,最后選擇發布成文章大頭兒子小頭爸爸叫去吃飯大頭兒子小頭爸爸叫去吃飯大頭兒子小頭爸爸叫去吃飯吃完了背小頭兒子回去正在牽著的手正在吃給所有對象擴展一個繼承的方法繼承爸爸要繼承人的功能正在 在群里討論JavaScript,然后得出了這幾種寫法,感覺是層層遞進,想了想,最后選擇發布成文章 ({ baby : 大頭兒子, ...

    forrest23 評論0 收藏0
  • 增量部署class文件引發血案

    摘要:背景項目中通過遠程調用服務框架調用了許多其它的服務其中有一個服務需要升級其升級不是版本上的升級而是整個服務重新取了一個名字使用的也是全新的包但是調用的方法沒有改變因此在升級時只是在調用服務類中修改了調用地址和調用返回實體由改為該中返回該調用 背景 項目中通過遠程調用服務框架調用了許多其它的服務,其中有一個服務wx/subscribe/contract/CircleService 需要升...

    lolomaco 評論0 收藏0
  • 做面試不倒翁:一道事件循環題引發血案

    摘要:通過查看的文檔可以發現整個分為個階段定時器相關任務,中我們關注的是它會執行和中到期的回調執行某些系統操作的回調內部使用執行,一定條件下會在這個階段阻塞住執行的回調如果或者關閉了,就會在這個階段觸發事件,執行事件的回調的代碼在文件中。 showImg(https://segmentfault.com/img/bVbd7B7?w=1227&h=644); 這次我們就不要那么多前戲,直奔主題...

    ispring 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<