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 种配置:REQUIREDSUPPORTSMANDATORYREQUIRES_NEWNOT_SUPPORTEDNEVERNESTED

默认是 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编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章

Day636.思考题解答② -Java业务开发常见错误

Day628.Spring声明式事务问题 -Java业务开发常见错误

Day616.SpringException常见错误 -Spring常见编程错误

Day748.Redis常见问题② -Redis 核心技术与实战

Day432.BASE理论&分布式事务常见解决方案 -谷粒商城

Day432.BASE理论&分布式事务常见解决方案 -谷粒商城