摘要:微信公眾號(hào)后端進(jìn)階,專注后端技術(shù)分享框架分布式中間件服務(wù)治理等等。
微信公眾號(hào)「后端進(jìn)階」,專注后端技術(shù)分享:Java、Golang、WEB框架、分布式中間件、服務(wù)治理等等。
老司機(jī)傾囊相授,帶你一路進(jìn)階,來不及解釋了快上車!
公司的某些業(yè)務(wù)用到了數(shù)據(jù)庫的悲觀鎖 for update,但有些同事沒有把 for update 放在 Spring 事務(wù)中執(zhí)行,在并發(fā)場景下發(fā)生了嚴(yán)重的線程阻塞問題,為了把這個(gè)問題吃透,秉承著老司機(jī)的職業(yè)素養(yǎng),我決定要給同事們一個(gè)交代。
案發(fā)現(xiàn)場最近公司的某些 Dubbo 服務(wù)之間的 RPC 調(diào)用過程中,偶然性地發(fā)生了若干起嚴(yán)重的超時(shí)問題,導(dǎo)致了某些模塊不能正常提供服務(wù)。我們的數(shù)據(jù)庫用的是 Oracle,經(jīng)過 DBA 排查,發(fā)現(xiàn)了一些 sql 的執(zhí)行時(shí)間特別長,對(duì)比發(fā)現(xiàn)這些執(zhí)行時(shí)間長的 sql 都帶有 for update 悲觀鎖,于是相關(guān)開發(fā)人員查看 sql 對(duì)應(yīng)的業(yè)務(wù)代碼,發(fā)現(xiàn) for update 沒有放在 Spring 事務(wù)中執(zhí)行,但是按照常理來說,如果 for update 沒有加 Spring 事務(wù),每次執(zhí)行完 Mybatis 都會(huì)幫我們 commit 釋放掉資源,并發(fā)時(shí)出現(xiàn)的問題應(yīng)該是沒有鎖住對(duì)應(yīng)資源產(chǎn)生臟數(shù)據(jù)而不是發(fā)生阻塞。但是經(jīng)過代碼的調(diào)試,不加 Spring 事務(wù)并發(fā)執(zhí)行確實(shí)會(huì)阻塞。
案例分析基于案發(fā)現(xiàn)場的問題所在,我特地寫了幾個(gè)針對(duì)問題的案例分析測試代碼,"talk is cheap, show you the code":
加 Spring 事務(wù)執(zhí)行但不提交事務(wù)public void forupdateByTransaction() throws Exception { // 主線程獲取獨(dú)占鎖 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(); // 阻塞不讓提交事務(wù) 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(); }
此時(shí) for update 被包裝在 Spring 事務(wù)中,將事務(wù)交由 Spring 管理,根據(jù)數(shù)據(jù)事務(wù)機(jī)制,sql 執(zhí)行過程中,只有執(zhí)行了 commit 或者 rollback 操作, 才會(huì)提交事務(wù),所以此時(shí)每次執(zhí)行 commit,for update 沒有被釋放,會(huì)鎖住對(duì)應(yīng)資源,直到提交事務(wù)釋放 for udpate。所以此時(shí)的主線程執(zhí)行更新操作會(huì)阻塞。
不加 Spring 事務(wù)并發(fā)執(zhí)行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(); } }
首先我們先將數(shù)據(jù)庫連接池的初始化大小調(diào)大一點(diǎn),使該次并發(fā)執(zhí)行至少會(huì)獲取 2 個(gè)以上 ID 不同的 connection 對(duì)象來執(zhí)行 for update,以下是某一次的執(zhí)行日志:
得到測試結(jié)果,發(fā)現(xiàn)如果有 2 個(gè)或以上 ID 不同的 connection 對(duì)象執(zhí)行 sql,會(huì)發(fā)生阻塞,而 Mysql 不會(huì)發(fā)生阻塞,至于 Mysql 為什么不會(huì)發(fā)生阻塞,后面我再給大家解釋。
由于我們使用的 druid 連接池,它的 autoCommit 默認(rèn)為 true,所以我此時(shí)將 druid 連接池的 autoCommit 參數(shù)設(shè)置為 false,再次跑測試代碼,發(fā)現(xiàn)此時(shí) oracle 不會(huì)發(fā)生阻塞,我們先記住這個(gè)測試結(jié)果,下面我會(huì)帶大家走一波源碼,來解釋這個(gè)現(xiàn)象。
聰明的你可能會(huì)想到,Mybatis 的底層源碼不是給我們封裝了一些重復(fù)性操作嗎,比如我們執(zhí)行一條 sql 語句,mybatis 自動(dòng)為我們 commit 或者 rollback了,這也是 JDBC 框架的基本要求,那么既然 Mybatis 幫我們 commit 了,for update 應(yīng)該會(huì)被釋放才對(duì),為什么還會(huì)發(fā)生阻塞問題呢?如果你能想到這個(gè)問題,說明你是個(gè)認(rèn)真思考的人,這個(gè)問題我們也是先記住,后面會(huì)有解釋。
加 Spring 事務(wù)并發(fā)執(zhí)行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(); } }
這個(gè)案例分析主要是為了測試是否跟 Spring 事務(wù)有關(guān)聯(lián),我將 druid 鏈接池的 autoCommit 參數(shù)分別設(shè)置為 true 和 false,發(fā)現(xiàn) for update 在 Spring 事務(wù)的包裝下并發(fā)執(zhí)行,并不會(huì)發(fā)生阻塞,從測試結(jié)果來看,似乎是跟 Spring 事務(wù)有很大的關(guān)系。
我們現(xiàn)在總結(jié)一下案例分析測試結(jié)果:
事務(wù)不提交,for update 悲觀鎖不會(huì)被釋放;
不加 Spring 事務(wù)并發(fā)執(zhí)行 for update 語句,如果有兩個(gè)以上的不同 ID 的 connection 執(zhí)行 for update,會(huì)發(fā)生阻塞現(xiàn)象,Mysql 則不會(huì)阻塞;
不加 Spring 事務(wù)并發(fā)執(zhí)行 for update 語句,并且 druid 連接池的 autocommit=false,不會(huì)發(fā)生阻塞;
加 Spring 事務(wù)并發(fā)執(zhí)行 for update 語句,不會(huì)發(fā)生阻塞。
貼上測試代碼地址:https://github.com/objcoding/...
源碼走一波基于上述的案例分析,我們?cè)创a走一波,從底層源碼的角度來解析為什么會(huì)有這樣的結(jié)果。
Mybatis 事務(wù)管理器有沒有發(fā)現(xiàn),我到現(xiàn)在也是一直在強(qiáng)調(diào) Spring 事務(wù),其實(shí)在數(shù)據(jù)庫的角度來說,sql 只要在 START TRANSACTION 與 COMMIT 或者 ROLLBACK 之間執(zhí)行,就算是一個(gè)事務(wù),而我強(qiáng)調(diào)的 Spring 事務(wù),指的是在Spring 管理下的事務(wù),而 Mybatis 也有自己的事務(wù)管理器,通常我們使用 Mybatis 都是配合 Spring 來使用,而 Spring 整合 Mybatis,在 Mybatis-spring 包中,有一個(gè)名叫 SpringManagedTransaction 的類,這個(gè)就是 Mybatis 在 Spring 體系下的的 JDBC 事務(wù)管理器,Mybatis 用它來管理 JDBC connection 的生命周期,別看它名字是以 Spring 開頭,但它和 Spring 的事務(wù)管理器沒有半毛錢關(guān)系。
Mybatis 執(zhí)行 sql 時(shí)會(huì)創(chuàng)建一個(gè) SqlSession 會(huì)話,關(guān)于 SqlSession,坐我旁邊的鐘同學(xué)之前有向我提問過 SqlSession 的創(chuàng)建機(jī)制,我特意寫了一篇文章,感興趣的可以看看,這里就不再重復(fù)述說了:
「鐘同學(xué),this is for you!」
在創(chuàng)建 SqlSession 時(shí),相應(yīng)地會(huì)創(chuàng)建一個(gè)事務(wù)管理器:
org.mybatis.spring.transaction.SpringManagedTransactionFactory#newTransaction:
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) { return new SpringManagedTransaction(dataSource); }
創(chuàng)建一個(gè) transaction 時(shí),我們發(fā)現(xiàn)傳入的 autoCommit 根本沒有賦值給 SpringManagedTransaction,這里暗藏玄機(jī),我們繼續(xù)往下看:
執(zhí)行 sql 時(shí),Mybatis 會(huì)從事務(wù)管理器中從數(shù)據(jù)庫連接池中獲取一個(gè) connection 對(duì)象:
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"); } }
這里會(huì)從數(shù)據(jù)庫連接池中獲取 connection 對(duì)象,然后將 connection 對(duì)象中的 autoCommit 值賦值給 SpringManagedTransaction!可以這么理解,在 Spring 體系下的 Mybatis 事務(wù)管理器,autoCommit 的值被數(shù)據(jù)庫連接池的覆蓋掉了!而后面的 debug 日志也說明了,這個(gè) JDBC connection 對(duì)象不歸你 Spring 管理,我 Mybatis 自己就可以管理了,你 Spring 就別瞎參合了。
sql 執(zhí)行完之后,Mybatis 會(huì)自動(dòng)幫我們 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 事務(wù)管理,那么會(huì)強(qiáng)制執(zhí)行 commit 操作,我們點(diǎn)進(jìn)去,發(fā)現(xiàn)最終調(diào)用的是 Mybatis 的事務(wù)管理器的 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(); } }
問題就出現(xiàn)在這里,前面我也說了,我們使用的 druid 數(shù)據(jù)庫連接池的 autoCommit 默認(rèn)為 true,而事務(wù)管理器獲取 connection 對(duì)象時(shí),又將 connection 的 autocommit 賦值給事務(wù)管理器,如果此時(shí) autoCommit 為 true,Mybatis 認(rèn)為 connection 已經(jīng)自動(dòng)提交事務(wù)了,既然這事不歸我管,那么我 Mybatis 自然就不會(huì)再去 commit 了。
根據(jù)測試結(jié)果,將 druid 的 autoCommit 設(shè)置為 false 后,不會(huì)發(fā)生阻塞現(xiàn)象,即 Mybaits 會(huì)執(zhí)行下面的 commit 操作。那么問題來了,connection 的 autocommit = true 時(shí),到底有沒有 commit ?從測試結(jié)果來看,很明顯沒有 commit。這里就要從數(shù)據(jù)庫層來解釋了,由于公司 Oracle 數(shù)據(jù)庫的 autocommit 使用的是默認(rèn)的 false 值,即需要顯式提交 commit 事務(wù)才會(huì)被提交。這也就是為什么當(dāng) druid 的 autoCommit=false 時(shí),并發(fā)執(zhí)行不會(huì)產(chǎn)生阻塞現(xiàn)象,因?yàn)?Mybatis 已經(jīng)幫我們自動(dòng) commit 了。
而為什么當(dāng) druid 的 autoCommit=true 時(shí),Mysql 依然不會(huì)阻塞呢?我先開啟 Mysql 的日志打印:
set global general_log = 1;
查看日志,發(fā)現(xiàn) Mysql 會(huì)為每條執(zhí)行的 sql 設(shè)置 autocommit=1,即自動(dòng)提交事務(wù),無須顯式提交 commit,每條 sql 就是一個(gè)事務(wù)。
Spring 事務(wù)管理器上面的案例分析中,加了 Spring 事務(wù)的并發(fā)執(zhí)行,并不會(huì)產(chǎn)生阻塞現(xiàn)象,顯然肯定是 Spring 事務(wù)做了一些不可描述的動(dòng)作,Spring 的事務(wù)管理器有很多個(gè),這里我們用的是數(shù)據(jù)庫連接池那個(gè)管理器,叫 DataSourceTransactionManager,我這里為了靈活控制事務(wù)范圍的細(xì)粒度,用的是聲明式事務(wù),我們繼續(xù)走一波源碼,從事務(wù)入口一路跟蹤進(jìn)來,發(fā)現(xiàn)第一步需要調(diào)用 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); }
我們?cè)?doBegin 方法發(fā)現(xiàn)了它偷偷地篡改了連接對(duì)象 autoCommit 的值,將它設(shè)為 false,這里想必大家都會(huì)明白其中的原理吧,Spring 管理事務(wù)其實(shí)就是在 sql 執(zhí)行前將當(dāng)前的 connection 對(duì)象設(shè)置為不自動(dòng)提交模式,接下來執(zhí)行的 sql 都不會(huì)自動(dòng)提交,等待事務(wù)結(jié)束時(shí),Spring 事務(wù)管理器會(huì)幫我們 commit 提交事務(wù)。這也就是為什么加了 Spring 事務(wù)的并發(fā)執(zhí)行并不會(huì)產(chǎn)生阻塞的原因,原理與上述 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); }
在事務(wù)完成之后,我們還需要將 connection 對(duì)象還原,因?yàn)?connection 存在于連接池當(dāng)中,close 時(shí)并不會(huì)真正關(guān)閉,而是被回收回連接池當(dāng)中了,如果不對(duì) connection 對(duì)象進(jìn)行還原,那么當(dāng)下一次會(huì)話拿到該 connection 對(duì)象,autoCommit 還是上一次會(huì)話的值,就會(huì)產(chǎn)生一些很隱晦的問題。
寫在最后其實(shí)這個(gè)問題從應(yīng)用層來分析還好,直接擼源碼就完了,主要是這個(gè)問題還涉及一些數(shù)據(jù)庫底層的一些原理,由于我對(duì)數(shù)據(jù)庫還不是那么地專業(yè),所以在這過程中,還特別請(qǐng)教了公司的 DBA 斌哥哥,非常感謝他的協(xié)助。
另外,我其實(shí)是不太建議使用 for update 這種悲觀鎖的,它太過于依賴數(shù)據(jù)庫層了,而且當(dāng)并發(fā)量起來了,雖然可以保證數(shù)據(jù)一致性,但是這樣犧牲了性能,會(huì)大大影響效率,嚴(yán)重拖垮數(shù)據(jù)庫資源,而且像這次一樣,有些開發(fā)人員使用了 for update 卻忘記 commit 事務(wù)了,導(dǎo)致引起很多鎖故障。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/74765.html
摘要:在群里討論,然后得出了這幾種寫法,感覺是層層遞進(jìn),想了想,最后選擇發(fā)布成文章大頭兒子小頭爸爸叫去吃飯大頭兒子小頭爸爸叫去吃飯大頭兒子小頭爸爸叫去吃飯吃完了背小頭兒子回去正在牽著的手正在吃給所有對(duì)象擴(kuò)展一個(gè)繼承的方法繼承爸爸要繼承人的功能正在 在群里討論JavaScript,然后得出了這幾種寫法,感覺是層層遞進(jìn),想了想,最后選擇發(fā)布成文章 ({ baby : 大頭兒子, ...
摘要:背景項(xiàng)目中通過遠(yuǎn)程調(diào)用服務(wù)框架調(diào)用了許多其它的服務(wù)其中有一個(gè)服務(wù)需要升級(jí)其升級(jí)不是版本上的升級(jí)而是整個(gè)服務(wù)重新取了一個(gè)名字使用的也是全新的包但是調(diào)用的方法沒有改變因此在升級(jí)時(shí)只是在調(diào)用服務(wù)類中修改了調(diào)用地址和調(diào)用返回實(shí)體由改為該中返回該調(diào)用 背景 項(xiàng)目中通過遠(yuǎn)程調(diào)用服務(wù)框架調(diào)用了許多其它的服務(wù),其中有一個(gè)服務(wù)wx/subscribe/contract/CircleService 需要升...
摘要:通過查看的文檔可以發(fā)現(xiàn)整個(gè)分為個(gè)階段定時(shí)器相關(guān)任務(wù),中我們關(guān)注的是它會(huì)執(zhí)行和中到期的回調(diào)執(zhí)行某些系統(tǒng)操作的回調(diào)內(nèi)部使用執(zhí)行,一定條件下會(huì)在這個(gè)階段阻塞住執(zhí)行的回調(diào)如果或者關(guān)閉了,就會(huì)在這個(gè)階段觸發(fā)事件,執(zhí)行事件的回調(diào)的代碼在文件中。 showImg(https://segmentfault.com/img/bVbd7B7?w=1227&h=644); 這次我們就不要那么多前戲,直奔主題...
閱讀 2746·2021-11-16 11:45
閱讀 1654·2021-09-26 10:19
閱讀 2051·2021-09-13 10:28
閱讀 2803·2021-09-08 10:46
閱讀 1530·2021-09-07 10:13
閱讀 1525·2019-08-30 13:50
閱讀 1374·2019-08-30 11:17
閱讀 1455·2019-08-29 13:18