java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring事务@Transactional失效

Spring事务@Transactional失效的8大场景与解决方法

作者:Micro麦可乐

在我们开发Spring Boot应用中,很多小伙伴以为只要在方法上加一个 @Transactional,但实际开发中,事务经常出现失效的情况,下面我们就来看看如何解决吧

1. 前言

在我们开发Spring Boot应用中,很多小伙伴以为只要在方法上加一个 @Transactional,事务就能自动回滚,保证数据一致性。但实际开发中,事务经常出现失效的情况:明明抛了异常,数据库还是提交了。你肯定会疑惑:“为什么我加了注解,数据还是没回滚?”

通过本文博主将彻底和小伙伴们说清楚,让大家别再踩坑!从 @Transactional 的参数详解入手,再结合常见事务失效场景给出 正确写法 vs 错误写法 对比,帮助小伙伴们彻底理解 Spring 事务机制。

2. @Transactional 核心参数解析

Spring的 @Transactional 提供了很多参数,但日常最常用的主要有以下几个:

@Transactional(
    propagation = Propagation.REQUIRED, // 事务传播行为
    isolation = Isolation.DEFAULT,      // 事务隔离级别
    rollbackFor = Exception.class,      // 回滚规则
    timeout = 30,                       // 超时时间(秒)
    readOnly = false                    // 是否只读事务
)

2.1 propagation (事务传播行为)

作用:定义了当前事务方法与另一个事务方法相互调用时,事务应该如何传播。

Spring的传播属性有以下几种:

REQUIRED (默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

假设现在有一个需求添加用户的时候给用户同时发放优惠券

用户ServiceuserService.saveUser() 方法 调用 优惠券ServicecouponService.add()方法。

userService.saveUser() 运行时,先开启一个事务,此时couponService.add()方法发现已经存在一个事务,就不会再开启事务,只要任何一个方法报错则都会回调。

REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。

依旧以上述添加用户的时候给用户同时发放优惠券为例子

userService.saveUser()运行时,先开启一个事务A。当运行couponService.add()时,把事务A挂起,然后开启事务B。就算事务A发生回滚,事务B依然能正常提交。

外部事务不会影响内部事务的提交和回滚。

2.2 isolation(隔离级别)

作用:定义事务的隔离级别,解决并发事务可能带来的脏读、不可重复读、幻读等问题。

2.3 rollbackFor / noRollbackFor(回滚规则)

作用:指定哪些异常会触发事务回滚

默认:仅遇到运行时异常 RuntimeExceptionError 才回滚

如果要对检查型异常(如 IOException)回滚,必须显式配置 rollbackFor = Exception.class

2.4 timeout (超时时间)

作用:事务的超时时间,单位为秒。如果超过该时间事务尚未完成,则自动回滚

2.5 readOnly(是否只读事务)

作用:提示数据库该事务为只读事务,数据库可能会进行一些优化。默认为false

3. 事务失效的常见场景

下面列出最常见的 8大事务失效场景,并配合代码示例对比

3.1 方法不是 public

Spring 默认使用基于代理的 AOP,对于非 public 方法,代理对象无法拦截到其调用,导致注解失效。

错误写法:

@Service
public class UserService {
    
    @Transactional
    void saveUser(User user) { // 不是public
        userMapper.insert(user);
        throw new RuntimeException("模拟异常");
    }
}

正确写法:

@Service
public class UserService {
    
    @Transactional
    public void saveUser(User user) {
        userMapper.insert(user);
        throw new RuntimeException("模拟异常");
    }
}

3.2 自调用(同类方法内部调用)

类内部的方法 A 调用另一个有 @Transactional 注解的方法 B,这属于自调用。调用的是 this 对象本身的方法,而不是被 Spring 代理增强过的对象的方法,因此事务不会生效。

错误写法:

@Service
public class UserService {

    @Transactional
    public void addUser(User user) {
        userMapper.insert(user);
        throw new RuntimeException("模拟异常");
    }

    public void process(User user) {
        // 自调用,绕过代理
        this.addUser(user);
    }
}

正确写法(通过代理调用):

@Service
public class UserService {

    @Transactional
    public void addUser(User user) {
        userMapper.insert(user);
        throw new RuntimeException("模拟异常");
    }
}

@Service
public class OrderService {

    @Autowired
    private UserService userService;

    public void process(User user) {
        // 通过Spring代理调用,事务才生效
        userService.addUser(user);
    }
}

3.3 异常被捕获“吃掉”

如果你在方法中捕获了异常,并且没有重新抛出,那么 Spring 的事务拦截器就感知不到异常,自然不会触发回滚

错误写法:

@Transactional
public void saveUser(User user) {
    try {
        userMapper.insert(user);
        int i = 1 / 0; // 异常
    } catch (Exception e) {
        System.out.println("异常被吃掉");
    }
}

正确写法(继续抛出):

@Transactional
public void saveUser(User user) {
    try {
        userMapper.insert(user);
        int i = 1 / 0;
    } catch (Exception e) {
        throw new RuntimeException("抛出异常,保证回滚", e);
    }
}

3.4 异常类型不匹配

默认情况下,只有 RuntimeExceptionError 会触发回滚。如果你抛出了 IOException, SQLException受检异常,事务依然会提交

错误写法:

@Transactional
public void saveUser(User user) throws IOException {
    userMapper.insert(user);
    throw new IOException("检查型异常");
}

正确写法:

@Transactional(rollbackFor = Exception.class)
public void saveUser(User user) throws IOException {
    userMapper.insert(user);
    throw new IOException("检查型异常");
}

3.5 数据库引擎不支持事务

以 MySQL 为例:InnoDB 支持事务MyISAM 不支持事务

如果表使用了 MyISAM,即使 @Transactional 生效,也不会回滚

-- 检查并修改表引擎
SHOW TABLE STATUS LIKE ‘your_table_name';
ALTER TABLE your_table_name ENGINE = InnoDB;

3.6 在异步方法中使用

@Async 标记的异步方法中,操作是在一个新的线程中执行的。此时的事务上下文和新线程的事务上下文是不同的,需要配置特殊的事务管理器来支持,否则容易失效

// 需要特殊配置的场景
@Service
public class AsyncService {

    @Async
    @Transactional // 普通配置下,此事务很可能失效
    public void asyncTask() {
        // 异步事务操作
    }
}

3.7 未被Spring容器管理

这也是一些小伙伴粗心容易犯的错,类没有加上 @Service, @Component 等注解,它就不会被 Spring 扫描并创建为 Bean,那么它上面的 @Transactional 注解自然无效

// 错误示例:只是一个普通类,不是Spring Bean
// @Service 注释掉了
public class MyService {

    @Transactional // 这个注解完全没用
    public void doSomething() {
        // ...
    }
}

3.8 propagation 参数设置错误

错误地设置传播行为可能导致意外情况。例如,在已经存在事务的方法中,以 NOT_SUPPORTEDNEVER 模式调用新方法

// 可能非预期的行为
@Service
public class MainService {

    @Transactional
    public void mainMethod() {
        // ... 主逻辑
        subService.subMethod(); // 子方法挂起了当前事务
    }
}

@Service
public class SubService {

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void subMethod() {
        // 此方法以非事务方式运行,即使操作失败,也不影响mainMethod的事务
        // 如果这里的操作需要原子性,那就出了问题
    }
}

4. 结语

小伙伴们通过本文的讲解,是否让你对Spring事务@Transactional注解有了一个更深的认识?实际上我们大部分开发场景都使用默认配置即可,同时建议都按照 @Transactional(rollbackFor = Exception.class) 进行注解,业务处理过程避免抛出受检异常IOException, SQLException 等,导致失效!

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

您可能感兴趣的文章:
阅读全文