Day619.Spring事务常见错误② -Spring编程常见错误
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day619.Spring事务常见错误② -Spring编程常见错误相关的知识,希望对你有一定的参考价值。
Spring事务常见错误②
继续讨论事务中的另外两个问题,一个是关于事务的传播机制,另一个是关于多数据源的切换问题。
一、环境前缀
课程表 course,记录课程名称和注册的学生数。
CREATE TABLE `course` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`course_name` varchar(64) DEFAULT NULL,
`number` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
学生选课表 student_course,记录学生表 student 和课程表 course 之间的多对多关联。
CREATE TABLE `student_course` (
`student_id` int(11) NOT NULL,
`course_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
同时我为课程表初始化了一条课程信息,id = 1,course_name = “英语”,number = 0。
新增学生选课记录
@Mapper
public interface StudentCourseMapper
@Insert("INSERT INTO `student_course`(`student_id`, `course_id`) VALUES (#studentId, #courseId)")
void saveStudentCourse(@Param("studentId") Integer studentId, @Param("courseId") Integer courseId);
课程登记学生数 + 1
@Mapper
public interface CourseMapper
@Update("update `course` set number = number + 1 where id = #id")
void addCourseNumber(int courseId);
新的业务类 CourseService,用于实现相关业务逻辑。分别调用了上述两个方法来保存学生与课程的关联关系,并给课程注册人数 +1。
最后,别忘了给这个方法加上事务注解。
@Service
public class CourseService
@Autowired
private CourseMapper courseMapper;
@Autowired
private StudentCourseMapper studentCourseMapper;
//注意这个方法标记了“Transactional”
@Transactional(rollbackFor = Exception.class)
public void regCourse(int studentId) throws Exception
studentCourseMapper.saveStudentCourse(studentId, 1);
courseMapper.addCourseNumber(1);
我们在之前的 StudentService.saveStudent() 中调用了 regCourse(),实现了完整的业务逻辑。
为了避免注册课程的业务异常导致学生信息无法保存,在这里 catch 了注册课程方法中抛出的异常。我们希望的结果是,当注册课程发生错误时,只回滚注册课程部分,保证学生信息依然正常。
@Service
public class StudentService
//省略非关键代码
@Transactional(rollbackFor = Exception.class)
public void saveStudent(String realname) throws Exception
Student student = new Student();
student.setRealname(realname);
studentService.doSaveStudent(student);
try
courseService.regCourse(student.getId());
catch (Exception e)
e.printStackTrace();
//省略非关键代码
为了验证异常是否符合预期,我们在 regCourse() 里抛出了一个注册失败的异常:
@Transactional(rollbackFor = Exception.class)
public void regCourse(int studentId) throws Exception
studentCourseMapper.saveStudentCourse(studentId, 1);
courseMapper.addCourseNumber(1);
throw new Exception("注册失败");
运行一下这段代码,在控制台里我们看到了以下提示信息:
java.lang.Exception: 注册失败
at com.spring.puzzle.others.transaction.example3.CourseService.regCourse(CourseService.java:22)
//…省略非关键代码…
Exception in thread “main” org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:873)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:710)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:533)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:304)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy D y n a m i c A d v i s e d I n t e r c e p t o r . i n t e r c e p t ( C g l i b A o p P r o x y . j a v a : 688 ) a t c o m . s p r i n g . p u z z l e . o t h e r s . t r a n s a c t i o n . e x a m p l e 3. S t u d e n t S e r v i c e DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) at com.spring.puzzle.others.transaction.example3.StudentService DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)atcom.spring.puzzle.others.transaction.example3.StudentService E n h a n c e r B y S p r i n g C G L I B EnhancerBySpringCGLIB EnhancerBySpringCGLIB$50cda404.saveStudent()
at com.spring.puzzle.others.transaction.example3.AppConfig.main(AppConfig.java:22)
其中,注册失败部分的异常符合预期,但是后面又多了一个这样的错误提示:
Transaction rolled back because it has been marked as rollback-only。
最后的结果是,学生和选课的信息都被回滚了,显然这并不符合我们的预期。我们期待的结果是即便内部事务 regCourse() 发生异常,外部事务 saveStudent() 俘获该异常后,内部事务应自行回滚,不影响外部事务。
那么这是什么原因造成的呢?
二、嵌套事务回滚错误
如上的问题,先通过伪代码把整个事务的结构梳理一下:
// 外层事务
@Transactional(rollbackFor = Exception.class)
public void saveStudent(String realname) throws Exception
//......省略逻辑代码.....
studentService.doSaveStudent(student);
try
// 嵌套的内层事务
@Transactional(rollbackFor = Exception.class)
public void regCourse(int studentId) throws Exception
//......省略逻辑代码.....
catch (Exception e)
e.printStackTrace();
可以看出来,整个业务是包含了 2 层事务,外层的 saveStudent() 的事务和内层的 regCourse() 事务。
在 Spring 声明式的事务处理中,有一个属性 propagation,表示打算对这些方法怎么使用事务,即一个带事务的方法调用了另一个带事务的方法,被调用的方法它怎么处理自己事务和调用方法事务之间的关系。
其中 propagation 有 7 种配置:REQUIRED
、SUPPORTS
、MANDATORY
、REQUIRES_NEW
、NOT_SUPPORTED
、NEVER
、NESTED
。
默认是 REQUIRED,它的含义是:如果本来有事务,则加入该事务,如果没有事务,则创建新的事务。
结合我们的伪代码示例,因为在 saveStudent() 上声明了一个外部的事务,就已经存在一个事务了,在 propagation 值为默认的 REQUIRED 的情况下, regCourse() 就会加入到已有的事务中,两个方法共用一个事务。我们再来看下 Spring 事务处理的核心,其关键实现参考 TransactionAspectSupport.invokeWithinTransaction():
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager))
// 是否需要创建一个事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try
// 调用具体的业务方法
retVal = invocation.proceedWithInvocation();
catch (Throwable ex)
// 当发生异常时进行处理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
finally
cleanupTransactionInfo(txInfo);
// 正常返回时提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
//......省略非关键代码.....
- 检查是否需要创建事务;
- 调用具体的业务方法进行处理;
- 提交事务;
- 处理异常。
这里要格外注意的是,当前案例是两个事务嵌套的场景,外层事务 doSaveStudent() 和内层事务 regCourse(),每个事务都会调用到这个方法。
所以,这个方法会被调用两次。下面我们来具体来看下内层事务对异常的处理。
当捕获了异常,会调用 TransactionAspectSupport.completeTransactionAfterThrowing() 进行异常处理:
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex)
if (txInfo != null && txInfo.getTransactionStatus() != null)
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex))
try
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
catch (TransactionSystemException ex2)
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
catch (RuntimeException | Error ex2)
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
//......省略非关键代码.....
在这个方法里,我们对异常类型做了一些检查,当符合声明中的定义后,执行了具体的 rollback 操作,这个操作是通过 TransactionManager.rollback() 完成的:
public final void rollback(TransactionStatus status) throws TransactionException
if (status.isCompleted())
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
processRollback(defStatus, false);
而 rollback() 是在 AbstractPlatformTransactionManager 中实现的,继续调用了 processRollback():
private void processRollback(DefaultTransactionStatus status, boolean unexpected)
try
boolean unexpectedRollback = unexpected;
if (status.hasSavepoint())
// 有保存点
status.rollbackToHeldSavepoint();
else if (status.isNewTransaction())
// 是否为一个新的事务
doRollback(status);
else
// 处于一个更大的事务中
if (status.hasTransaction())
// 分支1
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure())
doSetRollbackOnly(status);
if (!isFailEarlyOnGlobalRollbackOnly())
unexpectedRollback = false;
// 省略非关键代码
if (unexpectedRollback)
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
finally
cleanupAfterCompletion(status);
这个方法里区分了三种不同类型的情况:
- 是否有保存点;
- 是否为一个新的事务;
- 是否处于一个更大的事务中。
在这里,因为我们用的是默认的传播类型 REQUIRED,嵌套的事务并没有开启一个新的事务,所以在这种情况下,当前事务是处于一个更大的事务中,所以会走到情况 3 分支 1 的代码块下。
这里有两个判断条件来确定是否设置为仅回滚:if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure())
满足任何一个,都会执行 doSetRollbackOnly() 操作。
isLocalRollbackOnly 在当前的情况下是 false,所以是否分设置为仅回滚就由 isGlobalRollbackOnParticipationFailure() 这个方法来决定了,其默认值为 true, 即是否回滚交由外层事务统一决定 。
显然这里的条件得到了满足,从而执行 doSetRollbackOnly:
protected void doSetRollbackOnly(DefaultTransactionStatus status)
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
txObject.setRollbackOnly();
以及最终调用到的 DataSourceTransactionObject 中的 setRollbackOnly():
public void setRollbackOnly()
getConnectionHolder().setRollbackOnly();
到这一步,内层事务的操作基本执行完毕,它处理了异常,并最终调用到了 DataSourceTransactionObject 中的 setRollbackOnly()。
接下来,我们来看外层事务。因为在外层事务中,我们自己的代码捕获了内层抛出来的异常,所以这个异常不会继续往上抛,最后的事务会在 TransactionAspectSupport.invokeWithinTransaction() 中的 commitTransactionAfterReturning() 中进行处理:
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo)
if (txInfo != null && txInfo.getTransactionStatus() != null)
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
在这个方法里我们执行了 commit 操作,代码如下:
public final void commit(TransactionStatus status) throws TransactionException
//......省略非关键代码.....
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly())
processRollback(defStatus, true);
return;
processCommit(defStatus);
在 AbstractPlatformTransactionManager.commit() 中,当满足了 shouldCommitOnGlobalRollbackOnly() 和 defStatus.isGlobalRollbackOnly(),就会回滚,否则会继续提交事务。
其中 shouldCommitOnGlobalRollbackOnly() 的作用为,如果发现了事务被标记了全局回滚,并且在发生了全局回滚的情况下,判断是否应该提交事务,这个方法的默认实现是返回了 false,这里我们不需要关注它,继续查看 isGlobalRollbackOnly() 的实现:
public boolean isGlobalRollbackOnly()
return ((this.transaction instanceof SmartTransactionObject) &&
((SmartTransactionObject) this.transaction).isRollbackOnly());
这个方法最终进入了 DataSourceTransactionObject 类中的 isRollbackOnly():
public boolean isRollbackOnly()
return getConnectionHolder().isRollbackOnly();
其最终调用到的是 DataSourceTransactionObject 中的 setRollbackOnly():
public void setRollbackOnly()
getConnectionHolder().setRollbackOnly();
isRollbackOnly() 和 setRollbackOnly() 这两个方法的执行本质都是对 ConnectionHolder 中 rollbackOnly 属性标志位
的存取,而 ConnectionHolder 则存在于 DefaultTransactionStatus 类实例的 transaction 属性之中。
至此,答案基本浮出水面了,我们把整个逻辑串在一起就是:外层事务是否回滚的关键,最终取决于 DataSourceTransactionObject 类中的 isRollbackOnly(),而该方法的返回值,正是我们在内层异常的时候设置的。
所以最终外层事务也被回滚了,从而在控制台中打印出异常信息:“Transaction rolled back because it has been marked as rollback-only”。
内外层共同维护了一个变量来控制是否回滚事务,rollbackOnly 属性标志位
解决方案:
修改事务传递方式为REQUIRES_NEW
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void regCourse(int studentId) throws Exception
studentCourseMapper.saveStudentCourse(studentId, 1);
courseMapper.addCourseNumber(1);
throw new Exception("注册失败");
- 当子事务声明为 Propagation.REQUIRES_NEW 时,在 TransactionAspectSupport.invokeWithinTransaction() 中调用 createTransactionIfNecessary() 就会创建一个新的事务,独立于外层事务。
- 而在 AbstractPlatformTransactionManager.processRollback() 进行 rollback 处理时,因为 status.isNewTransaction() 会因为它处于一个新的事务中而返回 true,所以它走入到了另一个分支,执行了 doRollback() 操作,让这个子事务单独回滚,不会影响到主事务。
二、多数据源间切换之谜
假设新需求又来了,每个学生注册的时候,需要给他们发一
以上是关于Day619.Spring事务常见错误② -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章
Day628.Spring声明式事务问题 -Java业务开发常见错误
Day616.SpringException常见错误 -Spring常见编程错误
Day748.Redis常见问题② -Redis 核心技术与实战