Spring的@Transactional踩坑紀錄

前言

最近同事遇到一個問題,就是當兩個service都掛交易時,spring的AOP拋出一個異常訊息,如下:
org.springframework.transaction.UnexpectedRollbackException:
Transaction rolled back because it has been marked as rollback-only

😜😜😜 那為何會這樣呢? 來分析一下原因


1. 發生原因

Transaction rolled back because it has been marked as rollback-only

意思是交易已被標記為rollback (所以你不應該commit)

這情況就是:

當A和B都有交易,A呼叫B,B中發生例外狀況,
A在這邊捕捉B的例外狀況,但是A把這例外catch住,並沒有將例外狀況拋出,
導致A方法執行結束時,對A來說沒有問題,提交commit,
但因為交易已被標註rollback,出現衝突才會有了上述的錯誤。

分享實際測試的簡易程式

先用Spring Starter Project起一個專案,引入Spring Web

Aservice.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class Aservice {

@Autowired
Bservice Bservice;

@Transactional(rollbackFor = Exception.class)
public void insertData() {
System.out.println("insert in A");

try {
Bservice.insertData();
} catch (Exception e) {
System.out.println("ERRRRRRRRRRRRRRRRRRRRRRRR");
}
System.out.println("insert success in A");
}
}

Bservice.java

1
2
3
4
5
6
7
8
@Service
public class Bservice {
@Transactional
public void insertData() throws Exception {
System.out.println("insert in B and throw");
throw new RuntimeException();
}
}

testController.java

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class testController {

@Autowired
Aservice aservice;

@GetMapping("/")
public String test() {
aservice.insertData();
return "test";
}
}

當run起spring boot程式後 打http://localhost:9877/

得到的訊息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
insert in A
insert in B and throw
ERRRRRRRRRRRRRRRRRRRRRRRR
insert success in A
2023-01-18 14:25:09.805 ERROR 12436 --- [nio-9877-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only] with root cause

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:870) ~[spring-tx-5.3.24.jar:5.3.24]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:707) ~[spring-tx-5.3.24.jar:5.3.24]
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654) ~[spring-tx-5.3.24.jar:5.3.24]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407) ~[spring-tx-5.3.24.jar:5.3.24]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.24.jar:5.3.24]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.24.jar:5.3.24]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.24.jar:5.3.24]
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-5.3.24.jar:5.3.24]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.24.jar:5.3.24]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.24.jar:5.3.24]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.24.jar:5.3.24]
at com.example.service.Aservice$$EnhancerBySpringCGLIB$$bfc87790.insertData(<generated>) ~[classes/:na]
at com.example.controller.testController.test(testController.java:18) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_201]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_201]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_201]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_201]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.24.jar:5.3.24]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1071) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.24.jar:5.3.24]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.24.jar:5.3.24]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:670) ~[tomcat-embed-core-9.0.70.jar:4.0.FR]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.24.jar:5.3.24]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:779) ~[tomcat-embed-core-9.0.70.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.70.jar:9.0.70]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.24.jar:5.3.24]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.24.jar:5.3.24]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.24.jar:5.3.24]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.24.jar:5.3.24]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.24.jar:5.3.24]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.24.jar:5.3.24]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:891) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1784) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) [tomcat-embed-core-9.0.70.jar:9.0.70]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.70.jar:9.0.70]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_201]


所以 要怎麼解決這 UnexpectedRollbackException 問題呢?

解法 1: 修改Transactional的propagation屬性(可以做到A正常commit,但B做rollback)

Transactional的Default屬性
1.readOnly = false
2.propagation = Propagation.REQUIRED
3.isolation = Isolation.DEFAULT

Bservice.java

1
2
3
4
5
6
7
8
@Service
public class Bservice {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void insertData() throws Exception {
System.out.println("insert in B and throw");
throw new RuntimeException();
}
}

重點在看你想用哪一種傳播情境

  1. 除了不沿用default的REQUIRED
    propagation = Propagation.NOT_SUPPORTED

  2. 也可再開一條新的給B,A會正常commit,適合用在B沒有對DB操作
    propagation = Propagation.REQUIRES_NEW

  3. 或直接不用
    propagation = Propagation.NEVER

  4. 或A可以commit但B走rollback
    propagation = Propagation.NESTED

目前實測都是不會報錯~

關於傳播類型的個別差異可以參考此篇

https://blog.csdn.net/yanxin1213/article/details/100582643

表格比較

異常狀態 REQUIRES_NEW(兩個獨立交易) NESTED(B的交易嵌套在A的交易中) REQUIRED(同一個交易)
methodA拋異常 methodB正常 A會rollback,B正常commit A與B一起rollback A與B一起rollback
methodA正常method B拋異常 1.如果A中catchB的Exception,並沒有繼續向上拋,則B先rollback,A再正常commit; 2.如果A沒有catch B的Exception,預設則會將B的Exception向上拋,則B先rollback,A再rollback B先rollback,A再正常commit A與B一起rollback
methodA,B皆拋異常 B先rollback,A再rollback A與B一起rollback A與B一起rollback
methodA,B皆正常 B先commit,A再commit A與B一起commit A與B一起commit

另附上各種情境
https://www.tpisoftware.com/tpu/articleDetails/2741

此時console.log僅印出 不會噴Exception

1
2
3
4
insert in A
insert in B and throw
ERRRRRRRRRRRRRRRRRRRRRRRR
insert success in A

解法 2:自己手動處理這Exception(AB都rollback)

@Transactional是基於AOP實現的,

Spring AOP異常捕獲的原理是:
攔截的方法需要明確拋出異常,並且不能經過任何處理,這樣AOP代理才能捕獲方法的異常,
才能進行rollback,預設情況下AOP只捕獲RuntimeException異常。
或是你可以擴大範圍 @Transactional(rollbackFor = Exception.class)

B方法返回時,transcation已經被設置為rollback-only
但是A這邊catch住沒有繼續向外拋,那麼A方法結束時,就會由AOP來提交commit

所以這邊我們也要讓A從commit,變成rollback,就AB都會rollback了!

自己手動setRollbackOnly()

Aservice.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class Aservice {

@Autowired
Bservice Bservice;

@Transactional(rollbackFor = Exception.class)
public void insertData() {
System.out.println("insert in A");
try {
Bservice.insertData();
} catch (Exception e) {
//直接把全部改為rollback 就沒衝突問題
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
System.out.println("ERRRRRRRRRRRRRRRRRRRRRRRR");
}
System.out.println("insert success in A");
}
}

參考此篇:
https://blog.csdn.net/sunayn/article/details/107942963

解法 3:改用Component來取代service,因為我們在service的資料夾掛上交易的AOP,使得所有service都會有交易行為,只要拉出來放在別的資料夾,就沒有問題了。

這裡就不提供程式範例,但值得注意的是Component的使用時機
例如:共用元件又不須交易、特殊邏輯需要個別commit時。

假設有Aservice(有掛交易)跟一個Bcomponent(沒掛交易)
這樣A去呼叫B時,有可能會發生這種事,我畫個圖來表示

備註 要可以用@Transactional 需要設定連線資訊:

pom.xml引入依賴

1
2
3
4
5
6
7
8
9
10
11
<!--交易 要使用@Transactional -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- 交易需要設定連線 For DataBase connection -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

application.properties

1
2
3
4
5
6
server.port=9877
# *** 請自行置換
spring.datasource.url=jdbc:mysql://localhost:3306/***?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=***
spring.datasource.password=***
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

還有SpringbootTestAopApplication.java,要去scan 你的專案

1
2
3
4
5
6
7
8
@SpringBootApplication(scanBasePackages = { "com.example.service", "com.example.controller",
"com.example.aop" })

public class SpringbootTestAopApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTestAopApplication.class, args);
}
}

同場加映 1: 在Cservice 中去call 自己有交易的method

舉一個例子:
Cservice中有兩方法,insertData()有交易,讓沒有交易的queryData(),去呼叫有交易的方法
Aservice去呼叫Cservice中,沒有交易的方法queryData()

Cservice.java

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class Cservice {

@Transactional
public void insertData() throws Exception {
throw new RuntimeException();
}

public void queryData() throws Exception {
insertData();//call 自己內部有交易的方法,失效
System.out.println("insert C &query in C");
}
}

Aservice.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class Aservice {

@Autowired
Cservice Cservice;

@Transactional(rollbackFor = Exception.class)
public void insertData() {
System.out.println("insert in A");
try {
Cservice.queryData();
} catch (Exception e) {
System.out.println("ERRRRRRRRRRRRRRRRRRRRRRRR");
}
System.out.println("insert success in A");
}
}

testController.java

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class testController {

@Autowired
Aservice aservice;

@GetMapping("/")
public String test() {
aservice.insertData();
return "test";
}
}

這樣會印出 執行成功 並不會有衝突發生 代表@Transactional是失效的

1
2
3
insert in A
ERRRRRRRRRRRRRRRRRRRRRRRR
insert success in A

其實是由於使用Spring AOP代理造成的,
因為只有當交易方法被當前class以外的程式呼叫時,才會由Spring生成的代理物件來管理。

參考補充:@Transactional失效情境
https://juejin.cn/post/6844904096747503629

同場加映2: new一個class,呼叫其交易的方法

舉一個例子:
Aservice中去new 一個Cservice,呼叫其交易的方法insertData()

Cservice.java

1
2
3
4
5
6
7
8
@Service
public class Cservice {

@Transactional
public void insertData() throws Exception {
throw new RuntimeException();
}
}

Aservice.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class Aservice {

@Transactional(rollbackFor = Exception.class)
public void insertData() {
System.out.println("insert in A");
Cservice Cservice = new Cservice();
try {
Cservice.insertData();
} catch (Exception e) {
System.out.println("ERRRRRRRRRRRRRRRRRRRRRRRR");
}
System.out.println("insert success in A");
}
}

testController.java

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class testController {

@Autowired
Aservice aservice;

@GetMapping("/")
public String test() {
aservice.insertData();
return "test";
}
}

這樣會印出 執行成功 並不會有衝突發生 代表@Transactional是失效的

1
2
3
insert in A
ERRRRRRRRRRRRRRRRRRRRRRRR
insert success in A

說明如果去new了一個class,就不歸spring容器管理了,
再呼叫class中被spring注入的class的方法,就找不到了,代表這class沒有被spring管理起來。

參考:
https://blog.csdn.net/keepCode/article/details/122057148
https://blog.csdn.net/MOFEG/article/details/121380772


今天先分享到這邊~希望大家對AOP有更深的認識 XD

下集預告 (希望有空拉 XD):
1.@Transactional 到底何時做commit動作?
(可以先看原碼 TransactionAspectSupport.java #266行)

2.如果自己寫AOP 和 @Transaction 一起使用時,執行順序? 與@Before @Around @After…的用法


Spring的@Transactional踩坑紀錄
http://example.com/2023/01/18/Spring的@Transactional踩坑紀錄/
作者
Tayli Kuan
發布於
2023年1月18日
許可協議