一篇讓你學(xué)會(huì) 11個(gè)Spring 失效場(chǎng)景
其實(shí)關(guān)于spring事務(wù)失效的場(chǎng)景,網(wǎng)絡(luò)上文章介紹的不少,參差不齊。這里只分享下自己的見解,時(shí)長(zhǎng)大概10分鐘左右,先上個(gè)圖介紹下。
1.訪問(wèn)權(quán)限問(wèn)題
事務(wù)方法需要定義public,非public方法事務(wù)會(huì)失效。事務(wù)攔截器TransactionalInterceptor會(huì)在執(zhí)行方法前進(jìn)行攔截,通過(guò)動(dòng)態(tài)代理方式如果是cglib就是intercept方法或者jdk的invoke方法間接調(diào)用AbstractFallbackTransactionAttributeSource類的getTransactionAttribute方法獲取配置信息,附上源碼圖:
進(jìn)一步的跟蹤getTransactionAttribute方法,我們就能看到,spring對(duì)于非public修飾的方式,返回的事務(wù)對(duì)象是null,其中allowPublicMethodsOnly返回的是一個(gè)布爾false。
2.方法被final修飾
事務(wù)底層使用了aop,那么也就是說(shuō)通過(guò)jdk或者是cglib生成代理類,在代理類中實(shí)現(xiàn)的事務(wù)的功能,如果說(shuō)方法是final修飾的了,那么就會(huì)導(dǎo)致代理類中無(wú)法重寫該方法,從而導(dǎo)致添加事務(wù)失敗。同樣的如果是static的修飾的話也是無(wú)法通過(guò)動(dòng)態(tài)代理變成事務(wù)方法。
3.方法內(nèi)部調(diào)用
簡(jiǎn)單來(lái)說(shuō)就是一個(gè)方法內(nèi)部調(diào)用另一個(gè)方法,但是另一個(gè)方式是有事務(wù)的,這樣也會(huì)導(dǎo)致事務(wù)失效,因?yàn)檫@個(gè)調(diào)用的是this對(duì)象的方法,而不是另一個(gè)方法持有的對(duì)象,可以這里理解。
如果想要在方法內(nèi)部調(diào)用另一個(gè)方法也有事務(wù)的話,就需要新建一個(gè)service對(duì)象持有。
@Service
public class TmTrimServicebackImpl{
public void getById(Long id) {
TmTrimServiceliImpl.getTrimById(id);
}
}
@Service
public class TmTrimServiceliImpl{
@Transactional(rollbackFor=Exception.class)
public void getTrimById(Long id) {
TmTrimVO tmTrimVO = new TmTrimVO();
}
}
這樣,通過(guò)新建一個(gè)service方法,將事務(wù)添加到新建的service方法里就可以了。說(shuō)到這里可能小伙伴覺得這樣有點(diǎn)麻煩,那么是否有沒有其他的方式不新建一個(gè)方法呢,答案是可以的,就是注入自己,利用了spring ioc內(nèi)部的三級(jí)緩存的機(jī)制,這里注入自己就很好的保證了也不會(huì)出現(xiàn)循環(huán)依賴:
@Service
public class TmTrimServicebackImpl{
@Autowired
private TmTrimServicebackImpl tmTrimServicebackImpl;
public void getById(Long id) {
tmTrimServicebackImpl.getTrimById(id);
}
@Transactional(rollbackFor=Exception.class)
public void getTrimById(Long id) {
TmTrimVO tmTrimVO = new TmTrimVO();
}
}
其實(shí)到了這一步,還是發(fā)現(xiàn)有點(diǎn)不太雅觀,并不是說(shuō)上面代碼有什么問(wèn)題只是覺得,可以讓上面代碼更加好看一點(diǎn),那么有沒有呢,答案是有的,是什么呢?這就不得不佩服spring強(qiáng)大完善的支持,那就是AopContext.currentProxy(),這個(gè)就是創(chuàng)建代理類,在方法調(diào)·調(diào)用前后切入,這個(gè)代理類對(duì)象是保存在ThreadLocal中的,所以通過(guò)這個(gè)代理類對(duì)象調(diào)用事務(wù)方法就能生效了。
@Service
public class TmTrimServicebackImpl{
public void getById(Long id) {
((TmTrimServicebackImpl)AopContext.currentProxy()).getTrimById(id);
}
@Transactional(rollbackFor=Exception.class)
public void getTrimById(Long id) {
TmTrimVO tmTrimVO = new TmTrimVO();
}
}
這樣看來(lái),代碼是不是就優(yōu)雅多了,哈哈!!!
4.未被spring事務(wù)管理
這里需要明確一個(gè)前提,就是使用spring事務(wù)的前提,就是對(duì)象要被spring管理就需要?jiǎng)?chuàng)建bean實(shí)例,在開發(fā)中,我們都是通過(guò)@Controller,@Service,@Component,@Repository等注解自動(dòng)的實(shí)現(xiàn)依賴注入實(shí)例化的功能,但假如說(shuō)在相應(yīng)的控制層,業(yè)務(wù)層,數(shù)據(jù)層忘記加相應(yīng)的注解,那么也是會(huì)失效的。因?yàn)闆]有交給spring管理,例如:
5.多線程調(diào)用
回想起前幾年配置事務(wù)管理器時(shí),都會(huì)有這樣的一段配置:
通過(guò)這一段配置也可以知道,其實(shí)spring事務(wù)就是通過(guò)數(shù)據(jù)庫(kù)連接事務(wù)多線程連接會(huì)導(dǎo)致持有的connetion不是同一個(gè),從網(wǎng)上找了一張圖,通過(guò)這張圖進(jìn)一步理解:
接著附上偽代碼結(jié)合上面的圖進(jìn)一步理解:
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
System.out.println("保存role表數(shù)據(jù)");
}
}
事務(wù)add方法中調(diào)用了另一個(gè)事務(wù)doOtherThing,但是事務(wù)方法是在另一個(gè)線程中調(diào)用的,這樣就會(huì)導(dǎo)致兩個(gè)方法不在同一個(gè)線程中,獲取到的數(shù)據(jù)庫(kù)鏈接不一樣,是兩個(gè)不同的事務(wù),一旦doOtherThing發(fā)生異常,add方法也是不可能發(fā)生回滾的.這里需要解釋以下什么是同一個(gè)事務(wù),也就是說(shuō)只有擁有同一個(gè)數(shù)據(jù)庫(kù)連接才能同時(shí)提交和回滾。如果在不同的線程,拿到的數(shù)據(jù)庫(kù)連接肯定是不一樣的,所以是不同的事務(wù)。
6.多線程調(diào)用
這個(gè)就沒什么好講的了,也就是innodb和myisam引擎的不同,5版本以前默認(rèn)是myisam引擎,這個(gè)引擎是不支持事務(wù)的,5版本以后的innodb是支持事務(wù)的。
7.未開啟事務(wù)
這個(gè)其實(shí)可能也是比較容易忽略的,因?yàn)槲覀冇∠罄锖孟駴]怎么配置過(guò)怎么開啟事務(wù),也確實(shí)是這樣哈,為什么?其實(shí)原因很簡(jiǎn)單,springboot項(xiàng)目通過(guò)DataSourceTransactionManagerAutoConfiguration這個(gè)類已經(jīng)默默的為我們開啟了事務(wù)。
這個(gè)類會(huì)加載spring.datasource這個(gè)配置文件從而啟動(dòng)事務(wù),如果是非springboot項(xiàng)目就需要自己手動(dòng)在xml文件中配置事務(wù)管理器。
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
類似這樣的從而開啟事務(wù)。
8.錯(cuò)誤的傳播特性
在使用@Transactional注解時(shí),是可以指定propagation參數(shù)的,該參數(shù)是用來(lái)指定事務(wù)的傳播特性,其中只有required,requires_new,nested這三種才會(huì)創(chuàng)建新事務(wù):
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
像上面的Propagation.NEVER這種類型的傳播特性不支持事務(wù),如果有事務(wù)則會(huì)拋異常。
9.自己吞了異常
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
像這種手動(dòng)try...catch了異常,又沒有手動(dòng)拋出,那么sring就會(huì)認(rèn)為程序是異常的就不會(huì)回滾了。
10.手動(dòng)拋了別的異常
Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) throws Exception {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
捕獲了異常又拋出了exception異常,事務(wù)同樣不會(huì)回滾,因?yàn)閟pring事務(wù)默認(rèn)情況下只會(huì)回滾RuntimeException(運(yùn)行時(shí)異常)和Error(錯(cuò)誤),對(duì)于普通的Exception(非運(yùn)行時(shí)異常),不會(huì)回滾,網(wǎng)上找了一張圖:
這里exception里除了分為運(yùn)行時(shí)異常和非運(yùn)行時(shí)異常(ioException)。
1) 讓checked例外也回滾:
在整個(gè)方法前加上 @Transactional(rollbackFor=Exception.class)
2) 讓unchecked例外不回滾:
@Transactional(notRollbackFor=RunTimeException.class)
3)不需要事務(wù)管理的(只查詢的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)
這里需要提及的一句是,如果是自定義了的異常,比如說(shuō)我自定義了DALException異常,那么就應(yīng)該是@Transactional(notRollbackFor=DALException.class),一旦拋出的異常不屬于DALException異常,那么事務(wù)也是不會(huì)生效的。
11.嵌套事務(wù)回滾多了
其實(shí)這個(gè)就有點(diǎn)像是js里的冒泡事件,可能我只是需要底部,結(jié)果外層窗口事件也觸發(fā)了,聯(lián)想到事務(wù)這里,那么也是一樣的,嵌套多個(gè)可能只是想回滾對(duì)應(yīng)的事務(wù),就不用把其他事務(wù)也回滾了,這個(gè)可以通過(guò)try...catch來(lái)處理,將需要處理回滾的事務(wù)放這里面就不會(huì)把外層的也會(huì)滾了。
本文轉(zhuǎn)載自微信公眾號(hào)「小王子古木屋」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系小王子古木屋公眾號(hào)。