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]
[2]
[3]