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