(万字总结)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感兴趣的朋友,请多多关注💖💖💖
👨‍🔧个人主页 : 阿千弟

目录
相关文章
|
18天前
|
Java 数据库 开发者
|
2天前
|
SQL Java 关系型数据库
Spring 事务
Spring 事务
6 1
|
5天前
|
Java 数据库连接 数据库
Spring事务简介,事务角色,事务属性
Spring事务简介,事务角色,事务属性
15 2
|
9天前
|
Java 数据库连接 数据库
16:事务-Java Spring
16:事务-Java Spring
25 5
|
10天前
|
消息中间件 Java 关系型数据库
Spring事务与分布式事务
这篇文档介绍了事务的概念和数据库事务的ACID特性:原子性、一致性、隔离性和持久性。在并发环境下,事务可能出现更新丢失、脏读和不可重复读等问题,这些问题通过设置事务隔离级别(如读未提交、读已提交、可重复读和序列化)来解决。Spring事务传播行为有七种模式,影响嵌套事务的执行方式。`@Transactional`注解用于管理事务,其属性包括传播行为、隔离级别、超时和只读等。最后提到了分布式事务,分为跨库和跨服务两种情况,跨服务的分布式事务通常通过最终一致性策略,如消息队列实现。
|
12天前
|
监控 Java 测试技术
Spring Boot与事务钩子函数:概念与实战
【4月更文挑战第29天】在复杂的业务逻辑中,事务管理是确保数据一致性和完整性的关键。Spring Boot提供了强大的事务管理机制,其中事务钩子函数(Transaction Hooks)允许开发者在事务的不同阶段插入自定义逻辑。本篇博客将详细探讨事务钩子函数的概念及其在Spring Boot中的应用。
34 1
|
15天前
|
XML Java 数据库连接
精妙绝伦:玩转Spring事务编程的艺术
【4月更文挑战第20天】
30 0
|
22天前
|
XML Java 数据库
在Spring框架中,XML配置事务
在Spring框架中,XML配置事务
|
22天前
|
Java 数据库连接 数据库
spring事务失效(疑难杂症)
spring事务失效(疑难杂症)
|
28天前
|
XML Java 关系型数据库
注解驱动事务:Spring中基于注解的事务属性配置详解
注解驱动事务:Spring中基于注解的事务属性配置详解
43 0
注解驱动事务:Spring中基于注解的事务属性配置详解