Spring MVC框架对数据访问事务抽象封装在spring-tx模块中,一个简单@Transactional注解就可以对一个方法调用开启事务,在使用过程中,若对Spring MVC的事务配置和原理了解不够清楚,一不小心就会落入坑中。
注:本文的代码技术讨论基于Spring Framework Tx 5.2.6.RELEASE版本 + Spring Boot 2.2.7.RELEASE版本。
1. 坑1 - 事务方法调用过程中的异常抛出
对一个方法调用开启事务,是希望在运行过程中,一旦有未知异常发生,则需要回滚数据库表的相关写操作,保证数据操作的原子性和一致性。
下面的insertUser()方法中,首先对一个user记录进行了保存写入操作,然后有一个异常被抛出,这个时候user记录是否会被回滚?
@Service
public class UserService {
@Autowired
UserInfoDAO userInfoDAO;
@Transactional
public void insertUser(UserInfoDTO user) throws Exception {
userInfoDAO.insert(user);
throw new Exception("Oh, an error happened");
return user;
}
}
答案其实是不会,user记录还是被保存到了数据库表中。其原因是@Transactional在未配置任何rollbackFor异常类时,将会执行如下缺省配置,即只对RuntimeException和Error两种异常发生时才执行回滚操作,上面的代码中抛出的异常类是Exception,未命中回滚条件。
public class DefaultTransactionAttribute extends DefaultTransactionDefinition implements TransactionAttribute {
public boolean rollbackOn(Throwable ex) {
return ex instanceof RuntimeException || ex instanceof Error;
}
}
若希望在上面的情况下,回滚user记录的写入操作,一个解决办法是,在标注Transactional时指定如下的回滚异常类,
@Transactional(rollbackFor = Exception.class)
更多的Transactional注解配置说明见下文。
2. 坑2 - 内部调用方法被标注为事务
在下面的代码样例中,UserService提供了两个方法,
- public void insertUser(UserInfoDTO user)
- private void insertUserInner(UserInfoDTO user)
前一个方法将会调用后者,后者执行数据记录保存操作,@Transactional注解标注在后者方法上。
@RestController
public class TestController {
@Autowired
private UserService userService;
@RequestMapping(value = "/test", method = RequestMethod.POST)
public UserInfoDTO addUser(@RequestBody UserInfoDTO user) throws Exception {
return userService.insertUser(user);
}
}
@Service
public class UserService {
@Autowired
UserInfoDAO userInfoDAO;
public void insertUser(UserInfoDTO user) throws Exception {
insertUserInner(user);
}
@Transactional
private void insertUserInner(UserInfoDTO user) throws Exception {
userInfoDAO.insert(user);
throw new RuntimeException("Oh, an error happened");
}
}
若在insertUserInner方法中出现异常,上面的user记录同样也不会被回滚,原因是,
1. Transactional的实现是通过Spring AOP原理,由于Spring AOP对内部方法调用不起作用,数据事务也无法正常启动。
2. @Transactional注解只对public方法起作用,对于private/protected/package-visible的方法上是无效的。
要想对内部调用或私有方法启用数据事务,可以考虑使用可编程的数据事务。
3. 坑3 - 在一个事务方法调用了另外一个事务方法
在下面的代码样例中,事务方法UserAService.insertUser()调用了另外一个UserBService.insertUser()事务方法,并通过try/catch对后者的异常进行了日志输出处理。
@Service
public class UserAService {
@Autowired
UserBService userBService;
@Autowired
UserInfoDAO userInfoDAO;
@Transactional()
public void insertUser(UserInfoDTO user) throws Exception {
// 保存用户记录A
userInfoDAO.insert(user);
try {
userBService.insertUser(user);
} catch (Throwable e){
System.out.println(e.toString());
}
}
}
@Service
public class UserBService {
@Autowired
UserInfoDAO userInfoDAO;
@Transactional()
public void insertUser(UserInfoDTO user) throws Exception {
// 保存用户记录B
userInfoDAO.insert(user);
throw new RuntimeException("Oh, an error happened");
}
}
在UserBService.insertUser()方法执行过程中,若有未知异常发生,用户记录A和用户记录B在数据库表中的状态是怎样?是保存,还是回滚了?
有很多人认为,由于异常被try/catch了,所以用户记录A将会被正常保存,而用户记录B会被回滚,而事实上是,两个用户记录都未被保存到。
查看日志可以看到如下的报错信息,
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)at
org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:707) at
org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:385) at
org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) at
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at
com.pphh.demo.service.UserAService$$EnhancerBySpringCGLIB$$73a6d5aa.insertUser(<generated>) ~[classes/:na]
这个异常日志说当前事务已回滚,无法执行当前事务。查看整个事务执行过程,就可以了解这个报错的原因,
- UserAService.insertUser被调用,开启了事务A,保存用户记录A
- UserBService.insertUser被调用,由于当前已经有存在事务,根据Transactional方法默认事务传播定义,是需要加入到当前事务A
- UserBService.insertUser异常发生,回滚当前事务A,用户记录B被回滚
- UserAService.insertUser执行完毕,准备提交用户记录A,这时发现当前事务A已经被回滚,无法执行当前事务。
要想解决这个问题,让用户记录A将会被正常保存,而用户记录B被回滚,可以将UserBService.insertUser的事务传播定义为Propagation.NEW,
public class UserBService {
@Transactional(propagation = Propagation.NEW)
public void insertUser(UserInfoDTO user) throws Exception {
// 保存用户记录B
userInfoDAO.insert(user);
throw new RuntimeException("Oh, an error happened");
}
}
4. 开启事务调试日志
有时候为了方便查看事务运行情况,可以通过下方法开启事务日志,
# transaction
logging.level.org.springframework.transaction.interceptor=TRACE
如下是一段Trace执行日志,可以看到由于没有回滚规则定义,直接使用了缺省规则。
2020-05-26 12:16:14.329 o.s.t.i.TransactionInterceptor : Getting transaction for [com.pphh.demo.service.UserService.insertUser]
2020-05-26 12:16:14.490 o.s.t.i.TransactionInterceptor : Completing transaction for [com.pphh.demo.service.UserService.insertUser] after exception: java.lang.Exception: Oh, an error happened
2020-05-26 12:16:14.490 o.s.t.i.RuleBasedTransactionAttribute : Applying rules to determine whether transaction should rollback on java.lang.Exception: Oh, an error happened
2020-05-26 12:16:14.490 o.s.t.i.RuleBasedTransactionAttribute : Winning rollback rule is: null
2020-05-26 12:16:14.490 o.s.t.i.RuleBasedTransactionAttribute : No relevant rollback rule found: applying default rules
5. Transational注解配置说明
下面对Transational注解的各个配置定义进行了说明,供参考,
public @interface Transactional {
// 指定事务管理器
@AliasFor("transactionManager")
String value() default "";
// 同上
@AliasFor("value")
String transactionManager() default "";
// 事务嵌套时,事务的传播定义,有如下几种,
// Propagation.REQUIRED -- 当前若有事务存在,则加入该事务。若无事务,则创建一个事务。
// Propagation.SUPPORTS -- 当前若有事务存在,则加入该事务。若无事务,则继续非事务状态运行。
// Propagation.MANDATORY -- 当前必须在事务中,若当前无事务,则抛出异常。
// Propagation.REQUIRES_NEW -- 当前方法新起一个事务。当前若有事务存在,则挂起当前事务。
// Propagation.NOT_SUPPORTED -- 当前方法不支持事务,若有事务存在,则挂起当前事务。
// Propagation.NEVER -- 当前方法不支持事务,若有事务存在,则抛出异常。
Propagation propagation() default Propagation.REQUIRED;
// 事务隔离级别,有如下几个定义,该选项只适用于REQUIRED和REQUIRES_NEW事务传播级别
// DEFAULT -- 数据库缺省
// READ_UNCOMMITTED -- 读未提交
// READ_COMMITTED -- 读已提交
// REPEATABLE_READ -- 可重复读
// SERIALIZABLE -- 串行
Isolation isolation() default Isolation.DEFAULT;
// 事务超时时间,缺省是数据库指定,该选项只适用于REQUIRED和REQUIRES_NEW事务传播级别
int timeout() default -1;
// 只读事务,该选项只适用于REQUIRED和REQUIRES_NEW事务传播级别
boolean readOnly() default false;
// 事务回滚异常类
// 多个回滚条件可以通过如下指定
// @Transactional(rollbackFor = {Exception.class, Error.class})
Class<? extends Throwable>[] rollbackFor() default {};
// 事务回滚异常类,必须继承自Throwable
String[] rollbackForClassName() default {};
// 事务回滚异常类过滤条件
Class<? extends Throwable>[] noRollbackFor() default {};
// 事务回滚异常类过滤条件,必须继承自Throwable
String[] noRollbackForClassName() default {};
}