Spring Boot 事務(wù)配置管理
本來(lái)已收錄到我寫的10萬(wàn)字Springboot經(jīng)典學(xué)習(xí)筆記中,筆記在持續(xù)更新……文末有領(lǐng)取方式
1. 事務(wù)相關(guān)
場(chǎng)景:我們?cè)陂_發(fā)企業(yè)應(yīng)用時(shí),由于數(shù)據(jù)操作在順序執(zhí)行的過(guò)程中,線上可能有各種無(wú)法預(yù)知的問(wèn)題,任何一步操作都有可能發(fā)生異常,異常則會(huì)導(dǎo)致后續(xù)的操作無(wú)法完成。此時(shí)由于業(yè)務(wù)邏輯并未正確的完成,所以在之前操作過(guò)數(shù)據(jù)庫(kù)的動(dòng)作并不可靠,需要在這種情況下進(jìn)行數(shù)據(jù)的回滾。
事務(wù)的作用就是為了保證用戶的每一個(gè)操作都是可靠的,事務(wù)中的每一步操作都必須成功執(zhí)行,只要有發(fā)生異常就回退到事務(wù)開始未進(jìn)行操作的狀態(tài)。這很好理解,轉(zhuǎn)賬、購(gòu)票等等,必須整個(gè)事件流程全部執(zhí)行完才能人為該事件執(zhí)行成功,不能轉(zhuǎn)錢轉(zhuǎn)到一半,系統(tǒng)死了,轉(zhuǎn)賬人錢沒(méi)了,收款人錢還沒(méi)到。
事務(wù)管理是 Spring Boot 框架中最為常用的功能之一,我們?cè)趯?shí)際應(yīng)用開發(fā)時(shí),基本上在 service 層處理業(yè)務(wù)邏輯的時(shí)候都要加上事務(wù),當(dāng)然了,有時(shí)候可能由于場(chǎng)景需要,也不用加事務(wù)(比如我們就要往一個(gè)表里插數(shù)據(jù),相互沒(méi)有影響,插多少是多少,不能因?yàn)槟硞€(gè)數(shù)據(jù)掛了,把之前插的全部回滾)。
2. Spring Boot 事務(wù)配置
2.1 依賴導(dǎo)入
在 Spring Boot 中使用事務(wù),需要導(dǎo)入 mysql 依賴:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
導(dǎo)入了 mysql 依賴后,Spring Boot 會(huì)自動(dòng)注入 DataSourceTransactionManager,我們不需要任何其他的配置就可以用 @Transactional 注解進(jìn)行事務(wù)的使用。關(guān)于 mybatis 的配置,在上一節(jié)課中已經(jīng)說(shuō)明了,這里還是使用上一節(jié)課中的 mybatis 配置即可。
2.2 事務(wù)的測(cè)試
我們首先在數(shù)據(jù)庫(kù)表中插入一條數(shù)據(jù):
| id | user_name | password |
|---|---|---|
| 1 | 倪升武 | 123456 |
然后我們寫一個(gè)插入的 mapper:
public interface UserMapper {
@Insert("insert into user (user_name, password) values (#{username}, #{password})")
Integer insertUser(User user);
}
OK,接下來(lái)我們來(lái)測(cè)試一下 Spring Boot 中的事務(wù)處理,在 service 層,我們手動(dòng)拋出個(gè)異常來(lái)模擬實(shí)際中出現(xiàn)的異常,然后觀察一下事務(wù)有沒(méi)有回滾,如果數(shù)據(jù)庫(kù)中沒(méi)有新的記錄,則說(shuō)明事務(wù)回滾成功。
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@Transactional
public void isertUser(User user) {
// 插入用戶信息
userMapper.insertUser(user);
// 手動(dòng)拋出異常
throw new RuntimeException();
}
}
我們來(lái)測(cè)試一下:
@RestController
public class TestController {
@Resource
private UserService userService;
@PostMapping("/adduser")
public String addUser(@RequestBody User user) throws Exception {
if (null != user) {
userService.isertUser(user);
return "success";
} else {
return "false";
}
}
}
我們使用 postman 調(diào)用一下該接口,因?yàn)樵诔绦蛑袙伋隽藗€(gè)異常,會(huì)造成事務(wù)回滾,我們刷新一下數(shù)據(jù)庫(kù),并沒(méi)有增加一條記錄,說(shuō)明事務(wù)生效了。事務(wù)很簡(jiǎn)單,我們平時(shí)在使用的時(shí)候,一般不會(huì)有多少問(wèn)題,但是并不僅僅如此……
3. 常見問(wèn)題總結(jié)
從上面的內(nèi)容中可以看出,Spring Boot 中使用事務(wù)非常簡(jiǎn)單,@Transactional 注解即可解決問(wèn)題,說(shuō)是這么說(shuō),但是在實(shí)際項(xiàng)目中,是有很多小坑在等著我們,這些小坑是我們?cè)趯懘a的時(shí)候沒(méi)有注意到,而且正常情況下不容易發(fā)現(xiàn)這些小坑,等項(xiàng)目寫大了,某一天突然出問(wèn)題了,排查問(wèn)題非常困難,到時(shí)候肯定是抓瞎,需要費(fèi)很大的精力去排查問(wèn)題。
這一小節(jié),我專門針對(duì)實(shí)際項(xiàng)目中經(jīng)常出現(xiàn)的,和事務(wù)相關(guān)的細(xì)節(jié)做一下總結(jié),希望讀者在讀完之后,能夠落實(shí)到自己的項(xiàng)目中,能有所受益。
3.1 異常并沒(méi)有被 ”捕獲“ 到
首先要說(shuō)的,就是異常并沒(méi)有被 ”捕獲“ 到,導(dǎo)致事務(wù)并沒(méi)有回滾。我們?cè)跇I(yè)務(wù)層代碼中,也許已經(jīng)考慮到了異常的存在,或者編輯器已經(jīng)提示我們需要拋出異常,但是這里面有個(gè)需要注意的地方:并不是說(shuō)我們把異常拋出來(lái)了,有異常了事務(wù)就會(huì)回滾,我們來(lái)看一個(gè)例子:
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@Transactional
public void isertUser2(User user) throws Exception {
// 插入用戶信息
userMapper.insertUser(user);
// 手動(dòng)拋出異常
throw new SQLException("數(shù)據(jù)庫(kù)異常");
}
}
我們看上面這個(gè)代碼,其實(shí)并沒(méi)有什么問(wèn)題,手動(dòng)拋出一個(gè) SQLException 來(lái)模擬實(shí)際中操作數(shù)據(jù)庫(kù)發(fā)生的異常,在這個(gè)方法中,既然拋出了異常,那么事務(wù)應(yīng)該回滾,實(shí)際卻不如此,讀者可以使用我源碼中 controller 的接口,通過(guò) postman 測(cè)試一下,就會(huì)發(fā)現(xiàn),仍然是可以插入一條用戶數(shù)據(jù)的。
那么問(wèn)題出在哪呢?因?yàn)?Spring Boot 默認(rèn)的事務(wù)規(guī)則是遇到運(yùn)行異常(RuntimeException)和程序錯(cuò)誤(Error)才會(huì)回滾。比如上面我們的例子中拋出的 RuntimeException 就沒(méi)有問(wèn)題,但是拋出 SQLException 就無(wú)法回滾了。針對(duì)非檢測(cè)異常,如果要進(jìn)行事務(wù)回滾的話,可以在 @Transactional 注解中使用 rollbackFor 屬性來(lái)指定異常,比如 @Transactional(rollbackFor = Exception.class),這樣就沒(méi)有問(wèn)題了,所以在實(shí)際項(xiàng)目中,一定要指定異常。
3.2 異常被 ”吃“ 掉
這個(gè)標(biāo)題很搞笑,異常怎么會(huì)被吃掉呢?還是回歸到現(xiàn)實(shí)項(xiàng)目中去,我們?cè)谔幚懋惓r(shí),有兩種方式,要么拋出去,讓上一層來(lái)捕獲處理;要么把異常 try catch 掉,在異常出現(xiàn)的地方給處理掉。就因?yàn)橛羞@中 try...catch,所以導(dǎo)致異常被 ”吃“ 掉,事務(wù)無(wú)法回滾。我們還是看上面那個(gè)例子,只不過(guò)簡(jiǎn)單修改一下代碼:
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void isertUser3(User user) {
try {
// 插入用戶信息
userMapper.insertUser(user);
// 手動(dòng)拋出異常
throw new SQLException("數(shù)據(jù)庫(kù)異常");
} catch (Exception e) {
// 異常處理邏輯
}
}
}
讀者可以使用我源碼中 controller 的接口,通過(guò) postman 測(cè)試一下,就會(huì)發(fā)現(xiàn),仍然是可以插入一條用戶數(shù)據(jù),說(shuō)明事務(wù)并沒(méi)有因?yàn)閽伋霎惓6貪L。這個(gè)細(xì)節(jié)往往比上面那個(gè)坑更難以發(fā)現(xiàn),因?yàn)槲覀兊乃季S很容易導(dǎo)致 try...catch 代碼的產(chǎn)生,一旦出現(xiàn)這種問(wèn)題,往往排查起來(lái)比較費(fèi)勁,所以我們平時(shí)在寫代碼時(shí),一定要多思考,多注意這種細(xì)節(jié),盡量避免給自己埋坑。
那這種怎么解決呢?直接往上拋,給上一層來(lái)處理即可,千萬(wàn)不要在事務(wù)中把異常自己 ”吃“ 掉。
3.3 事務(wù)的范圍
事務(wù)范圍這個(gè)東西比上面兩個(gè)坑埋的更深!我之所以把這個(gè)也寫上,是因?yàn)檫@是我之前在實(shí)際項(xiàng)目中遇到的,該場(chǎng)景在這個(gè)課程中我就不模擬了,我寫一個(gè) demo 讓大家看一下,把這個(gè)坑記住即可,以后在寫代碼時(shí),遇到并發(fā)問(wèn)題,就會(huì)注意這個(gè)坑了,那么這節(jié)課也就有價(jià)值了。
我來(lái)寫個(gè) demo:
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public synchronized void isertUser4(User user) {
// 實(shí)際中的具體業(yè)務(wù)……
userMapper.insertUser(user);
}
}
可以看到,因?yàn)橐紤]并發(fā)問(wèn)題,我在業(yè)務(wù)層代碼的方法上加了個(gè) synchronized 關(guān)鍵字。我舉個(gè)實(shí)際的場(chǎng)景,比如一個(gè)數(shù)據(jù)庫(kù)中,針對(duì)某個(gè)用戶,只有一條記錄,下一個(gè)插入動(dòng)作過(guò)來(lái),會(huì)先判斷該數(shù)據(jù)庫(kù)中有沒(méi)有相同的用戶,如果有就不插入,就更新,沒(méi)有才插入,所以理論上,數(shù)據(jù)庫(kù)中永遠(yuǎn)就一條同一用戶信息,不會(huì)出現(xiàn)同一數(shù)據(jù)庫(kù)中插入了兩條相同用戶的信息。
但是在壓測(cè)時(shí),就會(huì)出現(xiàn)上面的問(wèn)題,數(shù)據(jù)庫(kù)中確實(shí)有兩條同一用戶的信息,分析其原因,在于事務(wù)的范圍和鎖的范圍問(wèn)題。
從上面方法中可以看到,方法上是加了事務(wù)的,那么也就是說(shuō),在執(zhí)行該方法開始時(shí),事務(wù)啟動(dòng),執(zhí)行完了后,事務(wù)關(guān)閉。但是 synchronized 沒(méi)有起作用,其實(shí)根本原因是因?yàn)槭聞?wù)的范圍比鎖的范圍大。也就是說(shuō),在加鎖的那部分代碼執(zhí)行完之后,鎖釋放掉了,但是事務(wù)還沒(méi)結(jié)束,此時(shí)另一個(gè)線程進(jìn)來(lái)了,事務(wù)沒(méi)結(jié)束的話,第二個(gè)線程進(jìn)來(lái)時(shí),數(shù)據(jù)庫(kù)的狀態(tài)和第一個(gè)線程剛進(jìn)來(lái)是一樣的。即由于mysql Innodb引擎的默認(rèn)隔離級(jí)別是可重復(fù)讀(在同一個(gè)事務(wù)里,SELECT的結(jié)果是事務(wù)開始時(shí)時(shí)間點(diǎn)的狀態(tài)),線程二事務(wù)開始的時(shí)候,線程一還沒(méi)提交完成,導(dǎo)致讀取的數(shù)據(jù)還沒(méi)更新。第二個(gè)線程也做了插入動(dòng)作,導(dǎo)致了臟數(shù)據(jù)。
這個(gè)問(wèn)題可以避免,第一,把事務(wù)去掉即可(不推薦);第二,在調(diào)用該 service 的地方加鎖,保證鎖的范圍比事務(wù)的范圍大即可。
4. 總結(jié)
本章主要總結(jié)了 Spring Boot 中如何使用事務(wù),只要使用 @Transactional 注解即可使用,非常簡(jiǎn)單方便。除此之外,重點(diǎn)總結(jié)了三個(gè)在實(shí)際項(xiàng)目中可能遇到的坑點(diǎn),這非常有意義,因?yàn)槭聞?wù)這東西不出問(wèn)題還好,出了問(wèn)題比較難以排查,所以總結(jié)的這三點(diǎn)注意事項(xiàng),希望能幫助到開發(fā)中的朋友。
該文已收錄到我寫的《10萬(wàn)字Springboot經(jīng)典學(xué)習(xí)筆記》中,點(diǎn)擊下面小卡片,進(jìn)入【武哥聊編程】,回復(fù):筆記,即可免費(fèi)獲取。
點(diǎn)贊是最大的支持

