1. Spring事务的正确用法解析
作为一名Java后端开发者,我经常看到团队里的小伙伴在使用Spring事务时踩坑。Spring事务看似简单,一个@Transactional注解就能搞定,但实际使用中隐藏着不少"陷阱"。今天我就结合自己多年踩坑经验,详细剖析Spring事务的正确使用姿势。
Spring事务本质上是通过AOP代理实现的,它会在方法调用前后自动开启和提交/回滚事务。这种声明式事务管理极大简化了开发工作,但同时也带来了一些理解上的门槛。理解事务的传播机制、隔离级别、回滚规则等核心概念,是避免踩坑的关键。
2. 事务不回滚的五大经典场景
2.1 错误的传播特性配置
传播特性(Propagation)决定了事务如何在不同方法间传递。Spring提供了7种传播行为:
- REQUIRED(默认):如果当前存在事务,则加入该事务;如果不存在,则新建一个事务
- SUPPORTS:如果当前存在事务,则加入该事务;否则以非事务方式执行
- MANDATORY:必须在一个已有事务中执行,否则抛出异常
- REQUIRES_NEW:无论当前是否存在事务,都新建一个事务
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务则挂起
- NEVER:以非事务方式执行,如果当前存在事务则抛出异常
- NESTED:如果当前存在事务,则在嵌套事务内执行
最常见的错误是将传播特性设置为NEVER或NOT_SUPPORTED,却期望方法在事务中执行。例如:
java复制@Transactional(propagation = Propagation.NEVER)
public void updateOrder(Order order) {
// 此方法永远不会在事务中执行
orderRepository.update(order);
}
实际经验:在大多数业务场景下,使用默认的REQUIRED传播特性即可满足需求。只有在需要特殊事务边界控制时,才考虑使用其他传播特性。
2.2 异常被捕获未抛出
这是新手最容易犯的错误之一。事务回滚依赖于异常抛出,如果在方法内捕获了异常但没有重新抛出,事务管理器就无法感知异常,自然不会回滚。
java复制@Transactional
public void processOrder(Order order) {
try {
orderService.validate(order);
inventoryService.reduceStock(order);
paymentService.charge(order);
} catch (Exception e) {
log.error("处理订单失败", e); // 仅记录日志,未抛出异常
// 事务不会回滚!
}
}
解决方案有两种:
- 不捕获异常,让异常自然抛出
- 捕获异常后,重新抛出RuntimeException或Error
java复制@Transactional
public void processOrder(Order order) {
try {
// 业务逻辑
} catch (Exception e) {
log.error("处理订单失败", e);
throw new RuntimeException("处理订单失败", e); // 重新抛出
}
}
2.3 抛出了错误的异常类型
Spring事务默认只对RuntimeException和Error进行回滚,对检查型异常(Exception)不会回滚。这是一个容易忽视的细节。
java复制@Transactional
public void updateUser(User user) throws Exception {
try {
userRepository.update(user);
} catch (DuplicateKeyException e) {
throw new Exception("用户已存在"); // 抛出检查型异常,事务不会回滚
}
}
解决方案:
- 抛出RuntimeException或其子类
- 通过rollbackFor指定需要回滚的异常类型
java复制@Transactional(rollbackFor = Exception.class)
public void updateUser(User user) throws Exception {
// 现在任何异常都会导致回滚
}
2.4 自定义回滚异常配置不当
虽然可以通过rollbackFor自定义回滚异常,但如果配置不当,反而会导致问题。例如:
java复制@Transactional(rollbackFor = BusinessException.class)
public void transfer(Account from, Account to, BigDecimal amount) {
accountService.debit(from, amount); // 可能抛出SQLException
accountService.credit(to, amount); // 可能抛出SQLException
}
如果这里抛出SQLException,由于不是BusinessException,事务不会回滚。建议将rollbackFor设置为Exception.class或Throwable.class,确保所有异常都能触发回滚。
2.5 嵌套事务回滚过多
使用NESTED传播特性时需要注意,内层事务的回滚不会影响外层事务,但外层事务的回滚会导致所有内层事务回滚。
java复制@Transactional
public void outerMethod() {
// 操作1
innerMethod(); // 内层事务
// 操作2
}
@Transactional(propagation = Propagation.NESTED)
public void innerMethod() {
// 内层操作
}
如果innerMethod回滚,outerMethod可以继续执行并提交(除了innerMethod的操作)。但如果outerMethod回滚,innerMethod的操作也会被回滚。
3. 事务不生效的七大原因
3.1 访问权限问题
Spring事务是通过代理实现的,如果方法是private的,代理将无法拦截方法调用,导致事务失效。
java复制@Transactional
private void internalUpdate() { // 事务不会生效
// 业务逻辑
}
解决方法:确保事务方法至少是protected级别的。
3.2 方法被final修饰
final方法无法被代理,自然也无法应用事务。
java复制@Transactional
public final void updateFinal() { // 事务不会生效
// 业务逻辑
}
解决方法:避免在事务方法上使用final修饰符。
3.3 方法内部调用
这是最常见的陷阱之一。当在一个类的方法内部调用另一个事务方法时,事务不会生效。
java复制@Service
public class OrderService {
@Transactional
public void placeOrder(Order order) {
validateOrder(order); // 内部调用,事务不生效
processPayment(order);
}
@Transactional
public void validateOrder(Order order) {
// 验证逻辑
}
}
解决方法有三种:
- 将方法拆分到不同Service中
- 通过AopContext获取代理对象
- 在类中注入自身(不推荐)
java复制@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自身
public void placeOrder(Order order) {
self.validateOrder(order); // 通过代理调用
processPayment(order);
}
@Transactional
public void validateOrder(Order order) {
// 验证逻辑
}
}
3.4 未被Spring管理
如果类没有交给Spring容器管理(缺少@Service、@Component等注解),其中的@Transactional自然也不会生效。
java复制public class ExternalService { // 没有Spring注解
@Transactional
public void externalMethod() {
// 事务不会生效
}
}
解决方法:确保类被适当的Spring注解标记。
3.5 多线程调用
事务是与线程绑定的,如果在方法内启动新线程执行数据库操作,这些操作将不在原事务中。
java复制@Transactional
public void multiThreadUpdate() {
new Thread(() -> {
userRepository.update(...); // 不在事务中
}).start();
}
解决方法:避免在事务方法中使用多线程操作数据库,或考虑使用分布式事务。
3.6 表不支持事务
使用MySQL时,如果表使用MyISAM引擎,它不支持事务。只有InnoDB引擎才支持事务。
解决方法:确保表使用InnoDB引擎。
3.7 未开启事务
虽然少见,但如果忘记在配置类上添加@EnableTransactionManagement,事务功能将不会启用。
java复制@Configuration
// 缺少@EnableTransactionManagement
public class AppConfig {
// 配置
}
解决方法:确保主配置类上有@EnableTransactionManagement注解。
4. 其他事务相关问题
4.1 大事务问题
大事务是指执行时间过长、涉及数据过多的事务。大事务会带来诸多问题:
- 占用数据库连接时间长,影响系统吞吐量
- 容易造成死锁
- 回滚代价高
典型的大事务反模式:
java复制@Transactional
public void batchProcess(List<Order> orders) {
for (Order order : orders) { // 处理1000个订单
validate(order);
process(order);
sendNotification(order);
}
}
优化方案:
- 拆分大事务为多个小事务
- 移除事务中的非必要操作(如远程调用、IO操作)
- 使用编程式事务精确控制事务边界
4.2 编程式事务
除了声明式事务(@Transactional),Spring还提供了编程式事务管理,通过TransactionTemplate可以更灵活地控制事务。
java复制@Service
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate;
public void complexOperation() {
transactionTemplate.execute(status -> {
// 业务逻辑
return result;
});
}
}
编程式事务适用于:
- 需要精细控制事务边界的情况
- 需要根据条件决定是否提交/回滚的情况
- 需要手动设置事务超时等属性的情况
5. 事务最佳实践总结
- 合理设置传播特性,大多数情况下使用默认的REQUIRED即可
- 正确配置rollbackFor,建议设置为Exception.class
- 避免在事务方法中处理无关逻辑,特别是远程调用和IO操作
- 注意事务方法的访问权限,不能是private或final
- 警惕自调用导致的事务失效问题
- 对于批处理操作,考虑拆分大事务为多个小事务
- 在需要精细控制时,考虑使用编程式事务
我在实际项目中最深刻的教训是:永远不要假设事务会如你预期的那样工作。在关键业务逻辑中,务必通过单元测试和集成测试验证事务行为。特别是在涉及资金、库存等敏感操作时,一个未回滚的事务可能导致严重的业务问题。