Spring事务失效的9种常见场景

149 阅读4分钟

Spring事务管理是Java应用中确保数据库操作一致性和完整性的关键机制之一。然而,在实际开发中,有时候会遇到Spring事务失效的情况,导致期望的事务行为无法正常发生。本文将深入探讨九种常见的导致Spring事务失效的场景,帮助开发者更好地理解事务管理的细节和注意事项。

image.png

场景 1

众所周知,java 的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。 但如果我们在开发过程中,把某些事务方法,定义了错误的访问权限,就会导致事务功能出问题,例如:

@Service
public class UserService {
    
    @Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

场景 2

有时候,某个方法不想被子类重写,这时可以将该方法定义成 final 的。普通方法这样定义是没问题的,但如果将事务方法定义成 final,例如:

@Service
public class UserService {
 
    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

如果你看过 spring 事务的源码,可能会知道 spring 事务底层使用了 aop,也就是通过 jdk 动态代理或者 cglib,帮我们生成了代理类,在代理类中实现的事务功能。

但如果某个方法用 final 修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

注意:如果某个方法是 static 的,同样无法通过动态代理,变成事务方法。

场景 3

有时候我们需要在某个 Service 类的某个方法中,调用另外一个事务方法,比如:

@Service
public class UserService {
 
    @Autowired
    private UserMapper userMapper;
 
  
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }
 
    @Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
}

我们看到在事务方法 add 中,直接调用事务方法 updateStatus。从前面介绍的内容可以知道,updateStatus 方法拥有事务的能力是因为 spring aop 生成代理了对象,但是这种方法直接调用了 this 对象的方法,所以 updateStatus 方法不会生成事务。

由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?

使用 AopContext.currentProxy() 或者基于 Spring Event 进行事件解耦当然也要注意事务问题

场景 4

在我们平时开发过程中,有个细节很容易被忽略,即使用 spring 事务的前提是:对象要被 spring 管理,需要创建 bean 实例。 通常情况下,我们通过 @Controller、@Service、@Component、@Repository 等注解,可以自动实现 bean 实例化和依赖注入的功能。

如果有一天,你匆匆忙忙地开发了一个 Service 类,但忘了加 @Service 注解,比如:


//@Service
public class UserService {
 
    @Transactional
    public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }    
}

场景 5

多线程调用

@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表数据");
    }
}

场景 6

表不支持事务 众所周知,在 mysql5 之前,默认的数据库引擎是myisam。 它的好处就不用多说了:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比 innodb 更好。 有些老项目中,可能还在用它。 在创建表的时候,只需要把ENGINE参数设置成MyISAM即可

场景 7

异常被忽略 有时候,开发者可能选择忽略掉一个异常,而不重新抛出或处理。这样的做法将导致事务失效,因为Spring事务管理依赖于异常来判断是否需要回滚事务。

@Transactional
public void handleException() {
    try {
        // 事务操作
        throw new RuntimeException("Simulate Exception");
    } catch (Exception e) {
        // 异常被忽略,事务失效
    }
}

在上述示例中,异常被捕获但未重新抛出或处理,导致事务失效。

场景 8

使用错误的事务传播行为

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void performTransaction() {
    // 使用错误的传播行为,可能导致事务失效
}

场景 9

对非受检查异常进行捕获 Spring事务默认只对RuntimeException及其子类进行回滚。如果一个受事务管理的方法抛出了非受检查异常,而异常被捕获了,并且没有重新抛出,事务将不会回滚。

@Transactional
public void performTransaction() {
    try {
        // 事务操作
        throw new Exception("Unchecked Exception");
    } catch (Exception e) {
        // 非受检查异常被捕获,事务失效
    }
}

总结

Spring事务管理是保证应用数据一致性和完整性的重要组成部分,但在实际开发中需要注意避免事务失效的情况。本文详细讨论了九种常见的导致Spring事务失效的场景,包括不是通过Spring容器管理的Bean调用事务方法、异常未被正确捕获或处理、方法内部调用导致的事务失效等。开发者应当牢记这些场景,并在开发过程中注意避免出现事务失效的情况,以确保数据的一致性和完整性。