Spring事務原理解析
前言
最近在編寫公司APP產(chǎn)品的商品砍價功能,其中有一個接口涉及并發(fā)訪問。自測時通過ApiFox接口管理工具進行壓測,落地數(shù)據(jù)時出現(xiàn)了"鎖失效"的情景。十分感謝后端小伙伴的幫助排查,解決了這個問題。
問題描述
并發(fā)接口中,先對主表數(shù)據(jù)進行讀取,進行業(yè)務判斷后,新增、修改它表的數(shù)據(jù)。在理應串行執(zhí)行的情況下發(fā)生了多個請求線程讀取到了相同的主表數(shù)據(jù),導致數(shù)據(jù)處理異常。也正是前言中所說的"鎖失效"了。(實際情況加鎖操作是有效的)
代碼復現(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";
}
- 最少的代碼進行演示,Controller方法體中的內(nèi)容應是Service中的代碼
- DistributedLock中封裝的Redission
- 通過將先讀后改的方式演示,實際中本質就是進行了這樣的操作,但會存在更多的業(yè)務代碼(不演示新增的情況)
ApiFox中通過創(chuàng)建100個請求線程進行壓測,最終concurrent_read_uncommit表中的num字段值為94,而非100。
排查
1. 鎖失效
新編寫了兩個簡單接口,第一個接口加鎖,并線程休眠30秒后釋放鎖。另一個接口加同樣的鎖,打印一條語句后直接返回。先調(diào)用第一個接口,在調(diào)用第二個接口。Debug中發(fā)現(xiàn)鎖是有效的,在redis中存有鎖Key。并且訪問第二個接口時,線程被阻塞在了加鎖行代碼。
2. 事務隔離級別
查詢數(shù)據(jù)庫事務默認隔離級別:
select @@tx_isolation;
結果
REPEATABLE-READ
就是默認的RR級別,那么說明同個事務內(nèi)多次讀取數(shù)據(jù)都會是一樣的,不會讀取到臟數(shù)據(jù)。
3. 修改Spring事務傳播配置
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
與這個并沒有關系,八竿子打不著。當時的想法時是多個并發(fā)請求在進入到了同個事務內(nèi),并一起讀取到了沒有被修改前的數(shù)據(jù)。細想想:
- 事務傳播配置一般用在不同事務方法間產(chǎn)生調(diào)用時的事務決策,是共用事務還是新創(chuàng)建事務,亦或是其他的方式進行處理
- test方法本身為根方法,也沒有調(diào)用其他的事務方法,所以無需配置事務傳播配置
- 即便不在同一事務內(nèi),依舊能查詢到其他事務修改但未提交的相同數(shù)據(jù)
解決方案
在鎖代碼塊中調(diào)用事務方法,而不是在事務方法中進行加鎖。
原因為:并發(fā)情境下,執(zhí)行速度過快,很有可能發(fā)生:請求線程在釋放鎖后沒有來得及提交事務,另一個請求線程在加鎖處被喚醒,繼而讀取到了事務未提交的數(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");
}
}
- 將需要加鎖的事務代碼進行提取另一個方法
- 調(diào)用方法中進行加鎖,并且必須要去掉事務注解
- 因為是在非事務方法調(diào)用事務方法,為了保證事務生效,需要通過事務代理Bean進行調(diào)用
這樣就保證了不會讀取到事務未提交的數(shù)據(jù),同時又具有鎖的排他性。
其實鎖一直都是有效的,本質原因就在于Spring的事務代理Bean屏蔽了事務代碼。我們不能手動的進行控制,也就是說你變更了不了事務代碼的順序。如果能將提交事務的行代碼寫到釋放鎖之前,就不會存在這個問題了。所以,也可以通過編程式事務解決這個問題,關于編程式事務,Spring也有做代碼封裝。如果不通過編程式事務,那么就只能通過上述代碼變相的來實現(xiàn)。
到此這篇關于Spring事務原理解析的文章就介紹到這了,更多相關Spring事務內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot線程池監(jiān)控的簡單實現(xiàn)
本文主要介紹了springboot線程池監(jiān)控的簡單實現(xiàn),文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01
Java微服務Filter過濾器集成Sentinel實現(xiàn)網(wǎng)關限流過程詳解
這篇文章主要介紹了Java微服務Filter過濾器集成Sentinel實現(xiàn)網(wǎng)關限流過程,首先Sentinel規(guī)則的存儲默認是存儲在內(nèi)存的,應用重啟之后規(guī)則會丟失。因此我們通過配置中心Nacos保存規(guī)則,然后通過定時拉取Nacos數(shù)據(jù)來獲取規(guī)則配置,可以做到動態(tài)實時的刷新規(guī)則2023-02-02
Java中數(shù)據(jù)轉換及字符串的“+”操作方法
本文主要介紹了Java中的數(shù)據(jù)類型轉換,包括隱式轉換和強制轉換,隱式轉換通常用于將范圍較小的數(shù)據(jù)類型轉換為范圍較大的數(shù)據(jù)類型,而強制轉換則是將范圍較大的數(shù)據(jù)類型轉換為范圍較小的數(shù)據(jù)類型,本文介紹Java中數(shù)據(jù)轉換以及字符串的“+”操作,感興趣的朋友一起看看吧2024-10-10

