Spring事务失效的9大场景与解决方法

 更新时间:2025年04月30日 09:29:48   作者:写bug写bug  
在日常开发中,我们经常使用Spring事务,这篇文章主要来和大家聊聊Spring事务失效的 9 种场景,并提供了相应的解决方法,有需要的小伙伴可以参考一下

前言

在日常开发中,我们经常使用Spring事务。最近,一个朋友去面试,被问到了这样一个面试题:在什么情况下,Spring 事务会失效?

今天,我将和大家聊聊Spring事务失效的 9 种场景。

1. 抛出检查异常(checked exceptions)

例如,你的事务控制代码如下:

@Transactional
public void transactionTest() throws IOException {
    User user = new User();
    UserService.insert(user);
    throw new IOException();
}

如果没有特别指定@Transactional,Spring 默认只会在遇到运行时异常RuntimeException或错误时回滚,而检查异常如IOException不会触发回滚。

public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}

解决方案:

知道原因后,解决方案也很简单。配置rollbackFor属性,例如:@Transactional(rollbackFor = Exception.class)

@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
    User user = new User();
    UserService.insert(user);
    throw new IOException();
}

2. 业务方法本身捕获并处理了异常

@Transactional(rollbackFor = Exception.class)
public void transactionTest() {
    try {
        User user = new User();
        UserService.insert(user);
        int i = 1 / 0;
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在这个场景中,事务失效的原因也很简单。Spring 是否回滚事务取决于你是否抛出了异常。如果你自己捕获了异常,Spring 就无法处理事务了。

看了上面的代码,你可能会觉得这么简单的问题,自己不可能犯这种低级错误。但我想告诉你,我身边几乎有一半的人都曾因此困扰过。

在编写业务代码时,代码可能会更复杂,有很多嵌套的方法。稍不注意,就很容易触发这个问题。举个简单的例子,假设你有一个审计功能,每次方法执行完后,将审计结果保存到数据库中。那么代码可能会写成这样:

@Service
public class TransactionService {
    @Transactional(rollbackFor = Exception.class)
    public void transactionTest() throws IOException {
        User user = new User();
        UserService.insert(user);
        throw new IOException();
    }
}

下面的切面会作用于TransactionService

@Component
publicclass AuditAspect {
    @Autowired
    private AuditService auditService;

    @Around(value = "execution (* com.dylan.service.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) {
        try {
            Audit audit = new Audit();
            Signature signature = pjp.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            String[] strings = methodSignature.getParameterNames();
            audit.setMethod(signature.getName());
            audit.setParameters(strings);
            Object proceed = pjp.proceed();
            audit.success(true);
            return proceed;
        } catch (Throwable e) {
            log.error("{}", e);
            audit.success(false);
        }
        auditService.save(audit);
        returnnull;
    }
}

在上面的例子中,如果程序执行异常,事务也会失效。原因是Spring的事务切面优先级最低。如果异常被切面捕获,Spring 自然无法正确处理事务,因为事务管理器无法捕获到异常。

解决方案:

只需移除try-catch。虽然我们知道在处理事务时,业务代码不能自己捕获异常,但只要代码变得复杂,我们很容易不小心犯错。

3. 同一个类中的方法调用

@Service
publicclass DefaultTransactionService implements Service {

    public void saveUser() throws Exception {
        // do something
        doInsert();
    }

    @Transactional(rollbackFor = Exception.class)
    public void doInsert() throws IOException {
        User user = new User();
        UserService.insert(user);
        thrownew IOException();
    }
}

这也是一个容易出错的场景。事务失效的原因也很简单。因为 Spring 的事务管理功能是通过动态代理实现的,而 Spring 默认使用 JDK 动态代理,JDK 动态代理通过接口实现,并通过反射调用目标类。简单理解,在saveUser()方法中,调用this.doInsert()时,this是真实对象,因此会直接执行doInsert的业务逻辑,而不是代理逻辑,从而导致事务失效。

解决方案:

方案 1:直接在saveUser方法上添加@Transactional注解。

方案 2:可以将这两个方法拆分到不同的类中。

方案 3:不使用注解实现事务,而是使用编程式事务来包裹需要开启事务的代码块。例如:transactionTemplate.execute()

public void doInsert() throws IOException {
    transactionTemplate.execute(() -> {
        User user = new User();
        UserService.insert(user);
        throw new IOException();
    });
}

4. 方法使用了final或static关键字

如果 Spring 使用 Cglib 代理实现(当你的代理类没有实现接口时),而你的业务方法恰好使用了finalstatic关键字,那么事务控制也会失效。因为 Cglib 使用字节码增强技术生成被代理类的子类,并重写被代理类的方法来实现代理。如果被代理的方法使用了finalstatic关键字,子类就无法重写被代理的方法。

如果 Spring 使用 JDK 动态代理实现,JDK 动态代理是基于接口实现的,那么被finalstatic修饰的方法也无法被代理。

总之,如果方法连代理都没有,那么事务回滚肯定无法实现。

解决方案:

尽量移除方法上的finalstatic关键字。

5. 方法不是public

如果方法不是public,Spring 事务也会失效,因为在 Spring 事务管理的源码AbstractFallbackTransactionAttributeSource中,computeTransactionAttribute()方法会判断目标方法是否是public。如果不是public,则返回null

// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
    return null;
}

解决方案:

将当前方法的访问级别改为public

6. 传播机制使用不当

Spring事务的传播机制指的是当多个事务方法相互调用时,事务应该如何传播的策略。Spring提供了七种事务传播机制:REQUIREDSUPPORTSMANDATORYREQUIRES_NEWNOT_SUPPORTEDNEVERNESTED。如果你不了解这些传播策略的原理,很容易导致事务失效。

@Service
publicclass TransactionsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private AddressMapper addressMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void doInsert(User user, Address address) throws Exception {
        // do something
        userMapper.insert(user);
        saveAddress(address);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveAddress(Address address) {
        // do something
        addressMapper.insert(address);
    }
}

在上面的例子中,如果用户插入失败,不会导致saveAddress()回滚,因为这里使用的传播机制是REQUIRES_NEWREQUIRES_NEW的原理是,如果当前方法没有事务,则创建一个新事务。如果当前方法已经有事务,则挂起当前事务并创建一个新事务。父事务会等到当前事务完成后才提交。如果父事务发生异常,不会影响子事务的提交。

解决方案:

将事务传播策略改为默认值REQUIREDREQUIRED的原理是,如果当前有事务,则加入该事务。如果没有事务,则创建一个新事务。父事务和被调用的事务处于同一个事务中。即使被调用的事务捕获了异常,整个事务仍然会回滚。

7. 没有被 Spring 管理

// @Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void updateOrder(Order order) {
        // update order
    }
}

如果此时@Service注解被注释掉,这个类就不会被 Spring 加载为 Bean,那么这个类就不会被 Spring 管理,事务自然也会失效。

解决方案:

确保每个使用事务注解的Service都被 Spring 管理。

8. 多线程调用

@Service
publicclass UserService {

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

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

@Service
publicclass RoleService {

    @Transactional
    public void doOtherThing() {
        try {
            int i = 1 / 0;
            System.out.println("save role table data");
        } catch (Exception e) {
            thrownew RuntimeException();
        }
    }
}

我们可以看到,在事务方法add中,调用了事务方法doOtherThing,但doOtherThing是在另一个线程中被调用的。

这会导致两个方法不在同一个线程中,获取的数据库连接也不同,因此是两个不同的事务。如果在doOtherThing方法中抛出异常,add方法是不可能回滚的。

我们所说的同一个事务,实际上指的是同一个数据库连接。只有在同一个数据库连接下,才能同时提交和回滚。如果在不同的线程中,获取的数据库连接肯定不同,因此它们是不同的事务。

解决方案:

这有点像分布式事务。尽量确保在同一个事务中处理。

9. 没有配置开启事务

如果在项目中没有配置 Spring 的事务管理器,即使使用了 Spring 的事务管理功能,Spring 的事务也不会生效。例如,如果你是一个 Spring Boot 项目,并且没有在 Spring Boot 项目中配置以下代码:

@EnableTransactionManagement

解决方案:

确保在项目中正确配置了事务管理器。

总结

本文简要阐述了 Spring 事务的实现原理,并列出了 9 种 Spring 事务失效的场景。相信很多朋友可能都遇到过这些问题。文章也详细解释了失效的原因,希望大家对 Spring 事务有新的理解。

到此这篇关于Spring事务失效的9大场景与解决方法的文章就介绍到这了,更多相关Spring事务失效内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • spring aop实现用户权限管理的示例

    spring aop实现用户权限管理的示例

    本篇文章主要介绍了spring aop实现用户权限管理的示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-12-12
  • 小伙熬夜用Java重现经典超级马里奥代码实例

    小伙熬夜用Java重现经典超级马里奥代码实例

    这篇文章主要介绍了Java重现经典超级马里奥,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-04-04
  • java 中动态代理(JDK,cglib)实例代码

    java 中动态代理(JDK,cglib)实例代码

    这篇文章主要介绍了java 中动态代理,这里介绍了JDK 动态代理与 cglib 动态代理的相关资料
    2017-04-04
  • 二种jar包制作方法讲解(dos打包jar eclipse打包jar文件)

    二种jar包制作方法讲解(dos打包jar eclipse打包jar文件)

    这篇文章主要介绍了二种jar包制作方法讲解:dos打包jar和eclipse打包jar文件,大家参考使用吧
    2013-11-11
  • java面试常见问题之Hibernate总结

    java面试常见问题之Hibernate总结

    这篇文章主要介绍了在java面试过程中hibernate比较常见的问题,包括Hibernate的检索方式,Hibernate中对象的状态,Hibernate的3种检索策略是什么,Session的find()方法以及Query接口的区别等方面问题的总结,需要的朋友可以参考下
    2015-07-07
  • 在IDEA中创建SpringBoot项目的详细步骤

    在IDEA中创建SpringBoot项目的详细步骤

    这篇文章主要给大家介绍了在IDEA中创建SpringBoot项目的详细步骤,文中有详细的图文介绍和代码示例,对大家的学习和工作有一定的帮助,需要的朋友可以参考下
    2023-09-09
  • 理解Java当中的回调机制(翻译)

    理解Java当中的回调机制(翻译)

    今天我要和大家分享一些东西,举例来说这个在JavaScript中用的很多。我要讲讲回调(callbacks)。你知道什么时候用,怎么用这个吗?你真的理解了它在java环境中的用法了吗?当我也问我自己这些问题,这也是我开始研究这些的原因
    2014-10-10
  • SpringBoot+websocket实现消息对话功能

    SpringBoot+websocket实现消息对话功能

    WebSocket是一种在Web应用程序中实现实时双向通信的技术,它可以用于在线游戏、在线聊天、推送通知、实时监控等,并且比传统的轮询技术更加高效和可靠,本文就给大家介绍基于SpringBoot+websocket实现消息对话功能,感兴趣的小伙伴可以自己动手试一试
    2023-09-09
  • Java经典面试题汇总:网络编程

    Java经典面试题汇总:网络编程

    本篇总结的是Java 网络编程相关的面试题,后续会持续更新,希望我的分享可以帮助到正在备战面试的实习生或者已经工作的同行,如果发现错误还望大家多多包涵,不吝赐教,谢谢
    2021-07-07
  • MybatisPlus使用@TableId主键id自增长无效的解决

    MybatisPlus使用@TableId主键id自增长无效的解决

    本文主要介绍了MybatisPlus使用@TableId主键id自增长无效的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04

最新评论