JAVA 使用 @Transactional 不生效?事务传播机制与代理原理深入解析

JAVA @Transactional 不生效?事务传播机制与代理原理深入解析

大家好!今天我们来聊聊 Java 中 @Transactional 注解失效的常见原因,以及深入探讨事务传播机制和代理原理,帮助大家更好地理解和使用事务。

首先,@Transactional 注解是 Spring 框架提供的声明式事务管理的核心注解。它可以简化事务管理的复杂性,开发者只需要通过注解的方式就能定义事务的边界和属性。但是,在实际开发中,我们经常会遇到 @Transactional 注解不生效的情况,导致数据一致性问题。 那么,到底是什么原因导致了这种问题呢?

一、@Transactional 注解失效的常见原因

  1. 未被 Spring 管理的 Bean

    @Transactional 注解依赖于 Spring 的 AOP (Aspect-Oriented Programming) 机制。只有被 Spring 管理的 Bean 才能被 AOP 代理,从而实现事务管理。 如果一个类没有被 Spring 容器管理(例如,通过 @Component, @Service, @Repository, @Controller 等注解声明),那么 @Transactional 注解就不会生效。

    //错误示例:未被 Spring 管理的类
    public class UserService {
    
        @Transactional
        public void transfer(int fromId, int toId, double amount) {
            //...
        }
    }

    解决方案: 确保你的类被 Spring 容器管理。

    @Service // 或者 @Component, @Repository, @Controller
    public class UserService {
    
        @Autowired
        private AccountDao accountDao;
    
        @Transactional
        public void transfer(int fromId, int toId, double amount) {
            accountDao.decreaseBalance(fromId, amount);
            accountDao.increaseBalance(toId, amount);
        }
    }
  2. 方法不是 public 的

    Spring 默认使用 JDK 动态代理来实现 AOP,而 JDK 动态代理只能代理接口方法,因此 @Transactional 注解只能用于 public 方法。 如果方法不是 public 的,Spring 将无法创建代理,事务也就不会生效。

    @Service
    public class UserService {
    
        @Transactional // 不生效,因为方法是 private 的
        private void transfer(int fromId, int toId, double amount) {
            //...
        }
    }

    解决方案: 确保你的事务方法是 public 的。

    @Service
    public class UserService {
    
        @Transactional // 生效
        public void transfer(int fromId, int toId, double amount) {
            //...
        }
    }
  3. 同一个类中方法调用

    这是最常见的问题之一。 当一个带有 @Transactional 注解的方法(例如 methodA)在同一个类中被另一个方法(例如 methodB)调用时,@Transactional 注解通常不会生效。 这是因为 Spring 的 AOP 代理是在 Bean 外部创建的,内部方法调用绕过了代理,导致事务增强逻辑没有被执行。

    @Service
    public class UserService {
    
        @Autowired
        private AccountDao accountDao;
    
        public void processTransfer(int fromId, int toId, double amount) {
            transfer(fromId, toId, amount); // 事务不生效
        }
    
        @Transactional
        public void transfer(int fromId, int toId, double amount) {
            accountDao.decreaseBalance(fromId, amount);
            accountDao.increaseBalance(toId, amount);
        }
    }

    解决方案:

    • transfer 方法抽取到另一个 Bean 中: 这是最推荐的解决方案。将 transfer 方法放到另一个 Spring 管理的 Bean 中,然后在 UserService 中注入该 Bean 并调用 transfer 方法。
    • 使用 TransactionTemplate TransactionTemplate 允许你手动控制事务的边界。
    • 使用 AopContext.currentProxy(): 这种方式不推荐使用,因为它依赖于 Spring 的内部实现,并且会导致代码的耦合度增加。你需要启用 expose-proxy="true" 才能使用。

    抽取到另一个 Bean 的示例:

    @Service
    public class TransferService {
    
        @Autowired
        private AccountDao accountDao;
    
        @Transactional
        public void transfer(int fromId, int toId, double amount) {
            accountDao.decreaseBalance(fromId, amount);
            accountDao.increaseBalance(toId, amount);
        }
    }
    
    @Service
    public class UserService {
    
        @Autowired
        private TransferService transferService;
    
        public void processTransfer(int fromId, int toId, double amount) {
            transferService.transfer(fromId, toId, amount); // 事务生效
        }
    }

    使用 TransactionTemplate 的示例:

    @Service
    public class UserService {
    
        @Autowired
        private TransactionTemplate transactionTemplate;
    
        @Autowired
        private AccountDao accountDao;
    
        public void processTransfer(int fromId, int toId, double amount) {
            transactionTemplate.execute(status -> {
                accountDao.decreaseBalance(fromId, amount);
                accountDao.increaseBalance(toId, amount);
                return null;
            });
        }
    }

    使用 AopContext.currentProxy() 的示例 (不推荐):

    首先,需要在 Spring 配置中启用 expose-proxy="true"

    <aop:aspectj-autoproxy expose-proxy="true"/>

    然后,在代码中使用 AopContext.currentProxy()

    @Service
    public class UserService {
    
        @Autowired
        private AccountDao accountDao;
    
        public void processTransfer(int fromId, int toId, double amount) {
            ((UserService) AopContext.currentProxy()).transfer(fromId, toId, amount); // 事务生效
        }
    
        @Transactional
        public void transfer(int fromId, int toId, double amount) {
            accountDao.decreaseBalance(fromId, amount);
            accountDao.increaseBalance(toId, amount);
        }
    }
  4. 异常被捕获但没有重新抛出

    @Transactional 注解默认情况下只会在 RuntimeExceptionError 抛出时才会回滚事务。 如果你捕获了异常,但没有重新抛出,事务将被认为是成功的,即使发生了错误。

    @Service
    public class UserService {
    
        @Autowired
        private AccountDao accountDao;
    
        @Transactional
        public void transfer(int fromId, int toId, double amount) {
            try {
                accountDao.decreaseBalance(fromId, amount);
                accountDao.increaseBalance(toId, amount);
            } catch (Exception e) {
                // 异常被捕获,但没有重新抛出,事务不会回滚
                e.printStackTrace();
            }
        }
    }

    解决方案: 重新抛出异常,或者使用 @Transactional 注解的 rollbackFor 属性指定需要回滚的异常类型。

    重新抛出异常的示例:

    @Service
    public class UserService {
    
        @Autowired
        private AccountDao accountDao;
    
        @Transactional
        public void transfer(int fromId, int toId, double amount) {
            try {
                accountDao.decreaseBalance(fromId, amount);
                accountDao.increaseBalance(toId, amount);
            } catch (Exception e) {
                // 重新抛出异常,事务会回滚
                throw new RuntimeException(e);
            }
        }
    }

    使用 rollbackFor 属性的示例:

    @Service
    public class UserService {
    
        @Autowired
        private AccountDao accountDao;
    
        @Transactional(rollbackFor = Exception.class) // 指定所有 Exception 都需要回滚
        public void transfer(int fromId, int toId, double amount) {
            try {
                accountDao.decreaseBalance(fromId, amount);
                accountDao.increaseBalance(toId, amount);
            } catch (Exception e) {
                // 异常被捕获,但 rollbackFor 指定了 Exception,事务会回滚
                e.printStackTrace();
            }
        }
    }
  5. 错误的事务传播行为

    事务传播行为定义了当一个被 @Transactional 注解的方法被另一个被 @Transactional 注解的方法调用时,事务如何传播。 如果传播行为配置不正确,可能会导致事务不生效。 例如,如果一个方法被配置为 PROPAGATION_NOT_SUPPORTED,那么它将运行在非事务环境中,即使调用它的方法有事务。

    @Service
    public class UserService {
    
        @Autowired
        private AccountDao accountDao;
    
        @Autowired
        private LogService logService;
    
        @Transactional
        public void transfer(int fromId, int toId, double amount) {
            accountDao.decreaseBalance(fromId, amount);
            accountDao.increaseBalance(toId, amount);
            logService.logTransfer(fromId, toId, amount); // 事务传播行为可能导致问题
        }
    }
    
    @Service
    public class LogService {
    
        @Transactional(propagation = Propagation.NOT_SUPPORTED) // 不支持事务
        public void logTransfer(int fromId, int toId, double amount) {
            // 记录日志
            // 即使 transfer 方法发生异常,logTransfer 方法的操作也不会回滚
        }
    }

    解决方案: 根据实际需求选择合适的事务传播行为。

  6. 数据库不支持事务

    如果你的数据库引擎不支持事务(例如,MySQL 的 MyISAM 引擎),那么 @Transactional 注解将不会生效。

    解决方案: 使用支持事务的数据库引擎,例如 MySQL 的 InnoDB 引擎。

  7. 使用了错误的事务管理器

    在 Spring 中,你需要配置一个事务管理器来处理事务。 如果你配置了错误的事务管理器,例如,将 JtaTransactionManager 用于单机应用,那么 @Transactional 注解将不会生效。

    解决方案: 根据你的应用场景选择合适的事务管理器。 对于单机应用,通常使用 DataSourceTransactionManager

  8. 异步方法中的事务

    如果在异步方法 (使用 @Async 注解) 中使用了 @Transactional 注解,事务通常不会生效。 这是因为异步方法是在另一个线程中执行的,而事务是线程绑定的。

    解决方案: 确保异步方法和调用它的方法在同一个事务上下文中,或者使用消息队列等机制来保证事务的一致性。 这种情况比较复杂,需要具体问题具体分析。

二、事务传播行为

事务传播行为定义了当一个事务方法调用另一个事务方法时,事务如何传播。 Spring 提供了以下几种事务传播行为:

传播行为 描述
PROPAGATION_REQUIRED 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 这是最常用的传播行为,也是 @Transactional 注解的默认传播行为。
PROPAGATION_REQUIRES_NEW 无论当前是否存在事务,都会创建一个新的事务。 如果当前存在事务,则将当前事务挂起,并在新的事务完成后恢复当前事务。
PROPAGATION_SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,则将当前事务挂起。
PROPAGATION_MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则创建一个嵌套事务。 嵌套事务可以独立于外部事务进行提交或回滚。 如果外部事务回滚,则嵌套事务也会回滚。 如果外部事务提交,则嵌套事务只有在成功提交后才会真正提交。 这种传播行为需要数据库支持嵌套事务,例如 Oracle 和 PostgreSQL。 在 MySQL 中,PROPAGATION_NESTED 相当于 PROPAGATION_REQUIRED

选择合适的事务传播行为非常重要,可以避免出现数据不一致的问题。

三、事务代理原理

Spring 通过 AOP 来实现声明式事务管理。 当一个 Bean 被 @Transactional 注解标记时,Spring 会为该 Bean 创建一个代理对象。 该代理对象会拦截对该 Bean 的方法的调用,并在方法执行前后执行事务相关的逻辑。

Spring 使用两种方式创建代理对象:

  1. JDK 动态代理: 如果 Bean 实现了接口,Spring 会使用 JDK 动态代理来创建代理对象。 JDK 动态代理只能代理接口方法。
  2. CGLIB 代理: 如果 Bean 没有实现接口,Spring 会使用 CGLIB 代理来创建代理对象。 CGLIB 代理可以代理类的方法,但不能代理 final 方法。

代理对象会拦截对被 @Transactional 注解标记的方法的调用,并在方法执行前后执行以下操作:

  1. 开启事务: 在方法执行前,代理对象会从事务管理器中获取一个事务,并开启该事务。
  2. 执行方法: 代理对象会调用目标对象的方法。
  3. 提交或回滚事务: 如果方法执行成功,代理对象会提交事务;如果方法执行过程中抛出异常,代理对象会回滚事务。
  4. 释放资源: 代理对象会释放事务相关的资源,例如数据库连接。

理解了事务代理原理,就能更好地理解 @Transactional 注解的工作方式,以及如何避免出现事务失效的问题。

四、实际案例分析

我们来看一个实际的案例,分析 @Transactional 注解失效的原因,并给出解决方案。

场景:

一个电商网站,用户下单时,需要扣减商品库存,创建订单,并发送短信通知。

@Service
public class OrderService {

    @Autowired
    private ProductDao productDao;

    @Autowired
    private OrderDao orderDao;

    @Autowired
    private SmsService smsService;

    @Transactional
    public void createOrder(int productId, int userId, int quantity) {
        // 1. 扣减商品库存
        productDao.decreaseStock(productId, quantity);

        // 2. 创建订单
        Order order = new Order();
        order.setProductId(productId);
        order.setUserId(userId);
        order.setQuantity(quantity);
        orderDao.createOrder(order);

        // 3. 发送短信通知
        smsService.sendSms(userId, "Order created successfully!");
    }
}

@Service
public class SmsService {

    public void sendSms(int userId, String message) {
        // 模拟发送短信,可能会失败
        if (Math.random() < 0.5) {
            throw new RuntimeException("Failed to send SMS!");
        }
        System.out.println("SMS sent to user " + userId + ": " + message);
    }
}

问题:

如果 smsService.sendSms() 方法抛出异常,productDao.decreaseStock() 方法和 orderDao.createOrder() 方法的操作不会回滚,导致数据不一致。

原因:

smsService.sendSms() 方法没有被 @Transactional 注解标记,因此它的异常不会触发事务回滚。

解决方案:

  1. sendSms() 方法也标记为 @Transactional

    @Service
    public class SmsService {
    
        @Transactional(propagation = Propagation.REQUIRES_NEW) // 创建新的事务
        public void sendSms(int userId, String message) {
            // 模拟发送短信,可能会失败
            if (Math.random() < 0.5) {
                throw new RuntimeException("Failed to send SMS!");
            }
            System.out.println("SMS sent to user " + userId + ": " + message);
        }
    }

    使用 PROPAGATION_REQUIRES_NEW 可以确保 sendSms() 方法在一个独立的事务中执行。 如果 sendSms() 方法失败,只会回滚 sendSms() 方法的事务,而不会影响 createOrder() 方法的事务。 当然,这种方式需要根据实际业务场景来判断是否合适。 如果发送短信是核心业务的一部分,那么可能更希望整个 createOrder() 方法都回滚。

  2. createOrder() 方法中捕获 sendSms() 方法的异常,并重新抛出:

    @Service
    public class OrderService {
    
        @Autowired
        private ProductDao productDao;
    
        @Autowired
        private OrderDao orderDao;
    
        @Autowired
        private SmsService smsService;
    
        @Transactional
        public void createOrder(int productId, int userId, int quantity) {
            try {
                // 1. 扣减商品库存
                productDao.decreaseStock(productId, quantity);
    
                // 2. 创建订单
                Order order = new Order();
                order.setProductId(productId);
                order.setUserId(userId);
                order.setQuantity(quantity);
                orderDao.createOrder(order);
    
                // 3. 发送短信通知
                smsService.sendSms(userId, "Order created successfully!");
    
            } catch (Exception e) {
                throw new RuntimeException(e); // 重新抛出异常,触发事务回滚
            }
        }
    }

    这种方式可以确保如果 sendSms() 方法失败,整个 createOrder() 方法都会回滚。

这个案例说明了在实际开发中,需要仔细分析业务场景,选择合适的事务传播行为和异常处理方式,才能保证数据的一致性。

五、排查 @Transactional 不生效问题的步骤

当遇到 @Transactional 注解不生效的问题时,可以按照以下步骤进行排查:

  1. 确认 Bean 是否被 Spring 管理: 检查类是否被 @Component, @Service, @Repository, @Controller 等注解标记。
  2. 确认方法是否是 public 的: @Transactional 注解只能用于 public 方法。
  3. 确认是否存在同一个类中的方法调用: 如果存在,需要将事务方法抽取到另一个 Bean 中,或者使用 TransactionTemplateAopContext.currentProxy()
  4. 确认异常是否被捕获但没有重新抛出: 如果捕获了异常,需要重新抛出,或者使用 @Transactional 注解的 rollbackFor 属性指定需要回滚的异常类型。
  5. 确认事务传播行为是否正确: 根据实际需求选择合适的事务传播行为。
  6. 确认数据库是否支持事务: 使用支持事务的数据库引擎。
  7. 确认是否使用了正确的事务管理器: 根据你的应用场景选择合适的事务管理器。
  8. 查看日志: Spring 的事务管理器会输出详细的日志,可以帮助你诊断问题。 启用 DEBUG 级别的日志可以提供更多信息。

代码之外的思考

理解 @Transactional 注解失效的原因以及事务传播机制和代理原理,能够帮助我们编写更健壮和可靠的应用程序。 掌握这些知识,我们可以更好地处理数据一致性问题,避免出现意外的错误。

总而言之,掌握 @Transactional 注解以及事务的传播机制和代理原理对于开发者来说至关重要。

排查问题有迹可循,问题的原因无外乎上面这几点

代理是核心,理解了代理才能更好的理解事务生效的原理

事务的传播行为需要根据实际的业务场景选择合适的策略

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注