今天有個(gè)同事遇到一個(gè)問(wèn)題,由于業(yè)務(wù)需求要求,在一個(gè)Service的一個(gè)方法A中有一個(gè)for循環(huán),每次循環(huán)里面的業(yè)務(wù)邏輯有可能發(fā)生異常,這個(gè)時(shí)候就需要將這個(gè)循環(huán)內(nèi)的所有數(shù)據(jù)庫(kù)操作給回滾掉,但是又不能影響到之前循環(huán)里數(shù)據(jù)的更改,并且后面的循環(huán)里不發(fā)生異常的情況下也需要正常操作數(shù)據(jù)庫(kù)。同事嘗試了很久結(jié)果還是不能滿(mǎn)足業(yè)務(wù)需求。
大概業(yè)務(wù)邏輯需求如下:
for(int i = 1;i<5;i++){
/***
**業(yè)務(wù)邏輯: 例如:每循環(huán)一次向數(shù)據(jù)庫(kù)user表插入一條記錄
**/
}
如果第一次循環(huán)沒(méi)有發(fā)生異常,第二次循環(huán)發(fā)生異常,第三、四次循環(huán)都沒(méi)有發(fā)生異常,那么最后數(shù)據(jù)表里有 1 3 4 三條記錄,第二次插入數(shù)據(jù)沒(méi)有寫(xiě)入,所以不能單單的在Service上加上事務(wù)。
按照這個(gè)業(yè)務(wù)需求我的想法在Service上加上@Transactional事務(wù),將可能發(fā)生異常的業(yè)務(wù)代碼提到另外一個(gè)方法B中,在B方法上面加上一個(gè)@Transactional注解,并且將B方法上事務(wù)傳播行為改成PROPAGATION_REQUIRES_NEW(創(chuàng)建新事務(wù),無(wú)論當(dāng)前存不存在事務(wù),都創(chuàng)建新事務(wù)),并且在第二次循環(huán)手動(dòng)設(shè)置一個(gè)算術(shù)異常,Demo代碼如下:
package com.dubboconsumer.dubboConsumer.service.imp;
import com.dubboconsumer.dubboConsumer.Mapper.DemoMapper;
import com.dubboconsumer.dubboConsumer.model.User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
@Transactional
public class DemoService {
@Resource
private DemoMapper demoMapper;
public void A(){
for (int i = 1;i<5;i++){
try {
this.B(i);
}catch (Exception e){
e.printStackTrace();
}
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void B(int i){//可能發(fā)生異常的方法B
User user = new User();
if (i==2){
user.setId(2);
user.setName("No"+i);
user.setAge(i);
demoMapper.insertUser(user);//往user表插入一條用戶(hù)記錄
int a = 1/0;//拋出ArithmeticException異常
}else {
user.setId(i);
user.setName("Yes"+i);
user.setAge(i);
demoMapper.insertUser(user);
}
}
}
運(yùn)行代碼,在代碼執(zhí)行到第二個(gè)循環(huán)int a = 1/0 時(shí)拋出異常,異常被方法A中catch到,但是數(shù)據(jù)庫(kù)沒(méi)有回滾,而是插入了 1、 2、3、4 四條記錄,說(shuō)明事務(wù)并沒(méi)有生效。
然后網(wǎng)上找各種帖子,各種嘗試:
- 首先數(shù)據(jù)庫(kù)方面,因?yàn)槲覀兊拈_(kāi)發(fā)庫(kù)之前就有事務(wù)回滾各種操作,我本地?cái)?shù)據(jù)庫(kù)(寫(xiě)demo用的本地庫(kù))的存儲(chǔ)引擎也是InnoDB(mysql的引擎是InnoDB才支持事務(wù)回滾)。
- 異常類(lèi)型方面,在方法B中的業(yè)務(wù)邏輯加上了try.catch塊,然后再手動(dòng)拋出一個(gè)RuntimeException,@Transactional加上rollbackFor = Exception.class
`package com.dubboconsumer.dubboConsumer.service.imp;
import com.dubboconsumer.dubboConsumer.Mapper.DemoMapper;
import com.dubboconsumer.dubboConsumer.model.User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
@Transactional
public class DemoService {
@Resource
private DemoMapper demoMapper;
public void A(){
for (int i = 1;i<5;i++){
try {
this.B(i);
}catch (Exception e){
e.printStackTrace();
}
}
}
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
public void B(int i){
try {
User user = new User();
if (i==2){
user.setId(2);
user.setName("No"+i);
user.setAge(i);
demoMapper.insertUser(user);
int a = 1/0;//拋出ArithmeticException異常
}else {
user.setId(i);
user.setName("Yes"+i);
user.setAge(i);
demoMapper.insertUser(user);
}
}catch (Exception e){
e.printStackTrace();
//TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new RuntimeException("插入出錯(cuò)!");
}
}
}
這樣修改之后執(zhí)行到 int a = 1/0; 異常會(huì)被try.catch到,但是再手動(dòng)拋出異常時(shí),異常又被方法A中Catch到,數(shù)據(jù)庫(kù)里還是插入了1、2、3、4四條數(shù)據(jù),所以事務(wù)并沒(méi)有生效。
- 還有帖子說(shuō)手動(dòng)回滾,所以上面代碼catch中加上TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 但是結(jié)果并沒(méi)有回滾,反而A中catch到報(bào)出錯(cuò)誤:
org.springframework.transaction.NoTransactionException: No transaction aspect-managed TransactionStatus in scope
at org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus(TransactionAspectSupport.java:122)
at com.dubboconsumer.dubboConsumer.service.imp.DemoService.B(DemoService.java:44)
at com.dubboconsumer.dubboConsumer.service.imp.DemoService.A(DemoService.java:20)
at com.dubboconsumer.dubboConsumer.service.imp.DemoService$$FastClassBySpringCGLIB$$c7449c96.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:669)
...
大概意思是這個(gè)類(lèi)里面沒(méi)有事務(wù),不能進(jìn)行手動(dòng)回滾。
問(wèn)題還是沒(méi)有解決,然后我又去網(wǎng)上翻各種帖子,在一個(gè)類(lèi)似問(wèn)題帖子回復(fù)中看到說(shuō),事務(wù)沒(méi)有回滾可能是因?yàn)閷?xiě)在同一個(gè)類(lèi)里,然后我就試了新建一個(gè)DoInsertService ,將方法B中的業(yè)務(wù)邏輯放到這個(gè)新建的DoInsertService ,并且在Service上加上@Transactional,代碼如下:
package com.dubboconsumer.dubboConsumer.service.imp;
import com.dubboconsumer.dubboConsumer.Mapper.DemoMapper;
import com.dubboconsumer.dubboConsumer.model.User;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Repository
@Transactional(rollbackFor = Exception.class)
public class DoInsertService {
@Resource
private DemoMapper demoMapper;
public void B(int i){
User user = new User();
if (i==2){
user.setId(i);
user.setName("No"+i);
user.setAge(i);
demoMapper.insertUser(user);
int a = 1/0; //拋出ArithmeticException異常
}else {
user.setId(i);
user.setName("Yes"+i);
user.setAge(i);
demoMapper.insertUser(user);
}
}
}
然后在原來(lái)的Service中調(diào)用DoInsertService的B方法,
package com.dubboconsumer.dubboConsumer.service.imp;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class DemoService {
@Resource
private DoInsertService doInsertService;
public void A(){
for (int i = 1;i<5;i++){
try {
doInsertService.B(i);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
運(yùn)行代碼A中catch到算術(shù)異常,這時(shí)再看數(shù)據(jù)庫(kù)中數(shù)據(jù),只有1、3、4三條記錄,說(shuō)明插入2這個(gè)數(shù)據(jù)時(shí)發(fā)生異常數(shù)據(jù)被回滾,沒(méi)有插入數(shù)據(jù)庫(kù)。
網(wǎng)上看到說(shuō)原因是: Spring之所以可以對(duì)開(kāi)啟@Transactional的方法進(jìn)行事務(wù)管理,是因?yàn)镾pring為當(dāng)前類(lèi)生成了一個(gè)代理類(lèi),然后在執(zhí)行相關(guān)方法時(shí),會(huì)判斷這個(gè)方法有沒(méi)有@Transactional注解,如果有的話,則會(huì)開(kāi)啟一個(gè)事務(wù)。
但是,上面同一個(gè)類(lèi)中A調(diào)用方式時(shí),在調(diào)用B時(shí),使用的并不是代理對(duì)象,從而導(dǎo)致this.B(i)時(shí)也不是代理對(duì)象,從而導(dǎo)致@Transactional失敗。所以,Spring 從同一個(gè)類(lèi)中的某個(gè)方法調(diào)用另一個(gè)有注解(@Transactional)的方法時(shí),事務(wù)會(huì)失效,些事務(wù)的時(shí)候一定要注意到這一點(diǎn)。