在企业级Java应用开发中,Spring事务管理就像给关键操作系上的安全带。但实际开发中经常遇到安全带"看似系上实则失效"的情况,这种"伪事务"状态往往在出现数据异常时才会被发现。最近在金融支付系统重构中,我们就因为一个@Transactional注解失效导致了对账不平,最终排查发现是同类事务问题中的第4种场景。
事务失效的本质是Spring的AOP代理机制未能正确拦截目标方法。理解这个原理后,所有失效场景都可以归结为:代理对象没有按照预期执行事务增强逻辑。下面通过6个典型场景的代码示例,带你看透事务失效的真相。
java复制@Service
public class OrderService {
public void createOrder(OrderDTO dto) {
// 其他业务逻辑
this.updateInventory(dto); // 这里的事务注解不会生效
}
@Transactional
public void updateInventory(OrderDTO dto) {
// 库存更新操作
}
}
当通过this调用内部方法时,实际上绕过了Spring生成的代理对象,直接调用了原始Bean的方法。这就好比用私家车接送客户却走了公交专用道——既没有公交车的特权(事务),还可能被处罚(数据不一致)。
java复制@Service
public class OrderService {
@Autowired
private InventoryService inventoryService;
public void createOrder(OrderDTO dto) {
inventoryService.updateInventory(dto);
}
}
@Service
public class InventoryService {
@Transactional
public void updateInventory(OrderDTO dto) {
// 事务生效
}
}
java复制@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
// 启动类配置
}
@Service
public class OrderService {
public void createOrder(OrderDTO dto) {
((OrderService)AopContext.currentProxy()).updateInventory(dto);
}
}
关键选择:方案1符合单一职责原则但会增加类数量,方案2保持代码内聚但需要特殊配置。金融系统推荐方案1,因为资金操作需要更清晰的职责划分。
java复制@Transactional
public void processPayment(Payment payment) {
try {
paymentDao.update(payment);
if(payment.getAmount() > 10000) {
throw new RiskControlException("金额超限");
}
} catch (Exception e) {
log.error("支付异常", e); // 事务不会回滚!
}
}
这里捕获了RuntimeException却没有重新抛出,就像消防员接到火警却只记录不出动——异常被"吞掉"导致事务管理器无法感知需要回滚。
java复制@Transactional(rollbackFor = Exception.class)
public void processPayment(Payment payment) throws RiskControlException {
try {
paymentDao.update(payment);
riskControlCheck(payment);
} catch (RiskControlException e) {
log.warn("风控拦截:{}", e.getMessage());
throw e; // 显式重新抛出
}
}
// 或者指定回滚异常类型
@Transactional(rollbackFor = {RiskControlException.class, SQLException.class})
事务异常处理黄金法则:
java复制@Transactional(propagation = Propagation.REQUIRED)
public void batchProcess(List<Order> orders) {
orders.forEach(order -> {
try {
orderService.processSingle(order); // 内部是REQUIRES_NEW
} catch (Exception e) {
// 即使单个失败也不影响整体
}
});
}
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processSingle(Order order) {
// 独立事务处理
}
}
这个电商订单批量处理案例中,外层REQUIRED和内层REQUIRES_NEW的搭配看似合理,但存在两个隐患:
| 传播行为 | 适用场景 | 连接占用 | 异常影响 |
|---|---|---|---|
| REQUIRED(默认) | 普通增删改 | 1个 | 内外层一起回滚 |
| REQUIRES_NEW | 日志记录、异步通知 | N个 | 内层独立回滚 |
| NESTED | 复杂业务子流程 | 1个(保存点) | 外层可选择性回滚 |
| NOT_SUPPORTED | 非事务操作 | 0个 | 挂起当前事务 |
实战建议:
sql复制-- 建表语句指定了不支持事务的引擎
CREATE TABLE `account` (
`id` bigint NOT NULL AUTO_INCREMENT,
`balance` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM;
即使Spring配置了事务,在MyISAM表上的操作依然会立即生效。这就像用记事本记录银行交易——没有撤销功能。
检查清单:
java复制@Transactional
public void transfer(Account from, Account to, BigDecimal amount) {
accountDao.lockAccount(from.getId()); // SELECT FOR UPDATE
// 长时间业务处理...
accountDao.updateBalance(from.getId(), amount.negate());
accountDao.updateBalance(to.getId(), amount);
}
当这个方法并发执行时,可能出现:
优化方案:
java复制@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 100))
public void transferWithRetry() {
// 业务逻辑
}
当自定义切面与事务切面共存时:
java复制@Aspect
@Order(1) // 优先级高于事务切面(默认Ordered.LOWEST_PRECEDENCE)
public class LogAspect {
@Around("@annotation(com.xxx.BusinessLog)")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
// 如果此处异常被捕获,事务切面将无法感知
return pjp.proceed();
}
}
切面就像安检流程——如果日志切面先执行并处理了异常,后面的事务"安检员"就看不到危险品了。
正确配置:
java复制@EnableTransactionManagement(order = Ordered.HIGHEST_PRECEDENCE)
public class ApplicationConfig {
// 保证事务切面最先执行
}
java复制public interface OrderService {
@Transactional
void cancelOrder(Long orderId);
}
@Service
public class OrderServiceImpl implements OrderService {
// 需要重新声明注解
@Override
@Transactional
public void cancelOrder(Long orderId) {
// 实现
}
}
接口上的注解就像贴在玻璃门上的告示——实现类这侧的"行人"可能看不见。Spring默认使用CGLIB代理时,接口注解不会被继承。
最佳实践:
java复制@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = false)
public class Application {}
java复制@SpringBootTest
public class TransactionTest {
@Autowired
private PaymentService paymentService;
@Test
void testTransactionRollback() {
assertThrows(RiskControlException.class, () -> {
paymentService.processPayment(createTestPayment(20000));
});
// 验证数据是否回滚
assertNull(paymentDao.findByOrderNo("test123"));
}
}
java复制// 在PlatformTransactionManager实现类打条件断点
if (transactionDefinition.getName().contains("updateInventory")) {
// 观察事务行为
}
properties复制logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
java复制@ArchTest
public static final ArchRule transactionalMethodsMustHaveRollbackConfig =
methods()
.that().areAnnotatedWith(Transactional.class)
.should().bePublic()
.andShould().notBeAnnotatedWith(Transactional.class)
.andShould(notDeclareExceptionOfType(RuntimeException.class));
最后分享我们团队使用的事务健康检查表,每次代码评审必查:
在微服务架构下,还要特别注意分布式事务与本地事务的边界划分。曾经就遇到过Seata全局事务和@Transactional本地事务混用导致的提交冲突,这个我们下次再专门探讨。