javascript
要不来重新认识Spring事务?三歪又学到了
從唯一性說起
寫了十幾年代碼,直到現在,我見過非常多的處理唯一性約束的方法都是放在代碼里,而非數據庫里。
直到現在我也一直很困惑,這些人為什么不使用數據庫的唯一索引呢?不過我并不想知道這個答案。
他們的做法很簡單,假如要保證name是唯一的,先使用Java代碼執行一個查詢語句:
select?*?from?example?where?name?=??然后根據返回值來判斷,如果是null則表明沒有這個name,接著執行插入語句即可:
insert?into?example(name)?values(?)如果不是null則表明這個name已經存在,那就返回name已存在的提示。
如果系統并發很小或者不是人為故意測試,這種方式完全沒有問題。
然而事實證明的是,還是偶爾會遇到問題,會出現name一樣的記錄。
類似這樣的情況還有抽獎問題,那就是判斷獎品是否還有剩余。
他們通常的做法也是先查詢獎品剩余數量,如下這樣:
select?remain_count?from?example?where?id?=??然后判斷返回值,如果大于0則表明獎品還有,則執行更新語句:
update?example?set?reamin_count?=?remain_count?-?1?where?id?=??如果不大于0則表明獎品沒有了,就返回獎品已經抽完的提示。
這種方案在獎品數量趨于0這個臨界值時一定會出問題,因為大部分抽獎都是有一定并發性的。
到最后會發現剩余獎品數量不是0而是負的,這些問題我都見過,好歹客戶不難纏,只需把多出的獎品錢掏了就行。
我實在想不通寫這些代碼的人是基于什么考慮的,這樣的寫法不僅代碼寫得多,而且也無法百分之百保證。
如果是我年輕的時候,一定會在心里“罵”這樣的代碼和寫代碼的人。
不過現在“老”了,很多事情都放得下了,權當“閉一只眼,再閉一只眼”了,況且我又不是項目經理。只要大方向不跑偏就行了。
也許這樣的人,人家就是把寫代碼當作一份糊口的工作而已,人家不愛好這個,不愿意想太多,我們也無可非議。
當然,我不使用這種方法,我一般會在數據庫里加上唯一索引,然后盡情的insert吧。
如果沒有唯一鍵沖突,那就一定會插入成功,如果有唯一鍵沖突,那就一定會拋異常,Spring把這個異常進行了轉化。
它就是DuplicateKeyException,我們只需try一下即可:
try?{xxxMapper.insertXXX(..);return?1; }?catch?(DuplicateKeyException?ex)?{log.warn(..);return?-1; }我們不去討論那種方法好,至少這種做法代碼寫的少,而且使用數據庫的唯一索引,絕對不會出現重復記錄。
我以為的我以為
如果有較大量數據需要插入的話,我們都會使用批量插入,如果使用Mybatis的話就是<foreach>標簽了。
但是有一個問題,如果插入的數據有重復的話,而且數據庫要求不能重復且還建了唯一索引,這時批量插入就沒法用了。
因為只要有一個唯一鍵沖突,這批數據都得完蛋。這其實沒有什么非常好的方法,不過可以先拿待插入數據進行檢測,把重復的直接排除掉。
但是需要寫更多的代碼,有些繁瑣。實在不行,只要時間上要求不高,還是采用單條插入吧。
我認為,如果有大量數據需要插入而且還要不重復,關鍵是數據里真有重復的,還是先對數據進行預處理,否則批量插入用不了,單條插入又非常耗時。
我就遇到了這樣的遺留問題,有重復的數據,所以不能使用批量插入,好歹數據量不大,那就單條單條的來吧。
按照我們的理解,單條數據唯一鍵沖突只影響這一條,肯定會拋異常,我們只要try/catch住,不會影響下一條的插入。當然,這是我以為的。
代碼當然是這樣寫的:
int?count?=?0; for?(XXX?xxx?:?xxxList)?{try?{xxxMapper.insertXXX(xxx);count++;}?catch?(DuplicateKeyException?ex)?{log.warn(..);} } return?count;先不要說for里面使用try/catch是不是合理,世界上哪有那么多的合理啊,快速解決問題才是王道,不合理的事情留到以后再說。
如果這樣真的可以的話,那也算是一種解決方法。可惜的是,一旦遇到唯一鍵沖突,異常雖然catch住了,但是事務照樣中止了,看來,“我以為的”還真成了我以為的。
我進行了多次其它嘗試,如catch更多的其它類型的異常,發現只能延遲事務的中止,但最后還是中止。我又在事務注解上設置不回滾某些類型的異常,發現還是不行。
多次嘗試之后,我放棄了,因為這是別人的或系統的遺留問題,沒有什么好的解決辦法,或者也改為別人的寫法,先查詢再插入,但是需要寫更多的代碼,也沒有太多時間了。
于是就決定不使用事務了,把事務注解去掉。問題得以解決了。后來還發現,這個方法被別的帶事務的方法調用了,默認又在事務里了,索性干脆直接使用注解標記為不支持事務。
掐斷了事務的傳播之后,這下真與事務絕緣了,世界清凈了。
所以,在從零開發新系統的時候,一定要多思考,不管是項目經理還是開發人員,一定要知道現在的某種做法會在日后帶來什么問題,如果什么都不想,日后必定會有很多奇葩的問題,簡直莫名其妙。
最終,我們不得不承認,沒有最爛的代碼,只有更爛的代碼。
重新認知Spring事務
說句心里話,這個事情真的讓我很意外,雖然我很少有“意外”,本以為可以的,結果卻是不行。于是我就仔細的思考。
Spring的事務給人的印象就是拋出了某些異常可以回滾,拋出了某些異常可以不回滾,而且是可以配置的,默認只回滾運行時異常。
這仿佛是在說明Spring可以catch住指定的異常,然后提交事務,或catch住某些異常,然后回滾事務,再把異常拋出給我們。
照這樣理解,那我們自己catch住異常豈不更好,不用勞Spring大駕,事實是不完全行的。由于Spring的事務行為是運行時通過生成子類注入的,所以沒有現成的源碼可看。
由于這件事,我又想起了我年輕時候的困惑,由于后來就不再想這個困惑了,所以一直沒有得到答案。
Spring把事務加在Service層的方法上,但很多時候,這些方法僅僅就是執行一個sql語句而已,無論是insert、update還是delete。
按照通常的理解,只有在涉及多個sql操作的時候才需要事務,這樣它們要么全部成功,要么有一個報錯就全部回滾,這也正是事務的原子性。
但是只有一個sql操作時,理論上不需要事務,因為它的成功與否并不會對別的sql產生影響,因為只有一個sql操作,默認就是原子的。而且一個sql操作,要么成功要么失敗,不會出現一半成功一半失敗的情況,這是數據庫保證的。
這個邏輯推理本身是沒有錯的,只是有些狹隘,因為我們把這個事務僅僅看作是數據庫的事務,僅僅把它限制在數據庫里了。這就是上面的一個疑惑的緣由,為什么只有一個sql操作也開啟事務。
Spring把事務加在Service層,其實是擴大了事務的范圍,把事務從數據庫里拿了出來,放到了Service層的Java代碼里了。讓我們的業務代碼也融入到了事務里。
我們可以先執行若干sql操作,沒有拋異常,然后再執行業務代碼,如果業務代碼拋了異常,Spring可以回滾事務,這樣先前的sql操作就撤銷了,宏觀來看sql操作和業務代碼就在一個事務里。
只不過很多時候我們沒有業務代碼,所以就只剩下一個sql操作了,因此也開著事務,這就解釋了前面的疑惑,為什么只有一個sql操作也開著事務。
于是我有一個大膽的猜測,Spring事務里說的“對哪些異常回滾和不回滾”這里的異常應該指的是業務代碼里拋出的異常,而不是對數據庫執行sql操作時拋出的異常。
因為執行業務代碼時拋出的某些異常可能并不影響對數據庫的操作,當然這是站在業務的角度來說的,所有Spring照樣可以提交事務,讓對數據庫的sql操作生效。
但是如果在對數據庫執行sql操作時拋出了異常,則一定會選擇回滾事務,畢竟這個事務是從數據庫里引出來然后擴大到整個業務層,而不是倒過來。
我感覺Spring可以通過異常類型來判斷是業務代碼拋出的還是數據庫操作拋出的,如果是業務代碼拋出的,我們可以自己catch住或配置為不回滾,則最終照樣提交事務。
如果是對數據庫執行操作時拋出的,則總是會回滾事務,即使我們自己catch住或配置為不回滾,也照樣沒有用,最后都會回滾,畢竟數據庫操作失敗,不應該再有任何幻想。
這樣就可以解釋本文開頭說的情況,雖然catch住了唯一鍵沖突異常或把該異常配置為不回滾,但是事務照樣中止。
注意,這些只是我的猜測,歡迎留言分享自己的看法或想法或猜測。
總結
以上是生活随笔為你收集整理的要不来重新认识Spring事务?三歪又学到了的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 别太把GitHub的Star当回事
- 下一篇: 用上 RocketMQ,系统性能提升了