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





