Mysql--实战篇--@Transactional失效场景及避免策略(@Transactional实现原理,失效场景,内部调用问题等)

在Spring框架中,@Transactional注解用于声明式事务管理,能够简化事务的处理逻辑。然而,在某些情况下,@Transactional可能会失效,导致事务无法按预期工作。了解这些失效场景及其原因,可以帮助你更好地管理和调试事务问题。

1、@Transactional失效的常见场景

(1)、方法非public访问权限

@Transactional注解通常只能应用于public方法上。如果将其应用于protected、private或包级私有方法上,由于Spring的代理机制无法拦截这些方法的调用,因此事务注解将失效。

(2)、同一个类的内部调用

当@Transactional注解的方法在同一类内部被另一个方法调用时,事务可能会失效。这是因为Spring的AOP(面向切面编程)机制是通过代理对象来实现事务管理的。只有当外部类通过代理对象调用带有@Transactional注解的方法时,Spring才会拦截该方法并为其创建事务。而在同一类内部直接调用方法时,不会经过代理对象,因此事务不会生效。

内部调用示例:

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // 带有@Transactional注解的方法
    @Transactional
    public void createUser(User user) {
        userRepository.save(user);
    }

    // 同一类内部调用带有@Transactional注解的方法
    public void createUserInternal() {
        User user = new User();
        user.setName("Alice");
        createUser(user);      // 事务不会生效
    }
}

解释:

在上面的例子中,createUser方法虽然带有@Transactional注解,但在createUserInternal方法中直接调用了createUser,这不会触发Spring的事务管理机制。因为createUserInternal和createUser是同一个类的方法,调用是通过this引用进行的,而不是通过代理对象调用的,因此事务不会生效。

解决方案:

  • 拆分到不同类:将createUser方法移到另一个服务类中,确保它是通过代理对象调用的。
    示例:
java 复制代码
  @Service
  public class UserService {

      @Autowired
      private UserRepository userRepository;

      @Transactional
      public void createUser(User user) {
          userRepository.save(user);
      }
  }

  @Service
  public class AnotherService {

      @Autowired
      private UserService userService;

      public void createUserInternal() {
          User user = new User();
          user.setName("Alice");
          userService.createUser(user);  // 通过代理对象调用,事务生效
      }
  }
  • 使用自定义代理:可以通过AopContext.currentProxy()获取当前类的代理对象,然后通过代理对象调用方法。
    示例:
java 复制代码
  @Service
  public class UserService {

      @Autowired
      private UserRepository userRepository;

      @Transactional
      public void createUser(User user) {
          userRepository.save(user);
      }

      public void createUserInternal() {
          User user = new User();
          user.setName("Alice");
          ((UserService) AopContext.currentProxy()).createUser(user);  // 使用代理对象调用
      }
  }

(3)、事务管理器配置错误

如果Spring容器中配置了多个事务管理器,但在使用@Transactional注解时没有明确指定事务管理器,可能会导致Spring使用默认的事务管理器,而这个默认的事务管理器可能不适用于当前的操作,从而导致事务注解失效。

(4)、方法内部捕捉异常

在使用@Transactional注解的方法中,如果内部捕获了可能导致事务回滚的异常,并且没有重新抛出一个Spring框架能够识别的运行时异常或声明式异常,那么事务管理器将无法感知到异常,从而可能导致事务不会回滚。

@Transactional注解默认只会对RuntimeException和未检查的异常进行回滚。如果在事务方法中捕获了异常并吞掉(即没有抛出或记录),事务将不会回滚,导致数据不一致。

错误示例:

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void createUser(User user) {
        try {
            userRepository.save(user);
            // 模拟异常
            if (user.getName().equals("Alice")) {
                throw new RuntimeException("模拟异常");
            }
        } catch (Exception e) {
            // 异常被捕获并吞掉,事务不会回滚
            System.out.println("捕获到异常:" + e.getMessage());
        }
    }
}

解释:

在上面的例子中,当user.getName()等于"Alice"时,会抛出一个RuntimeException,但这个异常被捕获并在catch块中处理,没有重新抛出。由于@Transactional注解默认只对未捕获的RuntimeException进行回滚,因此事务不会回滚,导致数据不一致。

解决方案:

  • 不要吞掉异常:确保在catch块中记录异常日志,并根据需要重新抛出异常。
    示例:
java 复制代码
  @Transactional
  public void createUser(User user) {
      try {
          userRepository.save(user);
          if (user.getName().equals("Alice")) {
              throw new RuntimeException("模拟异常");
          }
      } catch (Exception e) {
          // 记录异常日志
          logger.error("创建用户时发生异常", e);
          // 重新抛出异常,确保事务回滚
          throw e;
      }
  }
  • 手动标记事务为回滚:如果不希望重新抛出异常,可以使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记事务为回滚。
    示例:
java 复制代码
  import org.springframework.transaction.interceptor.TransactionAspectSupport;

  @Transactional
  public void createUser(User user) {
      try {
          userRepository.save(user);
          if (user.getName().equals("Alice")) {
              throw new RuntimeException("模拟异常");
          }
      } catch (Exception e) {
          // 手动标记事务为回滚
          TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
          logger.error("创建用户时发生异常", e);
      }
  }

(5)、使用final修饰的方法

如果使用final关键字修饰了方法,那么由于该方法不能被重写,Spring的代理机制将无法对其应用@Transactional注解,因此事务将失效。

(6)、静态方法

静态方法同样无法通过动态代理来应用@Transactional注解,因为静态方法不属于类的实例方法,而是属于类本身。

(7)、未被Spring管理的类

如果一个类没有被Spring管理(即没有使用@Controller、@Service、@Component、@Repository等注解进行标注),那么该类中的方法即使使用了@Transactional注解也不会生效。

(8)、传播特性配置错误

@Transactional 注解支持多种事务传播行为(Propagation),不同的传播行为会影响事务的创建和管理方式。如果不正确配置传播行为,可能会导致事务不按预期工作。

可以指定propagation参数来定义事务的传播行为。如果传播特性配置错误(例如设置为Propagation.NEVER,而当前存在事务),则事务将不会生效。

常见的传播行为:

  • REQUIRED(默认):如果当前存在事务,则加入该事务;否则创建一个新的事务。
  • REQUIRES_NEW:总是创建一个新的事务,如果当前存在事务,则将其挂起。
  • SUPPORTS:如果当前存在事务,则加入该事务;否则以非事务方式执行。
  • NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则将其挂起。
  • MANDATORY:必须在一个现有的事务中执行,否则抛出异常。
  • NEVER:必须在没有事务的情况下执行,否则抛出异常。
  • NESTED:如果当前存在事务,则在嵌套事务内执行;否则创建一个新的事务。

示例:

java 复制代码
@Service
public class UserService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        // 执行一些数据库操作
        methodB();
    }

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void methodB() {
        // 这里的事务不会生效,因为propagation设置为NOT_SUPPORTED
        // 任何数据库操作都不会参与事务
    }
}

解决方案:

  • 根据业务需求选择合适的传播行为。例如,如果你希望methodB与methodA共享同一个事务,应该使用REQUIRED或REQUIRES_NEW,而不是NOT_SUPPORTED或NEVER。
    改进后的代码:
java 复制代码
  @Service
  public class UserService {

      @Transactional(propagation = Propagation.REQUIRED)
      public void methodA() {
          // 执行一些数据库操作
          methodB();
      }

      @Transactional(propagation = Propagation.REQUIRED)
      public void methodB() {
          // 现在 methodB 会与 methodA 共享同一个事务
      }
  }

(9)、数据库不支持事务

如果使用的数据库表不支持事务(例如,某些类型的存储引擎或数据库系统不支持事务),那么即使使用了@Transactional注解,事务也不会生效。

(10)、异步线程调用

在Spring中,@Async注解用于开启异步任务执行。默认情况下,@Async和@Transactional不能同时生效。这是因为在异步任务中,Spring的事务管理器无法正确管理事务,导致事务失效。

异常示例:

java 复制代码
@Service
public class AsyncService {

    @Autowired
    private UserRepository userRepository;

    @Async
    @Transactional
    public void createUserAsync(User user) {
        userRepository.save(user);
        // 模拟异常
        if (user.getName().equals("Alice")) {
            throw new RuntimeException("模拟异常");
        }
    }
}

解释:

在上面的例子中,createUserAsync方法同时标注了@Async和@Transactional。由于@Async注解会将方法的执行交给一个新的线程池,而Spring的事务管理器是基于主线程的,因此在异步任务中,事务管理器无法正确管理事务,导致事务失效。

解决方案:

  • 使用Propagation.REQUIRES_NEW:可以在异步方法中使用Propagation.REQUIRES_NEW来强制创建一个新的事务。这样即使在异步任务中,事务也能正常工作。
    示例:
java 复制代码
  @Async
  @Transactional(propagation = Propagation.REQUIRES_NEW)  // 强制执行事务
  public void createUserAsync(User user) {
      userRepository.save(user);
      if (user.getName().equals("Alice")) {
          throw new RuntimeException("模拟异常");
      }
  }
  • 避免在异步方法中使用事务:如果异步任务不需要事务支持,建议将事务逻辑移到同步方法中,或者在异步任务中手动管理事务。

(11)、事务回滚规则设置不当

@Transactional注解允许你指定哪些异常会导致事务回滚(rollbackFor)和哪些异常不会导致事务回滚(noRollbackFor)。如果不正确配置这些规则,可能会导致事务在不应该回滚的情况下回滚,或者在应该回滚的情况下没有回滚。

示例:

java 复制代码
@Service
public class UserService {

    @Transactional(rollbackFor = Exception.class)
    public void updateUser(User user) {
        // 执行更新操作
        userMapper.updateUser(user);
        // 抛出一个自定义异常
        throw new CustomException("自定义异常");
    }

    public class CustomException extends Exception {
        public CustomException(String message) {
            super(message);
        }
    }
}

解决方案:

  • 根据业务需求合理配置rollbackFor和noRollbackFor。通常情况下,@Transactional默认只会对RuntimeException和其子类进行回滚。如果你希望对其他类型的异常也进行回滚,可以使用rollbackFor指定具体的异常类型。
    改进后的代码:
java 复制代码
  @Service
  public class UserService {

      @Transactional(rollbackFor = {CustomException.class, RuntimeException.class})
      public void updateUser(User user) {
          // 执行更新操作
          userMapper.updateUser(user);
          // 抛出一个自定义异常
          throw new CustomException("自定义异常");
      }

      public class CustomException extends Exception {
          public CustomException(String message) {
              super(message);
          }
      }
  }

如果你不希望某些异常导致事务回滚,可以使用 noRollbackFor:

java 复制代码
  @Service
  public class UserService {

      @Transactional(noRollbackFor = CustomException.class)
      public void updateUser(User user) {
          // 执行更新操作
          userMapper.updateUser(user);
          // 抛出一个自定义异常
          throw new CustomException("自定义异常");
      }

      public class CustomException extends Exception {
          public CustomException(String message) {
              super(message);
          }
      }
  }

2、避免@Transactional注解失效策略

(1)、确保方法是public的。

(2)、避免在同一个类中直接调用其他事务方法(可以通过注入自身的方式来解决)。

(3)、正确配置事务管理器。

(4)、不要在事务方法内部捕获并处理可能导致事务回滚的异常(或者重新抛出一个Spring框架能够识别的异常)。

(5)、避免使用final和static修饰事务方法。

(6)、确保类被Spring管理。

(7)、正确配置事务的传播特性。

(8)、使用支持事务的数据库表和存储引擎。

(9)、在多线程环境下,确保事务方法在同一个线程中执行。

通过遵循这些原则,可以最大程度地确保@Transactional注解在Spring框架中的正确性和有效性。

3、@Transactional实现原理

在Spring框架中,@Transactional注解用于声明式事务管理,它通过AOP(面向切面编程)来实现。Spring使用代理机制来拦截带有@Transactional注解的方法调用,并在其周围添加事务管理逻辑。

(1)、Spring创建代理对象

当Spring容器启动时,它会扫描所有容器中带有@Transactional 注解的方法,并为这些方法创建一个代理对象。这个代理对象会拦截对原始业务逻辑类的调用,并在方法执行前后添加事务管理逻辑。

这个代理对象会在方法调用前后执行以下操作:

  • 开启事务:在方法执行之前,Spring会检查当前是否存在事务。如果不存在,则创建一个新的事务。
  • 提交或回滚事务:在方法执行完毕后,Spring会根据方法的执行结果决定是提交还是回滚事务。如果方法正常结束,则提交事务;如果方法抛出异常,则根据配置决定是否回滚事务。
  • 传播行为:@Transactional注解还支持多种传播行为(如REQUIRED、REQUIRES_NEW等),决定了如何处理现有事务或创建新事务。

创建代理对象的方式:

  • CGLIB动态代理(默认方式)
    如果目标类没有实现接口,Spring会使用CGLIB动态代理来生成代理类。CGLIB通过继承目标类并重写其方法来实现代理。
  • JDK动态代理
    如果目标类实现了接口,Spring会优先使用JDK动态代理。JDK动态代理通过实现接口并使用InvocationHandler来拦截方法调用。

(2)、@Transactional伪代码

下面是 @Transactional 代理机制的伪代码描述,展示了 Spring 如何通过代理对象来管理事务的生命周期。

示例:

java 复制代码
// 假设这是 Spring 创建的代理对象
public class UserServiceProxy implements InvocationHandler {

    // 目标对象(原始的 UserService 实例)
    private final Object target;

    // 事务管理器
    private final PlatformTransactionManager transactionManager;

    // 事务属性(从 @Transactional 注解中读取)
    private final TransactionAttribute transactionAttribute;

    public UserServiceProxy(Object target, PlatformTransactionManager transactionManager, TransactionAttribute transactionAttribute) {
        this.target = target;
        this.transactionManager = transactionManager;
        this.transactionAttribute = transactionAttribute;
    }

    // 拦截对目标对象的方法调用
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 检查是否是需要事务管理的方法
        if (method.isAnnotationPresent(Transactional.class)) {
            return executeWithTransaction(method, args);
        } else {
            // 如果不是事务方法,直接调用目标对象的方法
            return method.invoke(target, args);
        }
    }

    // 执行带有事务管理的方法
    private Object executeWithTransaction(Method method, Object[] args) throws Throwable {
        // 1. 获取当前事务状态
        TransactionStatus status = null;
        try {
            // 2. 开启新事务或加入现有事务
            status = transactionManager.getTransaction(transactionAttribute);

            // 3. 调用目标对象的业务逻辑方法
            Object result = method.invoke(target, args);

            // 4. 提交事务
            transactionManager.commit(status);

            // 5. 返回业务方法的结果
            return result;
        } catch (Exception e) {
            // 6. 如果发生异常,回滚事务
            if (status != null) {
                transactionManager.rollback(status);
            }
            // 7. 抛出异常,让调用者处理
            throw e;
        } finally {
            // 8. 清理资源(如关闭连接等)
            // 这一步通常由连接池自动处理
        }
    }
}

// 假设这是 Spring 容器中的代理工厂
public class TransactionalProxyFactory {
    public Object createProxy(Object target, Class<?>[] interfaces, PlatformTransactionManager transactionManager, TransactionAttribute transactionAttribute) {
        // 使用 JDK 动态代理
        if (interfaces.length > 0) {
            return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                interfaces,
                new UserServiceProxy(target, transactionManager, transactionAttribute)
            );
        }
        // 使用 CGLIB 动态代理
        else {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(target.getClass());
            enhancer.setCallback(new UserServiceProxy(target, transactionManager, transactionAttribute));
            return enhancer.create();
        }
    }
}

(3)、实例分析解释

通过使用自我注入(Self-Injection)方式来解释下:

示例:

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private UserService self;  // 自我注入

    @Transactional
    public void createUser(User user) {
        // 业务逻辑:插入用户数据
        userMapper.insertUser(user);
        
        // 通过代理对象调用 updateUser 方法
        self.updateUser(user);
    }

    @Transactional
    public void updateUser(User user) {
        // 业务逻辑:更新用户数据
        userMapper.updateUser(user);
    }
}

解释:

  • Spring服务在创建容器时会自动扫描@Service,@Component,@Transactional等注解,并在容器中创建实例对象。在使用的地方通过@Autowired或@Resource注解可以拿出该对象的代理对象使用(注意此时拿出来的是代理对象,而不是原始对象)。
  • 针对@Transactional注解,spring内部通过aop的方式对注解方法做了围绕增强,如帮我们开启事务,结束帮我们提交事务等,如第二部分的伪代码。
  • 在本例中,本身就是UserService,内部在注入UserService self对象,这两个对象都是实现一样的功能,只不过self对象通过@Autowired注入,实际为UserService的代理对象。
    如果类中直接使用自身的updateUser方法,属于内部直接调用的范畴。如果使用self对象调用updateUser方法,则是通过代理对象实现的。代理对象会执行aop使事务生效。内部调用没有调用aop,所以事务就不会起作用了。

乘风破浪会有时,直挂云帆济沧海!!!

相关推荐
m0_748235954 小时前
CentOS 7使用RPM安装MySQL
android·mysql·centos
leegong231114 小时前
PostgreSQL 初中级认证可以一起学吗?
数据库
秋野酱5 小时前
如何在 Spring Boot 中实现自定义属性
java·数据库·spring boot
AI航海家(Ethan)6 小时前
PostgreSQL数据库的运行机制和架构体系
数据库·postgresql·架构
Kendra9199 小时前
数据库(MySQL)
数据库·mysql
时光书签10 小时前
Mongodb副本集群为什么选择3个节点不选择4个节点
数据库·mongodb·nosql
人才程序员11 小时前
【C++拓展】vs2022使用SQlite3
c语言·开发语言·数据库·c++·qt·ui·sqlite
极客先躯11 小时前
高级java每日一道面试题-2025年01月23日-数据库篇-主键与索引有什么区别 ?
java·数据库·java高级·高级面试题·选择合适的主键·谨慎创建索引·定期评估索引的有效性
指尖下的技术11 小时前
Mysql面试题----MyISAM和InnoDB的区别
数据库·mysql