Mysql--实战篇--@Transactional失效场景及避免策略(@Transactional实现原理,失效场景,内部调用问题等)
在Spring框架中,@Transactional注解用于声明式事务管理,能够简化事务的处理逻辑。然而,在某些情况下,@Transactional可能会失效,导致事务无法按预期工作。了解这些失效场景及其原因,可以帮助你更好地管理和调试事务问题。
1、@Transactional失效的常见场景
(1)、方法非public访问权限
@Transactional注解通常只能应用于public方法上。如果将其应用于protected、private或包级私有方法上,由于Spring的代理机制无法拦截这些方法的调用,因此事务注解将失效。
(2)、同一个类的内部调用
当@Transactional注解的方法在同一类内部被另一个方法调用时,事务可能会失效。这是因为Spring的AOP(面向切面编程)机制是通过代理对象来实现事务管理的。只有当外部类通过代理对象调用带有@Transactional注解的方法时,Spring才会拦截该方法并为其创建事务。而在同一类内部直接调用方法时,不会经过代理对象,因此事务不会生效。
内部调用示例:
@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方法移到另一个服务类中,确保它是通过代理对象调用的。
示例:
@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()获取当前类的代理对象,然后通过代理对象调用方法。
示例:
@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和未检查的异常进行回滚。如果在事务方法中捕获了异常并吞掉(即没有抛出或记录),事务将不会回滚,导致数据不一致。
错误示例:
@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块中记录异常日志,并根据需要重新抛出异常。
示例:
@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()手动标记事务为回滚。
示例:
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:如果当前存在事务,则在嵌套事务内执行;否则创建一个新的事务。
示例:
@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。
改进后的代码:
@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的事务管理器无法正确管理事务,导致事务失效。
异常示例:
@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来强制创建一个新的事务。这样即使在异步任务中,事务也能正常工作。
示例:
@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)。如果不正确配置这些规则,可能会导致事务在不应该回滚的情况下回滚,或者在应该回滚的情况下没有回滚。
示例:
@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指定具体的异常类型。
改进后的代码:
@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:
@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 如何通过代理对象来管理事务的生命周期。
示例:
// 假设这是 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)方式来解释下:
示例:
@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,所以事务就不会起作用了。
乘风破浪会有时,直挂云帆济沧海!!!