开发者

Spring中事务失效的8种常见使用场景避坑指南

目录
  • 一、事务注解应用在非public方法上
    • 问题现象
    • 原理分析
    • 解决方案
  • 二、同一个类中方法调用
    • 问题现象
    • 原理分析
    • 解决方案
  • 三、异常被吞没
    • 问题现象
    • 原理分析
    • 解决方案
  • 四、异常类型不匹配回滚规则
    • 问题现象
    • 原理分析
    • 解决方案
  • 五、数据库不支持事务
    • 问题现象
    • 原理分析
    • 解决方案
  • 六、错误的传播行为设置
    • 问题现象
    • 原理分析
    • 解决方案
  • 七、未被Spring管理的类
    • 问题现象
    • 原理分析
    • 解决方案
  • 八、事务管理器配置错误
    • 问题现象
    • 原理分析
    • 解决方案
  • 总结

    Spring事务管理是企业级Java应用的核心功能,看似简单的@Transactional注解,如果使用不当将会引发严重的生产问题,比如因事务失效带来的数据不一致问题。

    事务失效往往不会抛出异常,而是静默发生,等到业务出现问题时才被发现,造成严重的数据不一致。

    本文将分析8种导致Spring事务失效的使用问题并提供相应的解决方案。

    一、事务注解应用在非public方法上

    问题现象

    开发者在非public方法上添加@Transactional注解,但事务没有生效。

    @Service
    public class UserService {
        
        @Autowired
        private UserMapper userMapper;
        
        // 事务无效!方法不是public的
        @Transactional
        protected void createUser(User user) {
            userMapper.insert(user);
            // 如果这里抛出异常,数据不会回滚
        }
    }

    原理分析

    Spring AOP的代理机制默认只拦截public方法。这是因为事务通知是基于Spring AOP实现的,而Spring AOP默认只拦截public方法调用。

    查看AbstractFallbackTransactionAttributeSource类的源码可以发现:

    // AbstractFallbackTransactionAttributeSource.java
    protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowpublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
            return null;
        }
        // ...
    }

    非public方法上的@Transactional注解会被直接忽略,返回null,导致事务不生效。

    解决方案

    确保标注@Transactional的方法为public:

    @Service
    public class UserService {
        
        @Autowired
        private UserMapper userMapper;
        
        // 正确:方法是public的
        @Transactional
        public void createUser(User user) {
            userMapper.insert(user);
            // 如果抛出异常,数据会回滚
        }
    }

    二、同一个类中方法调用

    问题现象

    在同一个类中,非事务方法调用事务方法,或者事务方法调用另一个具有不同事务属性的方法,事务会失效。

    @Service
    public clasXRyrrds OrderService {
        
        public void createOrder(Order order) {
            // 直接调用同类中的事务方法
            this.createOrderWithTransaction(order);
            // 如果createOrderWithTransaction方法抛出异常
            // 事务不会回滚!
        }
        
        @Transactional
        public void createOrderWithTransaction(Order order) {
            // 数据库操作...
            throw new RuntimeException("故意抛出异常");
        }
    }

    原理分析

    Spring事务基于动态代理实现,当在同一个类中调用方法时,是通过this引用调用的,而不是通过代理对象。这导致事务切面无法拦截方法调用,事务自然就失效了。

    简言之,只有通过代理对象调用方法时,Spring事务才会生效。

    解决方案

    有几种方法可以解决:

    方法1:使用自我注入

    @Service
    public class OrderService {
        
        @Autowired
        private OrderService self; // 自我注入,注入的是代理对象
        
        public void createOrder(Order order) {
            // 通过代理对象调用事务方法
            self.createOrderWithTransaction(order);
            // 如果createOrderWithTransaction方法抛出异常
            // 事务会正常回滚
        }
        
        @Transactional
        public void createOrderWithTransaction(Order order) {
            // 数据库操作...
            throw new RuntimeException("故意抛出异常");
        }
    }

    方法2:使用AopContext获取代理对象(需要额外配置)

    @EnableASPectJAutoProxy(exposeProxy = true) // 配置类上添加此注解
    public class AppConfig {
        // ...
    }
    
    @Service
    public class OrderService {
        
        public void createOrder(Order order) {
            // 通过AopContext获取代理对象
          android  ((OrderService) AopContext.currentProxy())
                .createOrderWithTransaction(order);
            // 事务会正常工作
        }
        
        @Transactional
        public void createOrderWithTransaction(Order order) {
            // 数据库操作...
            throw new RuntimeException("故意抛出异常");
        }
    }

    方法3:将方法拆分到不同的类中

    @Service
    public class OrderFacadeShttp://www.devze.comervice {
        
        @Autowired
        private TransactionalOrderService orderService;
        
        public void createOrder(Order order) {
            orderService.createOrderWithTransaction(order);
            // 事务会正常工作
        }
    }
    
    @Service
    public class TransactionalOrderService {
        
        @Transactional
        public void createOrderWithTransaction(Order order) {
            // 数据库操作...
            throw new RuntimeException("故意抛出异常");
        }
    }

    三、异常被吞没

    问题现象

    开发者在事务方法中捕获了异常但没有重新抛出,导致事务无法回滚。

    @Service
    public class PaymentService {
        
        @Autowired
        private PaymentMapper paymentMapper;
        
        @Transactional
        public void processPayment(Payment payment) {
            try {
                paymentMapper.insert(payment);
                // 其他业务逻辑
                throw new RuntimeException("支付处理失败");
            } catch (Exception e) {
                log.error("支付异常", e);
                // 错误:捕获异常但未重新抛出,事务不会回滚
            }
        }
    }

    原理分析

    Spring事务管理器是通过异常触发回滚的。当方法执行过程中抛出异常,并且这个异常满足回滚条件时,事务管理器才会执行回滚操作。如果异常被捕获且没有重新抛出,Spring事务管理器就不知道发生了异常,自然不会回滚事务。

    解决方案

    有两种解决方法:

    方法1:重新抛出异常

    @Service
    public class PaymentService {
        
        @Autowired
        private PaymentMapper paymentMapper;
        
        @Transactional
        public void processPayment(Payment payment) {
            try {
                paymentMapper.insert(payment);
                // 其他业务逻辑
                throw new RuntimeException("支付处理失败");
            } catch (Exception e) {
                log.error("支付异常", e);
                // 正确:重新抛出异常,事务会回滚
                throw e; // 或者抛出新的异常
            }
        }
    }

    方法2:使用TransactionAspectSupport手动回滚

    @Service
    public class PaymentService {
        
        @Autowired
        private PaymentMapper paymentMapper;
        
        @Transactional
        public void processPayment(Payment payment) {
            try {
                payme编程ntMapper.insert(payment);
                // 其他业务逻辑
                throw new RuntimeException("支付处理失败");
            } catch (Exception e) {
                log.error("支付异常", e);
                // 正确:手动标记事务回滚
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            }
        }
    }

    四、异常类型不匹配回滚规则

    问题现象

    开发者抛出了异常,但事务没有回滚。这通常是因为抛出的异常类型不满足默认的回滚规则。

    @Service
    public class ReportService {
        
        @Autowired
        private ReportMapper reportMapper;
        
        @Transactional
        public void generateReport() {
            reportMapper.insertReportData();
            // 其他业务逻辑
            if (somethingWrong()) {
                // 抛出受检异常,默认不会导致事务回滚
                throw new IOException("报告生成失败");
            }
        }
    }

    原理分析

    Spring默认只在遇到未检查异常(RuntimeException及其子类)和Error时才回滚事务。受检异常(如IOException、SQLException)被认为是业务异常,默认情况下不会触发事务回滚。

    这是因为@Transactional注解的默认配置是:

    @Transactional(rollbackFor = RuntimeException.class)

    解决方案

    有两种解决方法:

    方法1:指定回滚的异常类型

    @Service
    public class ReportService {
        
        @Autowired
        private ReportMapper reportMapper;
        
        // 正确:指定回滚的异常类型包括IOException
        @Transactional(rollbackFor = {IOException.class})
        public void generateReport() throws IOException {
            reportMapper.insertReportData();
            // 其他业务逻辑
            if (somethingWrong()) {
                throw new IOException("报告生成失败");
                // 现在会触发事务回滚
            }
        }
    }

    方法2:将受检异常转换为非受检异常

    @Service
    public class ReportService {
        
        @Autowired
        private ReportMapper reportMapper;
        
        @Transactional
        public void generateReport() {
            reportMapper.insertReportData();
            // 其他业务逻辑
            try {
                if (somethingWrong()) {
                    throw new IOException("报告生成失败");
                }
            } catch (IOException e) {
                // 将受检异常转换为非受检异常
                throw new RuntimeException("报告生成失败", e);
                // 现在会触发事务回滚
            }
        }
    }

    五、数据库不支持事务

    问题现象

    所有事务配置看起来都正确,但事务仍然不生效。

    原理分析

    某些数据库引擎不支持事务功能。最常见的例子是mysql的MyISAM引擎,它不支持事务操作。如果表使用的是MyISAM引擎,即使Spring事务配置正确,也无法实现事务回滚。

    解决方案

    确保使用支持事务的数据库引擎:

    1. 对于MySQL,使用InnoDB引擎而不是MyISAM

    2. 检查表的创建语句,确保引擎类型正确:

    -- 检查表引擎
    SHOW TABLE STATUS WHERE Name = 'your_table_name';
    
    -- 修改表引擎为InnoDB
    ALTER TABLE your_table_name ENGINE=InnoDB;

    六、错误的传播行为设置

    问题现象

    在嵌套事务场景中,内部事务的回滚没有按照预期工作。

    @Service
    public class OrderService {
        
        @Autowired
        private PaymentService paymentService;
        
        @Transactional
        public void createOrder(Order order) {
            // 保存订单
            orderMapper.insert(order);
            
            try {
                // 调用支付服务
                paymentService.processPayment(order.getPayment());
            } catch (Exception e) {
                log.error("支付失败,但订单已创建", e);
                // 处理支付失败逻辑,但期望订单依然保存
            }
        }
    }
    
    @Service
    public class PaymentService {
        
        // 错误的传播行为,会影响外部事务
        @Transactional(propagation = Propagation.REQUIRED)
        public void processPayment(Payment payment) {
            paymentMapper.insert(payment);
            throw new RuntimeException("支付处理失败");
        }
    }

    原理分析

    Spring提供了不同的事务传播行为,用于控制事务的边界。最常用的是:

    • REQUIRED:默认值,如果当前存在事务,则加入该事务;如果不存在,则创建新事务
    • REQUIRES_NEW:创建新事务,如果当前存在事务,则挂起当前事务
    • NESTED:如果当前存在事务,则创建嵌套事务;如果不存在,则等同于REQUIRED
    • SUPPORTS:如果当前存在事务,则加入该事务;如果不存在,则以非事务方式执行

    使用不当的传播行为会导致事务范围不符合预期,特别是在异常处理场景中。

    解决方案

    根据业务需求选择正确的传播行为:

    @Service
    public class OrderService {
        
        @Autowired
        private PaymentService paymentService;
        
        @Transactional
        public void createOrder(Order order) {
            // 保存订单
            orderMapper.insert(order);
            
            try {
                // 调用支付服务
                paymentService.processPayment(order.getPayment());
            } catch (Exception e) {
                log.error("支付失败,但订单已创建", e);
                // 处理支付失败逻辑,但期望订单依然保存
            }
        }
    }
    
    @Service
    public class PaymentService {
        
        // 正确:使用REQUIRES_NEW创建独立事务
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void processPayment(Payment payment) {
            paymentMapper.insert(payment);
            throw new RuntimeException("支付处理失败");
            // 只有支付事务会回滚,不影响外部订单事务
        }
    }

    传播行为选择指南

    1. 如果希望内部方法的异常不影响外部事务:使用REQUIRES_NEW

    2. 如果希望内部方法的回滚不影响外部事务,但共享同一连接:使用NESTED(注意,这需要数据库支持保存点)

    3. 如果希望完全共享外部事务的命运:使用REQUIRED

    七、未被Spring管理的类

    问题现象

    在类上添加了@Transactional注解,但事务没有生效。

    // 未被Spring容器管理!
    public class UserService {
        
        @Autowired
        private UserMapper userMapper;
        
        @Transactional
        public void createUser(User user) {
            userMapper.insert(user);
            throw new RuntimeException("测试");
            // 事务不会回滚
        }
    }

    原理分析

    Spring事务是通过AOP实现的,只有被Spring容器管理的Bean才能被代理,进而应用事务切面。如果类没有被Spring正确识别为Bean,@Transactional注解就无法生效。

    常见的原因包括:

    1. 类上缺少@Component@Service等注解

    2. 类没有被组件扫描到

    3. 类是通过new关键字直接创建的实例

    解决方案

    确保类被Spring容器管理:

    @Service // 正确:添加@Service注解
    public class UserService {
        
        @Autowired
        private UserMapper userMapper;
        
        @Transactional
        public void createUser(User user) {
            userMapper.insert(user);
            throw new RuntimeException("测试");
            // 现在事务会正常回滚
        }
    }

    同时,确保组件扫描配置正确:

    @Configuration
    @ComponentScan("com.example.service") // 确保包路径正确
    public class AppConfig {
        // ...
    }

    八、事务管理器配置错误

    问题现象

    使用了正确的事务注解,但事务不生效或者出现异常。

    原理分析

    Spring支持多种事务管理器,针对不同的持久化技术:

    • • DataSourceTransactionManager:适用于JDBC和MyBATis
    • • JpaTransactionManager:适用于JPA
    • • HibernateTransactionManager:适用于Hibernate

    如果配置了错误的事务管理器,或者在多数据源环境中未指定正确的事务管理器,会导致事务失效。

    解决方案

    单数据源环境:确保配置正确的事务管理器

    @Configuration
    @EnableTransactionManagement
    public class DatabaseConfig {
        
        @Bean
        public DataSource dataSource() {
            // 数据源配置...
            return new HikariDataSource();
        }
        
        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource) {
            // 使用与持久化技术匹配的事务管理器
            return new DataSourceTransactionManager(dataSource);
        }
    }

    多数据源环境:指定使用的事务管理器

    @Configuration
    @EnableTransactionManagement
    public class MultiDatabaseConfig {
        
        @Bean
        public DataSource primaryDataSource() {
            // 主数据源配置...
            return new HikariDataSource();
        }
        
        @Bean
        public DataSource secondaryDataSource() {
            // 次数据源配置...
            return new HikariDataSource();
        }
        
        @Bean
        public PlatformTransactionManager primaryTransactionManager() {
            return new DataSourceTransactionManager(primaryDataSource());
        }
        
        @Bean
        public PlatformTransactionManager secondaryTransactionManager() {
            return new DataSourceTransactionManager(secondaryDataSource());
        }
    }
    
    @Service
    public class UserService {
        
        // 指定使用的事务管理器
        @Transactional(transactionManager = "primaryTransactionManager")
        public void createUser(User user) {
            // 使用主数据源的操作
        }
        
        @Transactional(transactionManager = "secondaryTransactionManager")
        public void createUserLog(UserL编程og log) {
            // 使用次数据源的操作
        }
    }

    总结

    Spring事务是一个强大的特性,但也隐藏着许多陷阱。理解这些陷阱的原理,可以帮助你更好地利用Spring事务,构建更加健壮的应用程序。

    到此这篇关于Spring中事务失效的8种常见使用场景避坑指南的文章就介绍到这了,更多相关Spring事务失效内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜