Spring事務(wù)原理解析
前言
最近在編寫(xiě)公司APP產(chǎn)品的商品砍價(jià)功能,其中有一個(gè)接口涉及并發(fā)訪問(wèn)。自測(cè)時(shí)通過(guò)ApiFox接口管理工具進(jìn)行壓測(cè),落地?cái)?shù)據(jù)時(shí)出現(xiàn)了"鎖失效"的情景。十分感謝后端小伙伴的幫助排查,解決了這個(gè)問(wèn)題。
問(wèn)題描述
并發(fā)接口中,先對(duì)主表數(shù)據(jù)進(jìn)行讀取,進(jìn)行業(yè)務(wù)判斷后,新增、修改它表的數(shù)據(jù)。在理應(yīng)串行執(zhí)行的情況下發(fā)生了多個(gè)請(qǐng)求線程讀取到了相同的主表數(shù)據(jù),導(dǎo)致數(shù)據(jù)處理異常。也正是前言中所說(shuō)的"鎖失效"了。(實(shí)際情況加鎖操作是有效的)
代碼復(fù)現(xiàn)
@RequestMapping("/test")
@Transactional(rollbackFor = Exception.class)
public String test() {
DistributedLock.lock("ct_lock");
try {
Map<String, Object> resultMap = jdbcTemplate.queryForMap("select * from concurrent_read_uncommit");
int num = Integer.parseInt(resultMap.get("num").toString());
num++;
jdbcTemplate.update("update concurrent_read_uncommit set num = " + num);
} finally {
DistributedLock.unlock("ct_lock");
}
return "success";
}
- 最少的代碼進(jìn)行演示,Controller方法體中的內(nèi)容應(yīng)是Service中的代碼
- DistributedLock中封裝的Redission
- 通過(guò)將先讀后改的方式演示,實(shí)際中本質(zhì)就是進(jìn)行了這樣的操作,但會(huì)存在更多的業(yè)務(wù)代碼(不演示新增的情況)
ApiFox中通過(guò)創(chuàng)建100個(gè)請(qǐng)求線程進(jìn)行壓測(cè),最終concurrent_read_uncommit表中的num字段值為94,而非100。
排查
1. 鎖失效
新編寫(xiě)了兩個(gè)簡(jiǎn)單接口,第一個(gè)接口加鎖,并線程休眠30秒后釋放鎖。另一個(gè)接口加同樣的鎖,打印一條語(yǔ)句后直接返回。先調(diào)用第一個(gè)接口,在調(diào)用第二個(gè)接口。Debug中發(fā)現(xiàn)鎖是有效的,在redis中存有鎖Key。并且訪問(wèn)第二個(gè)接口時(shí),線程被阻塞在了加鎖行代碼。
2. 事務(wù)隔離級(jí)別
查詢(xún)數(shù)據(jù)庫(kù)事務(wù)默認(rèn)隔離級(jí)別:
select @@tx_isolation;
結(jié)果
REPEATABLE-READ
就是默認(rèn)的RR級(jí)別,那么說(shuō)明同個(gè)事務(wù)內(nèi)多次讀取數(shù)據(jù)都會(huì)是一樣的,不會(huì)讀取到臟數(shù)據(jù)。
3. 修改Spring事務(wù)傳播配置
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
與這個(gè)并沒(méi)有關(guān)系,八竿子打不著。當(dāng)時(shí)的想法時(shí)是多個(gè)并發(fā)請(qǐng)求在進(jìn)入到了同個(gè)事務(wù)內(nèi),并一起讀取到了沒(méi)有被修改前的數(shù)據(jù)。細(xì)想想:
- 事務(wù)傳播配置一般用在不同事務(wù)方法間產(chǎn)生調(diào)用時(shí)的事務(wù)決策,是共用事務(wù)還是新創(chuàng)建事務(wù),亦或是其他的方式進(jìn)行處理
- test方法本身為根方法,也沒(méi)有調(diào)用其他的事務(wù)方法,所以無(wú)需配置事務(wù)傳播配置
- 即便不在同一事務(wù)內(nèi),依舊能查詢(xún)到其他事務(wù)修改但未提交的相同數(shù)據(jù)
解決方案
在鎖代碼塊中調(diào)用事務(wù)方法,而不是在事務(wù)方法中進(jìn)行加鎖。
原因?yàn)椋翰l(fā)情境下,執(zhí)行速度過(guò)快,很有可能發(fā)生:請(qǐng)求線程在釋放鎖后沒(méi)有來(lái)得及提交事務(wù),另一個(gè)請(qǐng)求線程在加鎖處被喚醒,繼而讀取到了事務(wù)未提交的數(shù)據(jù)。即讀取到了臟數(shù)據(jù),產(chǎn)生了"鎖失效"的效果。
修正代碼:
@RequestMapping("/test2")
public String test2() {
ConcurrentTransactionalController proxyBean = SpringContextUtils.getBean(this.getClass());
proxyBean.doTest2();
return "success";
}
@Transactional(rollbackFor = Exception.class)
public void doTest2() {
DistributedLock.lock("ct_lock");
try {
Map<String, Object> resultMap = jdbcTemplate.queryForMap("select * from concurrent_read_uncommit");
int num = Integer.parseInt(resultMap.get("num").toString());
num++;
jdbcTemplate.update("update concurrent_read_uncommit set num = " + num);
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
DistributedLock.unlock("ct_lock");
}
}
- 將需要加鎖的事務(wù)代碼進(jìn)行提取另一個(gè)方法
- 調(diào)用方法中進(jìn)行加鎖,并且必須要去掉事務(wù)注解
- 因?yàn)槭窃诜鞘聞?wù)方法調(diào)用事務(wù)方法,為了保證事務(wù)生效,需要通過(guò)事務(wù)代理Bean進(jìn)行調(diào)用
這樣就保證了不會(huì)讀取到事務(wù)未提交的數(shù)據(jù),同時(shí)又具有鎖的排他性。
其實(shí)鎖一直都是有效的,本質(zhì)原因就在于Spring的事務(wù)代理Bean屏蔽了事務(wù)代碼。我們不能手動(dòng)的進(jìn)行控制,也就是說(shuō)你變更了不了事務(wù)代碼的順序。如果能將提交事務(wù)的行代碼寫(xiě)到釋放鎖之前,就不會(huì)存在這個(gè)問(wèn)題了。所以,也可以通過(guò)編程式事務(wù)解決這個(gè)問(wèn)題,關(guān)于編程式事務(wù),Spring也有做代碼封裝。如果不通過(guò)編程式事務(wù),那么就只能通過(guò)上述代碼變相的來(lái)實(shí)現(xiàn)。
到此這篇關(guān)于Spring事務(wù)原理解析的文章就介紹到這了,更多相關(guān)Spring事務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IDEA中maven依賴(lài)報(bào)紅的問(wèn)題解決辦法
這篇文章主要給大家介紹了關(guān)于IDEA中maven依賴(lài)報(bào)紅的問(wèn)題解決辦法,在使用IDEA過(guò)程中,經(jīng)常會(huì)出現(xiàn)maven依賴(lài)報(bào)紅的問(wèn)題,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07
SpringBoot獲取配置文件的簡(jiǎn)單實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于SpringBoot如何獲取配置文件的簡(jiǎn)單實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
springboot線程池監(jiān)控的簡(jiǎn)單實(shí)現(xiàn)
本文主要介紹了springboot線程池監(jiān)控的簡(jiǎn)單實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
Spring?WebFlux怎么進(jìn)行異常處理源碼解析
這篇文章主要為大家介紹了Spring?WebFlux怎么進(jìn)行異常處理源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
Java微服務(wù)Filter過(guò)濾器集成Sentinel實(shí)現(xiàn)網(wǎng)關(guān)限流過(guò)程詳解
這篇文章主要介紹了Java微服務(wù)Filter過(guò)濾器集成Sentinel實(shí)現(xiàn)網(wǎng)關(guān)限流過(guò)程,首先Sentinel規(guī)則的存儲(chǔ)默認(rèn)是存儲(chǔ)在內(nèi)存的,應(yīng)用重啟之后規(guī)則會(huì)丟失。因此我們通過(guò)配置中心Nacos保存規(guī)則,然后通過(guò)定時(shí)拉取Nacos數(shù)據(jù)來(lái)獲取規(guī)則配置,可以做到動(dòng)態(tài)實(shí)時(shí)的刷新規(guī)則2023-02-02
java數(shù)據(jù)類(lèi)型與二進(jìn)制詳細(xì)介紹
這篇文章主要介紹了java數(shù)據(jù)類(lèi)型與二進(jìn)制詳細(xì)介紹的相關(guān)資料,這里對(duì)數(shù)據(jù)類(lèi)型進(jìn)行了一一介紹分析,并說(shuō)明自動(dòng)轉(zhuǎn)換和強(qiáng)制轉(zhuǎn)換,需要的朋友可以參考下2017-07-07
Java中數(shù)據(jù)轉(zhuǎn)換及字符串的“+”操作方法
本文主要介紹了Java中的數(shù)據(jù)類(lèi)型轉(zhuǎn)換,包括隱式轉(zhuǎn)換和強(qiáng)制轉(zhuǎn)換,隱式轉(zhuǎn)換通常用于將范圍較小的數(shù)據(jù)類(lèi)型轉(zhuǎn)換為范圍較大的數(shù)據(jù)類(lèi)型,而強(qiáng)制轉(zhuǎn)換則是將范圍較大的數(shù)據(jù)類(lèi)型轉(zhuǎn)換為范圍較小的數(shù)據(jù)類(lèi)型,本文介紹Java中數(shù)據(jù)轉(zhuǎn)換以及字符串的“+”操作,感興趣的朋友一起看看吧2024-10-10

