前言
作为Java开发工程师,相信大家对Spring中事务的使用并不陌生。但你可能只是停留在基础的使用层面上,当遇到特殊场景时,事务可能没有生效,直接在生产上暴露,导致严重的生产事故。今天,我们就来深入探讨Spring事务失效的各种场景及其解决方案 。
一、Spring事务原理简介
在深入了解事务失效场景前,我们先简单回顾一下Spring事务的工作原理。在JDBC中,我们需要手动控制事务:获取连接、设置自动提交为false、执行SQL、提交或回滚事务。这种方式对业务代码侵入性太大 。 Spring通过@Transactional注解控制事务,底层基于AOP实现。Spring在bean初始化过程中,发现方法有@Transactional注解时,会对相应的Bean进行代理,生成代理对象。方法调用时,会执行切面逻辑,包括开启事务、提交或回滚事务等。需要注意的是,Spring本身不实现事务,底层仍然依赖于数据库的事务支持 。
二、Spring事务失效的常见场景
1. 抛出检查异常(checked exceptions)
问题描述:当方法抛出检查异常(如IOException)而非运行时异常时,默认情况下事务不会回滚。
java
@Transactional
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
失效原因 :Spring默认只会在遇到运行时异常(RuntimeException)或错误(Error)时进行回滚 。 解决方案 :配置rollbackFor属性。
java
@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
2. 业务方法本身捕获了异常
问题描述:在业务方法中捕获异常而不重新抛出,导致事务无法感知异常。
java
@Transactional(rollbackFor = Exception.class)
public void transactionTest() {
try {
User user = new User();
UserService.insert(user);
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace(); // 异常被"吃掉"了
}
}
失效原因 :Spring是否回滚事务取决于是否抛出异常,如果异常被捕获,Spring无法处理事务 。 解决方案:
- 将异常原样抛出
- 手动设置回滚:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
3. 同一类中的方法调用
问题描述:在同一类中,非事务方法调用事务方法,事务不生效。
java
@Service
public class DefaultTransactionService implements Service {
public void saveUser() throws Exception {
doInsert(); // 直接调用,事务失效
}
@Transactional(rollbackFor = Exception.class)
public void doInsert() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();
}
}
失效原因 :Spring的事务管理通过动态代理实现。当在同一个类中直接调用方法时,不会经过代理,而是直接调用真实对象的方法 。 解决方案:
-
将方法拆分到不同的类中
-
通过代理对象调用:
java@Service public class DefaultTransactionService implements Service { @Autowired private DefaultTransactionService self; public void saveUser() throws Exception { self.doInsert(); // 通过代理调用 } } -
使用
@EnableAspectJAutoProxy(exposeProxy = true)和AopContext.currentProxy()
4. 方法使用final或static关键字
问题描述:使用final或static关键字修饰事务方法。
less
@Transactional
public final void save(User user) {
// 业务逻辑
}
@Transactional
public static void save(User user) {
// 业务逻辑
}
失效原因 :Spring使用Cglib代理时,通过生成子类并重写方法来实现代理。如果方法被final或static修饰,子类无法重写该方法。使用JDK动态代理时,基于接口实现,final和static方法也无法被代理 。 解决方案:移除方法上的final或static关键字。
5. 方法不是public
问题描述 :对非public方法使用@Transactional注解。
typescript
@Transactional
protected boolean save(User user) {
// 业务逻辑
}
失效原因 :Spring的事务管理源码AbstractFallbackTransactionAttributeSource中有判断,如果目标方法不是public的,则返回null 。
kotlin
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
解决方案:将方法访问级别改为public。
6. 错误使用传播机制
问题描述:事务传播行为配置不当导致事务不符合预期。
java
@Service
public class TransactionService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void doInsert(User user, Address address) throws Exception {
userMapper.insert(user);
saveAddress(address);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAddress(Address address) {
addressMapper.insert(address);
}
}
失效原因 :使用REQUIRES_NEW传播机制时,会新建事务并挂起当前事务。如果父事务发生异常,不会影响子事务的提交 。 传播机制说明:
- REQUIRED(默认):如果当前存在事务则加入,否则新建事务
- REQUIRES_NEW:新建事务,挂起当前事务(若存在)
- NOT_SUPPORTED:以非事务方式执行,挂起当前事务(若存在)
- SUPPORTS:如果当前有事务则加入,否则以非事务方式执行
解决方案:根据业务需求选择合适的传播行为,通常使用默认的REQUIRED即可。
7. 没有被Spring管理
问题描述:类没有被Spring管理,导致事务注解无效。
typescript
// @Service 注解被注释
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// 更新订单
}
}
失效原因 :如果类没有被Spring管理(没有@Service、@Component等注解),即使方法上有@Transactional注解,事务也不会生效 。 解决方案:确保类被Spring管理,添加相应的注解。
8. 多线程调用
问题描述:在多线程环境下调用事务方法。
java
@Service
public class UserService {
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing(); // 新线程中调用
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
int i = 1/0; // 异常不会导致主事务回滚
}
}
失效原因 :Spring将数据库连接放在ThreadLocal中,不同线程使用不同的数据库连接,因此是多线程不同的事务 。 解决方案:尽量避免在多线程中处理事务,或考虑使用分布式事务。
9. 其他失效场景
数据库引擎不支持事务 :如MySQL的MyISAM引擎不支持事务,应使用InnoDB引擎 。 事务管理器未配置 :在Spring Boot中会自动配置,但在传统Spring项目中需手动配置事务管理器 。 切面顺序问题:自定义切面捕获异常可能导致事务失效 。
kotlin
@Aspect
@Component
public class AuditAspect {
@Around(value = "execution (* com.example..*.*(..))")
public Object around(ProceedingJoinPoint pjp) {
try {
return pjp.proceed();
} catch (Throwable e) {
// 捕获异常后事务失效
log.error("错误", e);
}
return null;
}
}
三、调试事务问题的方法
-
开启事务日志:在application.properties中添加:
inilogging.level.org.springframework.transaction=DEBUG -
数据库监控 :通过
SHOW ENGINE INNODB STATUS查看事务状态 。 -
代码审查:检查是否触发了上述失效场景。
四、最佳实践总结
- 明确事务边界:根据业务场景选择合适的传播行为,避免过度使用REQUIRES_NEW导致性能开销 。
- 代理调用原则:同类方法调用必须通过代理对象(注入自身或AOP)。
- 异常处理规范 :事务方法避免"吞没"异常,统一使用运行时异常或配置
rollbackFor。 - 测试验证:编写单元测试和集成测试,验证事务行为是否符合预期。
- 代码规范:遵循团队代码规范,统一事务使用方式。
总结
Spring事务失效通常是由几个常见原因造成的:自调用、异常处理不当、方法权限问题、传播机制配置错误等。理解Spring事务的原理和这些失效场景,能够帮助我们在开发过程中避免陷阱,确保事务的正确性。在实际开发中,要确保事务方法正确配置,并在调用方法时遵循Spring AOP的代理机制 。 希望本文能帮助大家更好地理解和使用Spring事务,避免在生产环境中遇到事务失效的问题。如果你有其他关于Spring事务的问题或经验,欢迎在评论区分享。