本地事务失效 :本以为在同一个事务内的操作,实际上并没有被一起提交或回滚。通常发生在分布式或复杂应用架构中。
核心原因:本地数据库事务(通常指单个数据库上连接的事务)的有效范围,无法覆盖到所有想要一起提交或回滚的操作。
本地事务失效的典型场景
1.跨多个数据源(多个数据库连接)
场景描述 :需要同时操作两个独立的数据库,例如DB1和DB2。
代码示例:
java
@Transactional
public void transferAcrossDatabase() {
// 操作一:从DB1的用户表扣钱
userMapper.deductMoney("userA", 100); // 使用DataSource1连接DB1
// 操作二:往DB2的账户表加钱
accountMapper.addMoney("userB", 100); // 使用DataSource2连接DB2
}
失效原因:
- 这两个Mapper通常使用不同的DataSource,即两个不同的数据库连接。
- @Transactional默认只能管理一个数据库连接上的事务。
- 当deductMoney执行成功后,它的连接会立即提交事务(如果后续代码没出错),而addMoney是在另一个连接上执行,属于另一个独立的事务。此时如果addMoney失败,deductMoney的操作并不会回滚。
2.跨服务方法调用(RPC/HTTP)
在微服务架构中,这是导致数据不一致的"头号杀手"。
场景描述 :在同一个服务内,一个事务方法调用了另一个服务的方法。
代码示例:
java
@Transactional
public void placeOrder() {
// 1. 本地数据库操作:创建订单
orderMapper.insert(order); // 本地事务管理
// 2. 远程服务调用:扣减库存
inventoryServiceClient.deductStock(productId, quantity); // HTTP或RPC调用
// 3. 后续本地代码
...
}
失效原因:
- 库存服务是另一个独立的进程,有自己独立的数据库。
- 本地事务只能回滚orderMapper.insert的操作。
- 如果deductStock调用失败,虽然可以抛出异常回滚导致本地订单创建,但如果deductStock调用成功,而后续本地代码出错,会导致订单回滚,但库存已经被扣减的"脏数据"。
3.非公共方法或方法内调用
这与Spring AOP(面向切面编程)的实现机制有关。
场景描述 :在同一个类中,一个方法调用另一个有@Transactional注解的方法。
代码示例:
java
public class OrderService {
public void createOrder() {
// ... 一些逻辑
this.insertOrder(); // 在类内部调用事务方法
}
@Transactional
public void insertOrder() {
orderMapper.insert(order);
// 其他数据库操作
}
}
失效原因:
- Spring的事务管理是通过AOP代理实现的。当从类外部调用insertOrder时,实际上调用的是Spring生成的代理对象的方法,代理对象会开启事务。
- 当在类内部(如createOrder中)调用this.insertOrder()时,这是目标对象自身的调用,绕过了代理,因此@Transactional注解不会生效。
4.异常被捕获或被"吃掉"
事务的回滚依赖于运行时异常(RuntimeException)和Error。如果异常处理方式不对,事务管理器就不知道需要回滚。
场景描述 :在事务方法中,你把可能抛出异常的地方用try-catch包起来,但没有在catch块中再次抛出异常。
代码示例:
java
@Transactional
public void updateUser() {
try {
userMapper.update(user); // 如果这里出错...
// ... 其他操作
} catch (Exception e) {
logger.error("更新用户失败", e);
// 只是记录了日志,没有抛出新异常!
// 事务管理器认为一切正常,会提交事务。
}
}
失效原因:事务管理器只有在接收到异常信号时才会触发回滚。吞掉异常,就等于告诉框架"一切正常,可以提交"。
5.错误的异常类型
默认情况下,Spring事务只对未检查的异常(即RuntimeException和Error)进行回滚。对已检查异常(如Exception,IOException,SQLException等)不回滚。
场景描述 :方法抛出了一个已检查异常。
代码示例:
java
@Transactional
public void updateUser() throws Exception { // 声明抛出已检查异常
userMapper.update(user);
if (someCondition) {
throw new Exception("一个业务异常"); // 抛出已检查异常
}
}
失效原因:即使抛出了异常,但因为它是Exception类型,Spring默认不会回滚事务。
6.手动切断了数据库连接
在一些特殊操作中,可能会手动提交或设置自动提交,干扰了Spring的事务管理。
场景描述 :在事务方法中,手动获取了连接并改变了其自动提交状态。
代码示例:
java
@Transactional
public void manualConnectionOperation() {
SomeData data = getFromDatabase();
// 手动获取连接并操作
Connection conn = DataSourceUtils.getConnection(dataSource);
conn.setAutoCommit(true); // 改为自动提交,破坏了事务
// ... 执行一些SQL,会立即提交
conn.setAutoCommit(false);
}
失效原因:Spring通过将autoCommit设置为false来管理事务。手动改为了true,会导致SQL语句立即提交,不受事务控制。
如果避免和解决?
- 对于场景1(多数据源):使用分布式事务解决方案,如基于XA协议的JTA(如Atomikos)、Seata等。
- 对于场景2(跨服务):采用最终一致性方案,如Saga模式、事务消息(RocketMQ)、TCC模式等。
- 对于场景3(方法内调用):
- 将事务方法移到另一个Bean中。
- 通过AopContext.currentProxy()获取代理对象再调用。
- 对于场景4和5(异常处理):
- 确保在catch块中抛出运行时异常,例如throw new RuntimeException(e);。
- 使用@Transactional(rollbackFor = Exception.class)注解,强制对所有Exception及其子类都进行回滚。
- 对于场景6(手动连接):避免在事务方法中手动操作连接和提交,交由Spring统一管理。
总结
理解本地事务失效的关键在于理解它的边界------它只能管住当前方法通过同一个数据库连接执行的SQL操作。一旦你的操作超出了这个边界(跨连接、跨进程、跨方法调用),本地事务就会失效。