(万字总结)spring事务失效的12种场景, 实在太坑了

简介: 为了保障操作的原子性, 避免数据不一样的情况,我们一般都会用到spring事务.但spring事务失效的12种场景, 实在太坑了

♨️本篇文章记录的为spring事务失效的12种场景相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬🙉🙉🙉。
♨️如果文章有什么需要改进的地方还请大佬不吝赐教❤️🧡💛
👨‍🔧个人主页 : 阿千弟

前言:
对于从事java工作的同学来说spring的事务肯定再熟悉不过了, 在某些业务业务场景下, 如果一个请求中需要同时写入多张表的数据, 为了保障操作的原子性(即要么同时成功, 要么同时失败), 避免数据不一样的情况,我们一般都会用到spring事务, 确实spring事务用起来贼爽, 就一个简单的注解@Transactional, 但你使用不合理, 它也能坑你于无形, 今天我们就来聊一聊spring事务失效的12种场景, 实在太坑了!!!

@[TOC]

1. 访问权限问题

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

@Service
public class UserService {
   
   

    @Transactional
    private void add(User user) {
   
   
        saveDate(user);
        updateData(user);
    }
}

就像这段代码, 我们可以看到add方法的访问权限被定义成立private, 这样会导致事务失效, spring要求被代理事务必须是public的, 说白了AbstractFallbackTransactionAttributeSource类中的computeTransactionAttribute方法中有个判断 : 如果目标方法不是public, 则返回TransactionAttribute返回null
在这里插入图片描述

总结 : 如果我们自定义的事务方法访问权限不是public, 而是private, default或protected的话,spring则不会提供事务功能.

2. 方法用final修饰

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

@Service
public class UserService {
   
   

    @Transactional
    public final void add (User user) {
   
   
        saveData(user);
        updateData(user);
    }
}

在这段代码中, add方法被定义成final的,这样会导致事务失效, 为什么会这样?

原因 : spring事务底层使用了AOP, 也就是JDK动态代理或者CG lib帮我们生成了代理类, 在代理类中实战实现的事务功能, 但如果某个方法用final修饰了,那么在它的代理类中就无法重写该方法, 而添加事务功能.

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

3. 方法内部调用

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

@Service
public class UserService {
   
   

    @Autowired
    private UserMapper userMapper;

    public void add (User user) {
   
   
        userMapper.insertUser(user);
        updateData(user);
    }

    @Transactional
    public void updateData(User user) {
   
   
        doSomething();
    }
}

这段代码中, 我们可以看到在事务方法add中, 直接调用事务方法updateData(), 从前面介绍的内容可以知道updateData()方法拥有事务的能力是因为spring AOP生成了代理对象, 但是这种方法直接调用了this对象的方法, 所以updateData()方法不会生成事务. 由此可见, 在同一个类中的方法直接内部调用会导致事务失效.

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

方案一 : 新加一个Service方法
这个方法非常简单, 只需新加一个Service方法, 把@Transactional注解加到新Service方法上, 把需要事务执行的代码移到新方法中, 具体代码如下

@Service
public class ServiceA {
   
   
    @Autowired
    private ServiceB serviceB;

    public void save (User user) {
   
   
        queryData1();
        queryData2();
        serviceB.doSave(user);
    }
}

@Service
public class ServiceB {
   
   

    @Transactional(rollbackFor = Exception.class)
    public void doSave (User user) {
   
   
        addData1();
        updateData2();
    }
}

方案二 : 在该Service类中注入自己
可能有人会有疑问, 这样做法会不会出现循环依赖问题. 答案是不会, 其实Spring IOC内部的三级缓存保证了它不会出现循环依赖问题

@Service
public class ServiceA {
   
   
    @Autowired
    private ServiceA serviceA;

    public void save (User user) {
   
   
        queryData1();
        queryData2();
        serviceA.doSave(user);
    }
}

方案三 : 通过AOP Context
在该Service类中使用AopContext.currentProxy()获取代理对象

@Service
public class ServiceA {
   
   

    public void save (User user) {
   
   
        queryData1();
        queryData2();
        ((serviceA)AopContext.currentProxy()).doSave(user);
    }

    @Transactional(rollbackFor = Exception.class)
    public void doSave (User user) {
   
   
        addData1();
        updateData2();
    }
}

4. 未被spring管理

在我们平时的开发中, 有个细节很容易被忽略, 即使用spring事务的前提是对象要被spring管理创建Bean实例. 通常我们通过@Controller, @Service, @Component, @Repository等注解可以自动实现Bean实例化和依赖注入功能. 如果有一天你匆忙的写了一段代码, 可是忘了加@Service注解, 比如这段代码, UserService类没有加@Service注解, 那么该类不会交给spring管理, 所以它的add()方法也不会生成事务.

//@Service
public class UserService {
   
   

    @Transactional
    private void add(User user) {
   
   
        saveDate(user);
        updateData(user);
    }
}

5. 多线程调用

在实际的开发中, 多线程的使用场景还是挺多的, 如果spring事务用在多线程场景中会有问题吗?

看下面这段代码

@Slf4
@Service
public class UserService {
   
   

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

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

@Service
public class RoleService {
   
   

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

我们可以看到事务方法add()中, 调用了事务方法doOtherThing(), 但是事务方法doOtherThing()是在另外一个线程中调用的, 这样就会导致两个方法不在同一个线程中, 获取到的数据库连接不一样, 从而是两个不同的事务, 如果想doOtherThing()方法中抛了异常, add()方法也回滚是不可能的.

在这里插入图片描述
看过spring源码的朋友, 可能会知道spring的事务是通过数据库连接来实现的, 当前线程中保存了一个map, key是数据源, value是数据库连接, 我们说的同一个事务其实是指同一个数据库连接, 只有拥有同一个数据库连接才能同时提交和回滚, 如果在不同的线程, 拿到的数据库连接肯定是不一样的, 使用是不同的事务

6. 表不支持事务

在MySQL5之前, 默认的数据库引擎是MyISAM, 它的好处就不用多说了, 索引文件和数据文件是分开存储的, 对于查多写少的单表操作性能比innodb好, 有些老项目中, 可能还在用它, 在创建表的时候只需要把ENGINE参数设置成MyISAM即可;,
在这里插入图片描述
MyISAM好用, 但有个很致命的缺点, 那就是不支持事务, 如果是单表操作还好, 不会出现太大的问题, 但如果需要跨多张表操作, 数据极有可能会出现不完整的情况; 此外, MyISAM还不支持行级锁和外键, 使用在实际业务场景中, MyISAM使用的并不多, MyISAM终究是个消失在时间线中的产物, 取而代之的是innodb.
所以在实际的开发中, 发现事务失效那不一定是spring的锅, 最好确认一下你使用的那张表是否支持事务.

7. 未开启事务

有时候事务没生效的根本原因是没有开启事务, 看到这句话可能会觉得很好笑, 如果项目搭建好了事务功能肯定是有的, 但如果是在搭建项目demo的时候只有一张表, 而这张表的事务没有生效, 那么会是什么原因造成的呢, 原因有很多, 但是没有开启事务这个原因极其容易被忽略.

    <!--声明式事务配置-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <tx:advice id="advice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    <!-- 用切点把事务切进去 -->
    <aop:config>
        <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
        <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
    </aop:config>

如果你使用的是springboot项目, 那么你很幸运, 因为 springboot通过DataSourceTransactionManagerAutoConfiguration类以及默默帮你开启了事务, 你需要做的事很简单, 只需要配置spring.datasource相关参数即可; 但如果你使用还是传统的spring项目, 则需要在applicationContext.xml文件中手动配置事务相关参数, 如果忘了配置, 事务肯定不会生效, 如果在pointcut标签中的切入点匹配规则配错了的话, 有些类的事务也不会生效

8. 错误的传播特性

我们在使用@Transactional注解时是可以指定propagation参数的, 该参数的作用是指定事务的传播特性

spring目前支持7种传播特性:
| 传播行为 | 含义 |
|--|--|
REQUIRED | 如果当前上下文种存在事务, 那么加入该事务; 如果不存在事务, 则会创建一个事务; 它是默认的传播属性值
SUPPORTS |如果当前上下文种存在事务, 则支持事务加入事务; 如果不存在事务, 则使用非事务的方式执行
MANDATORY |如果当前有事务存在, 就以事务方式执行; 如果没有事务, 就抛出异常
REQUIRESNEW |每次就会新建一个事务, 并且同时将上下文中的事务挂起, 执行当前新建事务完成以后, 上下文事务恢复再执行
NOTSUPPORTED |如果当前上下文中存在事务, 则挂起当前事务, 然后新的方法在没有事务的环境中执行
NEVER| 如果当前上下文中存在事务, 则抛出异常, 否则在无事务环境上执行代码
NESTED |如果当前上下文中存在事务, 则嵌套事务执行, 如果不存在事务, 则新建事务

如果我们在手动设置propagation参数的时候把传播特性设置错了, 就像下面这段代码add()方法的事务传播特性定义成了Propagation.NEVER, 这种类型的传播特性不支持事务, 如果有事务则会抛异常

@Service
public class UserService {
   
   

    @Transactional(propagation = Propagation.NEVER)
    private void add(User user) {
   
   
        saveDate(user);
        updateData(user);
    }
}

目前只有这三种传播特性才会创建新事务 :REQUIRED,REQUIRESNEW, NESTED

9. 自己吞了异常

也有这样一个常见的原因, 开发者在代码中手动try...catch了异常, 事务不会生效
因为开发者自己捕获了异常, 又没有手动抛出, 换句话说就是把异常吞掉了, 如果想要spring事务能够正常回滚, 必须抛出它能够处理的异常, 如果没有抛异常, 则spring认为程序是正确的

@Slf4j
@Service
public class UserService {
   
   

    @Transactional(propagation = Propagation.NEVER)
    private void add(User user) {
   
   
        try{
   
   
            saveDate(user);
            updateData(user);
        } catch (Exception e) {
   
   
            log.error(e.getMessage(), e);
        }
    }
}

10. 手动抛了别的异常

开发者没有手动捕获异常, 但如果异常抛的不正确, spring事务也不会回滚

@Slf4j
@Service
public class UserService {
   
   

    @Transactional(propagation = Propagation.NEVER)
    private void add(User user) throws Exception{
   
   
        try{
   
   
            saveDate(user);
            updateData(user);
        } catch (Exception e) {
   
   
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

上面这段代码中, 开发者自己捕获了异常, 又手动抛了异常Exception, 事务同样不会回滚.
因为spring事务默认情况下只会回滚RuntimeException(运行时异常) 和 Error(错误), 对应普通的Exception(非运行时异常)它不会回滚

11. 自定义了指定异常类(rollbackFor属性)回滚

在使用@Transactional注解声明事务时, 有时我们想自定义回滚的异常spring也是支持的, 可以通过设置rollbackFor参数来完成, 但如果这个参数的值设置错了, 就会引发一些莫名其妙的问题

@Service
public class UserService {
   
   

    @Transactional(rollbackFor = BusinessException.class)
    private void add(User user) throw Exception{
   
   
        saveDate(user);
        updateData(user);
    }
}

如果在执行上面这段代码保存和更新数据时, 程序报错了, 抛了SqlException, DuplicateKeyException等异常, 而BusinessException是我们自定义的异常, 报错的异常不属于BusinessException, 所以事务也不会回滚, 即使rollbackFor有默认值但在阿里巴巴开发者规范中还是要求开发者重新指定该参数, 因为使用默认值一旦程序抛出了Exception, 事务不会回滚就会引发很大的bug, 所以在一般情况下建议将参数设置为Exception或Throwable

12. 嵌套事务回滚多了

@Service
public class UserService {
   
   

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

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

    @Service
    public class RoleService {
   
   

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

这种情况使用了嵌套的内部事务, 原本是希望调用roleService.doOtherThing()方法时如果出现了异常, 只回滚doOtherThing方法里的内容, 不回滚userMapper.insertUser里的内容, 即回滚保存点, 但事实上insertUser也回滚了, 为什么?

因为doOtherThing方法如果出现了异常没有手动捕获会继续向上抛, 到外层add方法的代理方法中捕获了异常, 所以这种情况是直接回滚了整个事务, 不只回滚单个保存点.

解决方法: 可以将内部嵌套事务放在try/catch中, 而且不继续往上抛异常, 这样就能保证如果内部嵌套事务中出现异常只回滚内部事务, 而不影响外部事务

@Service
public class UserService {
   
   

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(User user) throws Exception {
   
   
        userMapper.insertUser(user);
        try{
   
   
            roleService.doOtherThing();        
        } catch (Exception e) {
   
   
            log.error(e.getMessage(), e);
        }
    }
}

@Service
public class RoleService {
   
   

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

在这里插入图片描述在这里插入图片描述

如果这篇【文章】有帮助到你💖,希望可以给我点个赞👍,创作不易,如果有对Java后端或者对spring感兴趣的朋友,请多多关注💖💖💖
👨‍🔧个人主页 : 阿千弟

目录
相关文章
|
3天前
|
Java 开发者 Spring
理解和解决Spring框架中的事务自调用问题
事务自调用问题是由于 Spring AOP 代理机制引起的,当方法在同一个类内部自调用时,事务注解将失效。通过使用代理对象调用、将事务逻辑分离到不同类中或使用 AspectJ 模式,可以有效解决这一问题。理解和解决这一问题,对于保证 Spring 应用中的事务管理正确性至关重要。掌握这些技巧,可以提高开发效率和代码的健壮性。
24 13
|
29天前
|
缓存 安全 Java
Spring高手之路26——全方位掌握事务监听器
本文深入探讨了Spring事务监听器的设计与实现,包括通过TransactionSynchronization接口和@TransactionalEventListener注解实现事务监听器的方法,并通过实例详细展示了如何在事务生命周期的不同阶段执行自定义逻辑,提供了实际应用场景中的最佳实践。
45 2
Spring高手之路26——全方位掌握事务监听器
|
1月前
|
Java 关系型数据库 数据库
京东面试:聊聊Spring事务?Spring事务的10种失效场景?加入型传播和嵌套型传播有什么区别?
45岁老架构师尼恩分享了Spring事务的核心知识点,包括事务的两种管理方式(编程式和声明式)、@Transactional注解的五大属性(transactionManager、propagation、isolation、timeout、readOnly、rollbackFor)、事务的七种传播行为、事务隔离级别及其与数据库隔离级别的关系,以及Spring事务的10种失效场景。尼恩还强调了面试中如何给出高质量答案,推荐阅读《尼恩Java面试宝典PDF》以提升面试表现。更多技术资料可在公众号【技术自由圈】获取。
|
2月前
|
Java 开发者 Spring
Spring高手之路24——事务类型及传播行为实战指南
本篇文章深入探讨了Spring中的事务管理,特别是事务传播行为(如REQUIRES_NEW和NESTED)的应用与区别。通过详实的示例和优化的时序图,全面解析如何在实际项目中使用这些高级事务控制技巧,以提升开发者的Spring事务管理能力。
58 1
Spring高手之路24——事务类型及传播行为实战指南
|
2月前
|
JavaScript Java 关系型数据库
Spring事务失效的8种场景
本文总结了使用 @Transactional 注解时事务可能失效的几种情况,包括数据库引擎不支持事务、类未被 Spring 管理、方法非 public、自身调用、未配置事务管理器、设置为不支持事务、异常未抛出及异常类型不匹配等。针对这些情况,文章提供了相应的解决建议,帮助开发者排查和解决事务不生效的问题。
|
2月前
|
XML Java 数据库连接
Spring中的事务是如何实现的
Spring中的事务管理机制通过一系列强大的功能和灵活的配置选项,为开发者提供了高效且可靠的事务处理手段。无论是通过注解还是AOP配置,Spring都能轻松实现复杂的事务管理需求。掌握这些工具和最佳实践,能
66 3
|
3月前
|
Java API Spring
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中拦截器的入门教程和实战项目场景实现的详细指南。
38 0
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
|
3月前
|
Java API Spring
springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中过滤器的基础知识和实战项目应用的教程。
42 0
springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
|
3月前
|
Java 关系型数据库 MySQL
Spring事务失效,我总结了这7个主要原因
本文详细探讨了Spring事务在日常开发中常见的七个失效原因,包括数据库不支持事务、类不受Spring管理、事务方法非public、异常被捕获、`rollbackFor`属性配置错误、方法内部调用事务方法及事务传播属性使用不当。通过具体示例和源码分析,帮助开发者更好地理解和应用Spring事务机制,避免线上事故。适合所有使用Spring进行业务开发的工程师参考。
46 2
|
3月前
|
Java 程序员 Spring
Spring事务的1道面试题
每次聊起Spring事务,好像很熟悉,又好像很陌生。本篇通过一道面试题和一些实践,来拆解几个Spring事务的常见坑点。
Spring事务的1道面试题