當(dāng)Transactional遇上synchronized的解決方法分享
問題情形
假設(shè)代碼如下:
//controller層: @GetMapping("/t1") @Transactional(rollbackFor = Exception.class) public void getTest1() { String n = countNumService.getCount(); System.out.println(" t1 : " + n); try { Thread.sleep(60000); } catch (InterruptedException e) { throw new RuntimeException(e); } } @GetMapping("/t2") @Transactional(rollbackFor = Exception.class) public void getTest2() { String n = countNumService.getCount(); System.out.println(" t2 : " + n); // 忽略其他的增刪操作 } //service層: @Override @Transactional(rollbackFor = Exception.class) public synchronized String getCount() { // 獲取 CountNum countNum = countNumMapper.selectById(1); countNum.setNumber(countNum.getNumber() + 1); // 修改 countNumMapper.updateById(countNum); return countNum.getNumber().toString(); }
問題分析
首先,在 getTest1()
和 getTest2()
這兩個(gè)方法中都加了 @Transactional
注解,因此它們會(huì)分別開啟自己的事務(wù)。假設(shè)在某一刻,兩個(gè)線程同時(shí)調(diào)用了 /t1
和 /t2
接口,并且 /t1
接口中執(zhí)行了較長的睡眠操作,于是 /t2
的業(yè)務(wù)邏輯率先完成,并且更新了數(shù)據(jù)庫中的Number
字段。
隨后 /t1
的業(yè)務(wù)邏輯也完成了,但是由于之前被阻塞了 60 秒鐘,此時(shí)讀取到的計(jì)數(shù)器值已經(jīng)過期了,不能反映最新的狀態(tài)。因此 /t1
返回的結(jié)果將是過期的數(shù)據(jù),與 /t2
返回的結(jié)果不一致。所以這段代碼存在一個(gè)并發(fā)問題,可能導(dǎo)致數(shù)據(jù)的不一致性
。
解決方法
這里我給出常用的解決方法:
- 把
getCount()
方法上的synchronized
去掉,使用樂觀鎖的方式來控制并發(fā)訪問。 - 將
/t1
和/t2
兩個(gè)接口的事務(wù)設(shè)置為同一個(gè)事務(wù),即兩個(gè)接口共享同一個(gè)事務(wù)上下文??梢酝ㄟ^ Spring 的聲明式事務(wù)管理機(jī)制來實(shí)現(xiàn)。(在 Spring 框架中,我們可以使用 @Transactional 注解來聲明式地管理事務(wù)。使用PROPAGATION_REQUIRED
屬性可以表示當(dāng)前方法需要加入到一個(gè)存在的事務(wù)中,如果不存在,則開啟新的事務(wù)。) - 在
getCount()
方法中,進(jìn)行增量更新,而不是直接把Number
字段加 1,例如使用update count_num set number = number + 1 where id = ?
等 SQL 語句來實(shí)現(xiàn)。 - 保留
synchronized
關(guān)鍵字的情況下,添加事務(wù)管理器進(jìn)行手動(dòng)事務(wù)管理。
代碼參考
1.樂觀鎖方案
@Override @Transactional(rollbackFor = Exception.class) public String getCount() { CountNum countNum = countNumMapper.selectById(1); // 使用版本號(hào)作為樂觀鎖 int version = countNum.getVersion(); countNum.setNumber(countNum.getNumber() + 1); countNum.setVersion(version + 1); // 更新操作必須要包含版本號(hào)字段 int rows = countNumMapper.updateById(countNum); if (rows == 0) { throw new OptimisticLockException("事務(wù)中更新失敗"); } return countNum.getNumber().toString(); }
2.將 /t1
和 /t2
兩個(gè)接口的事務(wù)設(shè)置為同一個(gè)事務(wù)。在 CountNumService
類的事務(wù)注解上添加了 propagation = Propagation.REQUIRED
屬性,表示當(dāng)前方法需要加入到一個(gè)已存在的事務(wù)中。此時(shí),如果 /t1
和 /t2
調(diào)用的是同一個(gè) CountNumService
實(shí)例,則它們將共享同一個(gè)事務(wù)上下文。
@Service public class CountNumService { @Autowired private CountNumMapper countNumMapper; @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public String getCount() { // 與之前代碼相同 } } @RestController public class CountNumController { @Autowired private CountNumService countNumService; @GetMapping("/t1") public void getTest1() { String n = countNumService.getCount(); System.out.println(" t1 : " + n); try { Thread.sleep(60000); } catch (InterruptedException e) { throw new RuntimeException(e); } } @GetMapping("/t2") public void getTest2() { String n = countNumService.getCount(); System.out.println(" t2 : " + n); // 忽略其他的增刪操作 } }
3.SQL中增量更新方案
@Mapper public interface CountNumMapper { @Update("UPDATE count_num SET number = number + 1 WHERE id = 1") int updateNumber(); } @Override @Transactional(rollbackFor = Exception.class) public String getCount() { // 直接執(zhí)行 SQL 語句進(jìn)行增量更新操作 countNumMapper.updateNumber(); // 再查詢一次獲取最新值 CountNum countNum = countNumMapper.selectById(1); return countNum.getNumber().toString(); }
4.手動(dòng)事務(wù)管理方案
@Service public class CountNumService { @Autowired private CountNumMapper countNumMapper; // 使用spring事務(wù)管理器 @Autowired private PlatformTransactionManager transactionManager; public synchronized String getCount() { TransactionStatus status = null; try { DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); //將傳播行為設(shè)置為 PROPAGATION_REQUIRED,以確保當(dāng)前方法在事務(wù)內(nèi)執(zhí)行: definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); //獲取當(dāng)前事務(wù)的狀態(tài)信息: status = transactionManager.getTransaction(definition); CountNum countNum = countNumMapper.selectById(1); countNum.setNumber(countNum.getNumber() + 1); int rows = countNumMapper.updateById(countNum); if (rows == 0) { throw new RuntimeException("事務(wù)中更新失敗"); } //提交事務(wù): transactionManager.commit(status); return countNum.getNumber().toString(); } catch (Exception e) { if (status != null) { //回滾事務(wù): transactionManager.rollback(status); } throw e; } } }
到此這篇關(guān)于當(dāng)Transactional遇上synchronized的解決方法分享的文章就介紹到這了,更多相關(guān)Transactional synchronized內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何基于JWT實(shí)現(xiàn)接口的授權(quán)訪問詳解
授權(quán)是最常見的JWT使用場景,下面這篇文章主要給大家介紹了關(guān)于如何基于JWT實(shí)現(xiàn)接口的授權(quán)訪問的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-02-02應(yīng)用Java泛型和反射導(dǎo)出CSV文件的方法
這篇文章主要介紹了應(yīng)用Java泛型和反射導(dǎo)出CSV文件的方法,通過一個(gè)自定義函數(shù)結(jié)合泛型與反射的應(yīng)用實(shí)現(xiàn)導(dǎo)出CSV文件的功能,具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2014-12-12Java 利用枚舉實(shí)現(xiàn)接口進(jìn)行統(tǒng)一管理
這篇文章主要介紹了Java 利用枚舉實(shí)現(xiàn)接口進(jìn)行統(tǒng)一管理,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02基于SpringBoot接口+Redis解決用戶重復(fù)提交問題
當(dāng)網(wǎng)絡(luò)延遲的情況下用戶多次點(diǎn)擊submit按鈕導(dǎo)致表單重復(fù)提交,用戶提交表單后,點(diǎn)擊瀏覽器的【后退】按鈕回退到表單頁面后進(jìn)行再次提交也會(huì)出現(xiàn)用戶重復(fù)提交,辦法有很多,我這里只說一種,利用Redis的set方法搞定,需要的朋友可以參考下2023-10-10