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