Spring事务失效
Spring事务失效的本质是 Spring的声明式事务管理(@Transactional)基于AOP(动态代理)实现,当你的调用方式破坏了它的代理机制时,事务就不会生效。
核心原理回顾
首先,要理解失效,必须知道它如何工作。当你在一个方法上标注 @Transactional
时,Spring会为该Bean创建一个代理对象。当你调用这个代理对象的方法时,代理会在方法执行前开启事务,在方法执行后提交或回滚事务。如果你绕过了这个代理对象,直接调用真实对象的方法,事务增强就不会发生。
事务失效的常见场景
1. 事务方法非Public修饰(最常见陷阱之一)
- 原因: Spring的AOP代理(无论是JDK动态代理还是CGLIB)默认情况下只能对公共方法(public) 进行代理。这是由Spring的
AbstractFallbackTransactionAttributeSource
类决定的,它在计算事务属性时,非public方法会返回null
,导致不会为该方法创建代理事务逻辑。 - 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserService {
private void createUserPrivate(User user) { // 错误!private方法
userDao.insert(user);
// 其他操作...
}
protected void updateUserProtected(User user) { // 错误!protected方法
userDao.update(user);
}
} - 解决方案: 确保所有
@Transactional
注解的方法都是public
的。
2. 自调用(Invocations Within the Same Class)
- 原因: 这是最隐蔽和常见的陷阱。在一个Bean的内部,方法A调用带有
@Transactional
注解的方法B,此时调用的this.methodB()
是真实对象的方法,而不是代理对象的方法,因此事务注解不会生效。 - 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderService {
public void placeOrder(Order order) {
// ... 一些业务逻辑
this.updateOrderStatus(order); // 自调用,事务失效!
// 这里的 this 是真实对象,不是代理对象
}
public void updateOrderStatus(Order order) {
// 更新订单状态
orderDao.update(order);
}
} - 解决方案:
- (推荐)将方法B抽取到另一个Bean中: 这样通过注入的Bean调用,必然是代理对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class OrderService {
private TransactionalService transactionalService; // 注入另一个Bean
public void placeOrder(Order order) {
// ... 一些业务逻辑
transactionalService.updateOrderStatus(order); // 通过代理调用,事务有效
}
}
public class TransactionalService {
public void updateOrderStatus(Order order) {
// ...
}
} - 通过AopContext获取当前代理对象(不推荐,需要额外配置):
1
2
3
4
5// 首先在配置中开启暴露代理 @EnableAspectJAutoProxy(exposeProxy = true)
public void placeOrder(Order order) {
// ...
((OrderService) AopContext.currentProxy()).updateOrderStatus(order);
}
- (推荐)将方法B抽取到另一个Bean中: 这样通过注入的Bean调用,必然是代理对象。
3. 异常类型不正确或被捕获
- 原因:
@Transactional
默认只在抛出未检查的异常(即RuntimeException
及其子类)和Error时才会回滚。如果抛出了已检查异常(如Exception
,IOException
等),事务默认会提交。
更糟糕的是,如果你在方法内直接try-catch
了异常并且没有重新抛出,那么代理对象根本看不到任何异常,会认为方法执行成功,从而提交事务。 - 示例1 - 抛出检查异常:
1
2
3
4
5
6
7
public void createUser(User user) throws Exception {
userDao.insert(user);
if (someCondition) {
throw new Exception("这是一个检查异常"); // 事务会提交,不会回滚!
}
} - 示例2 - 异常被吞掉:
1
2
3
4
5
6
7
8
9
10
11
public void createUser(User user) {
try {
userDao.insert(user);
someOperationThatMightFail(); // 这里可能抛出RuntimeException
} catch (Exception e) {
// 捕获了异常,没有重新抛出!
log.error("Error occurred", e);
// 代理认为方法正常执行,会提交事务
}
} - 解决方案:
- 修改回滚的异常类型: 使用
@Transactional(rollbackFor = Exception.class)
,指定在遇到任何Exception
时都回滚。 - 正确处理异常: 在catch块中,如果需要回滚,必须手动抛出异常(通常是抛出一个
RuntimeException
)。1
2
3
4catch (Exception e) {
log.error("Error occurred", e);
throw new RuntimeException(e); // 或者自定义的运行时异常
}
- 修改回滚的异常类型: 使用
4. 数据库引擎不支持事务
- 原因: 如果你使用的是MySQL,并且数据表使用的存储引擎是MyISAM,那么事务功能是无效的,因为MyISAM本身就不支持事务。
- 解决方案: 将数据库表的存储引擎改为InnoDB。
5. 错误的传播特性(Propagation)
- 原因: propagation属性配置不当。例如:
@Transactional(propagation = Propagation.NOT_SUPPORTED)
: 表示以非事务方式运行,挂起当前事务。@Transactional(propagation = Propagation.NEVER)
: 表示不能存在事务,否则抛出异常。- 在一个没有事务的方法中,调用
Propagation.MANDATORY
(强制要求存在事务)的方法,会抛出异常。
- 解决方案: 根据业务逻辑正确配置传播行为。最常用的是
REQUIRED
(默认)和REQUIRES_NEW
。
6. 未被Spring管理的Bean(即没有使用Spring注解)
- 原因: 你在一个普通的、没有被
@Component
,@Service
,@Controller
等注解的类上使用@Transactional
,Spring根本不知道它的存在,自然不会为它创建代理。 - 示例:
1
2
3
4public class CommonService { // 缺少 @Service 等注解
// 无效
public void save() { ... }
} - 解决方案: 确保你的类是一个Spring Bean。
7. 多数据源配置中,事务管理器配置错误
- 原因: 在多个数据源的项目中,你可能配置了多个
PlatformTransactionManager
Bean。如果你在使用@Transactional
时没有指定使用哪个事务管理器,Spring会使用默认的(按类型注入,如果找到多个就会出问题)。 - 解决方案: 在使用
@Transactional
注解时,通过value
或transactionManager
属性明确指定使用哪个事务管理器Bean的名字。1
2// 指定使用名为orderTransactionManager的事务管理器
public void processOrder() { ... }
总结与排查 checklist
当遇到事务失效时,可以按以下顺序排查:
- 看方法是否是
public
。 - 看是不是自调用(同一个类中方法A调方法B)。
- 看异常是否被正确抛出(是不是检查异常?是不是被catch吞掉了?)。
- 看Bean是否被Spring管理(有没有
@Service
等注解)。 - 看数据库引擎(是否是InnoDB)。
- 看传播特性配置(是不是配了
NOT_SUPPORTED
,NEVER
等)。 - 看多数据源配置(是否指定了正确的事务管理器)。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技术之路!
评论