跳至主要內容

事务

chanchaw大约 4 分钟javaspring

概述

通过动态代理的方式实现数据库事务,是否为 Bean 类创建代理对象的判断标准:

  1. 类头上使用了注解 @Transactional
  2. 类中至少有一个方法使用了注解 @Transactional

代理对象在执行某个方法时会进入 TransactionInterceptor # invoke() 方法,该方法的执行逻辑是:

  1. 使用所配置的 PlatformTransactionManager 事务管理器新建一个数据库连接
  2. 修改数据库连接的 autocommit 为 false
  3. 执行 MethodInvocation.proceed() 方法 - 即执行业务方法
  4. 提交 - 如果有抛出异常则回滚

事务的完整执行流程

下图的文档在 180 阿里云盘的:文档 -> pro

spring事务01
spring事务01

案例

setGlobalRollbackOnParticipationFailure 和 catch

spring事务02
spring事务02

子事务的回滚案例

spring事务03
spring事务03

手动回滚

catch 异常后要手动强制回滚

spring事务04
spring事务04

事物失效的场景

访问权限

4中访问权限 private , default , protect , public 中只有最后一个 public 类型的方法支持事务

static , final

被这两个关键字修饰的方法也不支持

调用兄弟方法

例如下面的使用方法,会导致 methodA 的事务失效

@Service
public class TestService {
    @Transactional
	public void methodA(){
        methodB();
        methodC();
    }

    public void methodB(){}
    public void methodC(){}
}

方法一:将其他兄弟方法重构在另外一个类中 方法二:注入自己,使用被注入的自己调用兄弟方法,使用该方法要在配置文件中开启设置

spring:
 main:
  allow-circular-references: true
@Service
public class TestService {
    @Autowired
    private TestService testService;
    
    @Transactional
	public void methodA(){
        testService.methodB();
        testService.methodC();
    }

    public void methodB(){}
    public void methodC(){}
}

方法三:使用代理类调用兄弟方法

@Service
    public class TestService {
        @Transactional
        public void methodA(){

        }

        /**
* 这里调用methodA() 的事务将会生效
*/
        public void methodB(){
            ((TestService)AopContext.currentProxy()).methodA();
        }
    }

未被 spring 管理

没有使用 @Service , @Component 等纳入容器的注解,导致类没有被 spring 管理自然无法使用 spring 的事务

多线程环境

例如下面方法 add 中使用多线程调用了 doOtherThing ,当该方法出现异常时不会触发回滚

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

表不支持事务

MYSQL的MYISAM引擎不支持,如果创建表时使用了该引擎则该表不支持事务

未开启事务

springboot 默认是开启的,但是传统的 spring mvc 需要手动在配置文件中开启

传播特性设置不对

REQUIRED - 默认配置,如果上下文已存在事务则加入,如果没有则新建一个事务 SUPPORTS - 如果上下文存在事务则加入,如果不存在则以非事务的方式运行 MANDATORY - 如果上下文已有事务则执行,没有事务则抛出异常 REQUIRES_NEW - 每次都会新建一个事务,并将上下文已经存在的事务挂起,执行完本事务后再恢复上下文事务 NOT_SUPPORTED - 如果上下文中存在事务则挂起,保证本方法运行在非事务中,然后恢复上下文事务 NEVER - 上下文中存在事务则抛出异常,保证本方法运行在非事务中 NESTED - 上下文存在事务则嵌套事务执行,否则新建事务执行

错误的异常

自己处理掉异常不抛出则不会回滚,同时不正确的异常类型也不会回滚,例如下面的普通异常 Exception不会回滚。spring 只会针对运行时异常和错误回滚。

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

如果自定义的异常没有继承 RuntimeException,也会导致不回滚,但是可以通过指定异常类型来触发回滚

@Slf4j
@Service
public class UserService {
    
    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}

例如上面使用 Transactional 的同时指定了自定义的异常类,当抛出该类型的异常时候也会回滚。注意这里又又坑了,如果方法内部抛出了SqlException 等其他异常又会导致不触发回滚。

回滚保存点问题

设计意图是 doOtherThing 方法回滚不影响外层的 add 方法,代码如下

public class UserService {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

但实际情况是如果 doOtherThing 方法出现异常导致回滚了同时也会导致 add 方法回滚,因为 doOtherThing 的异常冒泡特性导致方法 add 也收到了异常,进而导致 userMapper.insertUser 方法也回滚了。要实现上面提到的设计意图,即使 doOtherThing 回滚不会导致 userMapper.insertUser 方法的回滚,可以这样写

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);
        try {
            roleService.doOtherThing();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

将 doOtherThing 抛出的异常捕获,不继续向上抛即可。