1 场景
一个服务中有存在多个数据库事务,要求:
- 保证数据一致
- 不产生脏数据
- 不误删数据
即前面的事务正常运行,后面的事务出现异常,数据库保持调用该服务前的状态
2 方案
Springboot开启事务,在Service实现层添加@Transactional注解,但是该注解默认捕捉RuntimeException和Error异常,出现如Exception异常时,需要手动捕捉,即不手动捕捉,会出现@Transactional注解失效.
2.1 原生回滚
使用默认的@Transactional回滚策略,即@Service实现层多个事务不使用try…catch,若出现事务异常,服务层会自动回滚.
2.1.0 Code
两个保存事务,第一个保存事务正常执行,两个事务之间添加一个异常,此时回滚生效,数据库不会新增任何数据.
package com.company.web.service.impl;
import com.company.web.service.IDataSaveService;
import com.company.web.mapper.QuestionsMapper;
import com.company.web.mapper.AnswersMapper;
import com.company.web.dto.*;
import java.util.Map;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import java.util.HashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
/**
* Data save implements.
* @author xindaqi
* @since 2020-10-20
*/
@Service
@Transactional
public class DataSaveServiceImpl implements IDataSaveService{
@Autowired
private QuestionsMapper questionsMapper;
@Autowired
private AnswersMapper answersMapper;
@Override
public Boolean saveQuestionAndAnswerWithTransactionRollback(QuestionAnswerInputDTO params) {
Map<String, String> questionsMap = new HashMap<>();
questionsMap.put("questions", params.getQuestions());
Integer addQuestionFlag = questionsMapper.addQuestions(questionsMap);
Integer errorFlag = 1/0;
Map<String, String> answersMap = new HashMap<>();
answersMap.put("answers", params.getAnswers());
answersMap.put("questions", params.getQuestions());
Integer addAnswerFlag = answersMapper.addAnswers(answersMap);
return true;
}
}
2.1.2 结果
第一个事务正常存储,两个事务之间出现异常时,事务回滚,通过日志Transaction synchronization deregistering可知,此时执行了回滚,注销了SqlSession.
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@336086cf]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@73698f0d] will be managed by Spring
==> Preparing: insert into questions_repository (questions) values (?)
==> Parameters: Q2020-10-20-2(String)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@336086cf]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@336086cf]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@336086cf]
java.lang.ArithmeticException: / by zero
at com.company.web.service.impl.DataSaveServiceImpl.saveQuestionAndAnswer(DataSaveServiceImpl.java:37)
at com.company.web.service.impl.DataSaveServiceImpl$$FastClassBySpringCGLIB$$b95c5951.invoke(<generated>)
2.2 try吃掉回滚
由于Springboot2的@Transactional默认只能捕捉RuntimeException异常,当出现如Exception的异常时,该异常则不能捕捉,数据库出现数据不一致.
2.2.1 Code
在@Service实现层,多个事务使用try…catch,产生的Exception异常,会被代码捕捉,事务@Transactional无法捕捉,当多个事务之间出现异常时,数据库会执行运行正常的事务,不会回滚,此时会产生脏数据或数据不一致(误删).
package com.company.web.service.impl;
import com.company.web.service.IDataSaveService;
import com.company.web.mapper.QuestionsMapper;
import com.company.web.mapper.AnswersMapper;
import com.company.web.dto.*;
import java.util.Map;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import java.util.HashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
/**
* Data save implements.
* @author xindaqi
* @since 2020-10-20
*/
@Service
@Transactional
public class DataSaveServiceImpl implements IDataSaveService{
@Autowired
private QuestionsMapper questionsMapper;
@Autowired
private AnswersMapper answersMapper;
@Override
public Boolean saveQuestionAndAnswerWithTryCatchWithoutRollback(QuestionAnswerInputDTO params) {
try {
Map<String, String> questionsMap = new HashMap<>();
questionsMap.put("questions", params.getQuestions());
Integer addQuestionFlag = questionsMapper.addQuestions(questionsMap);
Integer errorFlag = 1/0;
Map<String, String> answersMap = new HashMap<>();
answersMap.put("answers", params.getAnswers());
answersMap.put("questions", params.getQuestions());
Integer addAnswerFlag = answersMapper.addAnswers(answersMap);
return true;
}catch(Exception e) {
e.printStackTrace();
return false;
}
}
}
2.2.2 结果
Sprinboot原生的事务@Transactional无法捕捉Exception异常,成功的事务会正常操作数据库,数据库的数据部分发生改变.
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@621d38e5]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@45d439b9] will be managed by Spring
==> Preparing: insert into questions_repository (questions) values (?)
==> Parameters: Q2020-10-20-2(String)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@621d38e5]
java.lang.ArithmeticException: / by zero
at com.company.web.service.impl.DataSaveServiceImpl.saveQuestionAndAnswerWithTryCatchWithoutRollback(DataSaveServiceImpl.java:54)
at com.company.web.service.impl.DataSaveServiceImpl$$FastClassBySpringCGLIB$$b95c5951.invoke(<generated>)
2.3 rollbackfor捕捉回滚
当多个事务出现@Transactional无法捕获的异常,如Exception时,需要手动捕获异常,在注解中添加rollbackfor捕捉指定的异常类.
@Transactional(rollbackFor = Exception.class)
当然这还不够,需要在catch中添加回滚处理方法,才能最终实现回滚.
try{
//TODO: mutiple transaction.
}catch(Exception e){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
2.3.1 Code
在方法上添加事务注解并指定回滚的异常类型,在catch中添加回滚方法setRollbackOnly(),当出现Exception异常时,捕获当前的异常并回滚.
package com.company.web.service.impl;
import com.company.web.service.IDataSaveService;
import com.company.web.mapper.QuestionsMapper;
import com.company.web.mapper.AnswersMapper;
import com.company.web.dto.*;
import java.util.Map;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import java.util.HashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
/**
* Data save implements.
* @author xindaqi
* @since 2020-10-20
*/
@Service
@Transactional
public class DataSaveServiceImpl implements IDataSaveService{
@Autowired
private QuestionsMapper questionsMapper;
@Autowired
private AnswersMapper answersMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean saveQuestionAndAnswerWithTryCatchWithRollback(QuestionAnswerInputDTO params) {
try {
Map<String, String> questionsMap = new HashMap<>();
questionsMap.put("questions", params.getQuestions());
Integer addQuestionFlag = questionsMapper.addQuestions(questionsMap);
Integer errorFlag = 1/0;
Map<String, String> answersMap = new HashMap<>();
answersMap.put("answers", params.getAnswers());
answersMap.put("questions", params.getQuestions());
Integer addAnswerFlag = answersMapper.addAnswers(answersMap);
return true;
}catch(Exception e) {
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return false;
}
}
}
2.3.2 结果
事务之间出现异常时,先抛出异常,在异常之后,会执行回滚,即Transaction synchronization deregistering SqlSession注销SqlSession.
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@252bc44f]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@41f5088d] will be managed by Spring
==> Preparing: insert into questions_repository (questions) values (?)
==> Parameters: Q2020-10-20-7(String)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@252bc44f]
java.lang.ArithmeticException: / by zero
at com.company.web.service.impl.DataSaveServiceImpl.saveQuestionAndAnswerWithTryCatchWithRollback(DataSaveServiceImpl.java:79)
at com.company.web.service.impl.DataSaveServiceImpl$$FastClassBySpringCGLIB$$b95c5951.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
........
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@252bc44f]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@252bc44f]
3 小结
- Springboot2事务回滚方式:
序号 | 方式 | 备注 |
---|---|---|
1 | 自动 | @Service实现层添加@Transactional注解 |
2 | 手动 | 方法上添加@Transactional注解使用过rollbackFor参数捕获指定异常类 在catch中使用setRollbackOnly()方法回滚 |
- 事务回滚标志:
Transaction synchronization deregistering SqlSession
[参考文献]
[1]https://blog.csdn.net/weixin_44874115/article/details/104392779
[2]https://www.cnblogs.com/myitnews/p/12364455.html
[3]https://www.cnblogs.com/jpfss/p/11152338.html