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