當(dāng)Transactional遇上synchronized的解決方法分享
問(wèn)題情形
假設(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();
}問(wèn)題分析
首先,在 getTest1() 和 getTest2() 這兩個(gè)方法中都加了 @Transactional 注解,因此它們會(huì)分別開(kāi)啟自己的事務(wù)。假設(shè)在某一刻,兩個(gè)線程同時(shí)調(diào)用了 /t1 和 /t2 接口,并且 /t1 接口中執(zhí)行了較長(zhǎng)的睡眠操作,于是 /t2 的業(yè)務(wù)邏輯率先完成,并且更新了數(shù)據(jù)庫(kù)中的Number字段。
隨后 /t1 的業(yè)務(wù)邏輯也完成了,但是由于之前被阻塞了 60 秒鐘,此時(shí)讀取到的計(jì)數(shù)器值已經(jīng)過(guò)期了,不能反映最新的狀態(tài)。因此 /t1 返回的結(jié)果將是過(guò)期的數(shù)據(jù),與 /t2 返回的結(jié)果不一致。所以這段代碼存在一個(gè)并發(fā)問(wèn)題,可能導(dǎo)致數(shù)據(jù)的不一致性。
解決方法
這里我給出常用的解決方法:
- 把
getCount()方法上的synchronized去掉,使用樂(lè)觀鎖的方式來(lái)控制并發(fā)訪問(wèn)。 - 將
/t1和/t2兩個(gè)接口的事務(wù)設(shè)置為同一個(gè)事務(wù),即兩個(gè)接口共享同一個(gè)事務(wù)上下文??梢酝ㄟ^(guò) Spring 的聲明式事務(wù)管理機(jī)制來(lái)實(shí)現(xiàn)。(在 Spring 框架中,我們可以使用 @Transactional 注解來(lái)聲明式地管理事務(wù)。使用PROPAGATION_REQUIRED屬性可以表示當(dāng)前方法需要加入到一個(gè)存在的事務(wù)中,如果不存在,則開(kāi)啟新的事務(wù)。) - 在
getCount()方法中,進(jìn)行增量更新,而不是直接把Number字段加 1,例如使用update count_num set number = number + 1 where id = ?等 SQL 語(yǔ)句來(lái)實(shí)現(xiàn)。 - 保留
synchronized關(guān)鍵字的情況下,添加事務(wù)管理器進(jìn)行手動(dòng)事務(wù)管理。
代碼參考
1.樂(lè)觀鎖方案
@Override
@Transactional(rollbackFor = Exception.class)
public String getCount() {
CountNum countNum = countNumMapper.selectById(1);
// 使用版本號(hào)作為樂(lè)觀鎖
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 語(yǔ)句進(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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot 引?MybatisGenerator的實(shí)現(xiàn)步驟
本文主要介紹了SpringBoot 引?MybatisGenerator的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
Mybatis-Plus Wrapper條件構(gòu)造器超詳細(xì)使用教程
接口方法的參數(shù)中,會(huì)出現(xiàn)各種 Wrapper,比如 queryWrapper、updateWrapper 等。Wrapper 的作用就是用于定義各種各樣的條件(where)。所以不管是查詢、更新、刪除都會(huì)用到Wrapper2022-03-03
顯示IntelliJ IDEA工具的Run Dashboard功能圖文詳解
這篇文章主要介紹了顯示IntelliJ IDEA工具的Run Dashboard功能,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07
基于Java實(shí)現(xiàn)的Dijkstra算法示例
這篇文章主要介紹了基于Java實(shí)現(xiàn)的Dijkstra算法示例,一個(gè)比較典型的算法示例,需要的朋友可以參考下2014-07-07
Java實(shí)現(xiàn)對(duì)象排序的兩種方式詳解
這篇文章主要介紹了Java實(shí)現(xiàn)對(duì)象排序的兩種方式詳解,在Java中經(jīng)常會(huì)涉及到對(duì)象數(shù)組的排序問(wèn)題,則就提到對(duì)象之間的比較問(wèn)題,今天我們就來(lái)看一下兩種不同排序方式之間的區(qū)別,需要的朋友可以參考下2023-09-09
關(guān)于Gateway網(wǎng)關(guān)中配置跨域的三種方案
文章總結(jié):介紹了三種處理跨域請(qǐng)求的方法:在Controller類上添加注解、通過(guò)配置類實(shí)現(xiàn)重寫WebMvcConfigurer接口和在配置文件中統(tǒng)一設(shè)置,希望這些方法能幫助讀者解決跨域問(wèn)題2024-11-11
springAop實(shí)現(xiàn)講解(看這篇夠了)
AOP面向切面編程是一種編程范式,它通過(guò)將通用的橫切關(guān)注點(diǎn)(如日志、事務(wù)、權(quán)限控制等)與業(yè)務(wù)邏輯分離,使得代碼更加清晰、簡(jiǎn)潔、易于維護(hù),這篇文章主要介紹了springAop實(shí)現(xiàn)講解(看這篇夠了),需要的朋友可以參考下2024-02-02

