引言
"只要你不考虑事务的问题,总有一天事务会来考虑你。"
忘记是哪位哲人说的这句话了,在最近的一次code review中,我逐渐体会到了这句话的分量。
当我看到同事在一个复杂的业务逻辑中混用了@Transactional
注解和TransactionTemplate
时,就预感到可能会有问题。
我问他为什么这样设计,他的回答让我有些意外:"反正都能实现事务,应该没什么区别吧?"
这个回答让我意识到,很多开发者对Spring的事务管理机制其实理解得并不深入。大家往往知道加个@Transactional
就能开启事务,但对于什么时候该用声明式、什么时候该用编程式,以及它们背后的工作原理,却很少深究。
事务管理看似简单,实则暗藏玄机。
今天我们就来深入聊聊Spring中的三种事务管理方式,以及它们各自的适用场景和潜在的风险。

正文
三种方式,各有千秋
Spring给我们提供了三种处理事务的方式:@Transactional
注解、TransactionTemplate
和直接使用TransactionManager
。
就像武侠小说里的三种兵器,每种都有自己的招式和适用场景。
@Transactional:最省心的
提到事务管理,基本上都会想到去用@Transactional
。
确实,这玩意儿用起来简单粗暴,在方法上加一个注解就完事了。
java
@Service
public class UserService {
@Transactional
public void createUser(User user) {
userRepository.save(user);
// 如果这里抛异常,上面的操作会回滚
sendWelcomeEmail(user);
}
}
最大的优点就是无侵入。
业务代码依然干净,事务逻辑完全交给Spring在背后搞定了。但是,这种黑盒式的便利必然有代价。
Spring通过AOP来实现声明式事务,给方法外面包了一层代理,这就会导致一些让人头疼的问题:
内部方法调用失效:这个坑踩过的人应该不少。
只对public方法生效 :私有方法或者protected方法上加@Transactional
毫无卵用。
要匹配异常类型:默认只有RuntimeException和Error会触发回滚,检查型异常需要特殊配置。
可以看这篇文章 juejin.cn/post/749634...
虽然这些都是老生常谈的问题了,但在实际开发中,还是有太多因为这种细节出bug的情况。
TransactionTemplate:可控的
TransactionTemplate
算是声明式和编程式事务的一个平衡点。它既保持了一定的灵活性,又不会让代码变得过于复杂。
java
@Service
public class AccountService {
@Autowired
private TransactionTemplate transactionTemplate;
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
transactionTemplate.execute(status -> {
try {
accountRepository.debit(fromAccount, amount);
accountRepository.credit(toAccount, amount);
return null;
} catch (InsufficientFundsException e) {
// 可以根据业务逻辑决定是否回滚
status.setRollbackOnly();
throw e;
}
});
}
}
用TransactionTemplate
的好处是你可以精确控制事务的边界,也不用担心方法调用的问题。
而且还可以在事务执行过程中获取到事务的状态信息,做点更细致的控制。
但这种方式也有些问题。
最明显的就是代码的可读性会变差一些,业务逻辑和事务逻辑会混在一起。
另外,如果业务逻辑比较复杂,嵌套层次深的话,代码也会变得不太好维护。
当然一般来说都会在额外封装一些方法以供业务侧来调用。
TransactionManager:最灵活的
如果想要完全的控制权,那就得直接用TransactionManager
了。这种方式最灵活,但灵活的代价就是容易出错。
java
@Service
public class PaymentService {
@Autowired
private PlatformTransactionManager transactionManager;
public void processPayment(Payment payment) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
paymentRepository.save(payment);
notificationService.sendPaymentConfirmation(payment);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
}
这种方式的好处是可以精确控制事务的每一个细节,比如隔离级别、传播行为、超时时间等等。
但是代码量会增加很多,而且容易出错。
一旦忘记提交或者回滚,那迟早会有数据不一致的惊喜。
该选哪个?
经过上面的介绍,到底该怎么选择呢?
我的建议是这样的:
大部分情况下,@Transactional就够用了。它简单、直接,能解决90%的场景。除非你遇到了它解决不了的问题,否则没必要搞复杂。
需要精细控制时,考虑TransactionTemplate 。比如你需要在事务执行过程中根据某些条件决定是否回滚,或者需要在一个方法里管理多个事务,这时候就需要用上TransactionTemplate
了。
极少数情况下才直接用TransactionManager。通常是在写框架代码或者有非常特殊的需求时才会用到。
一个例子
让我举个实际的例子来说明这三种方式的差异。
假设我们要实现一个批量导入用户的功能,要求是:如果某个用户导入失败,不应该影响其他用户的导入。
用@Transactional
的话,可以这样写:
java
@Service
public class UserImportService {
public void importUsers(List<User> users) {
for (User user : users) {
try {
createUserWithTransaction(user);
} catch (Exception e) {
log.error("导入用户失败: {}", user.getUsername(), e);
}
}
}
@Transactional
public void createUserWithTransaction(User user) {
userRepository.save(user);
userProfileRepository.save(user.getProfile());
}
}
这种方式的问题是,你需要单独抽出一个方法来保证事务边界。如果忘记了,或者因为某些原因不能抽方法,事务就不会生效。
用TransactionTemplate
的话:
java
@Service
public class UserImportService {
@Autowired
private TransactionTemplate transactionTemplate;
public void importUsers(List<User> users) {
for (User user : users) {
try {
transactionTemplate.execute(status -> {
userRepository.save(user);
userProfileRepository.save(user.getProfile());
return null;
});
} catch (Exception e) {
log.error("导入用户失败: {}", user.getUsername(), e);
}
}
}
}
这种方式就很清晰,事务边界在代码中一目了然,而且不用担心方法调用的问题。
那TransactionManager
呢?
写业务的时候基本不太会用到,所以咱也不多费口舌了。
踩过的坑
说到事务管理,我自己也踩过不少坑。
印象最深的一次是在处理一个报表统计的功能时,由于数据量比较大,我想着用只读事务来提高性能:
java
@Transactional(readOnly = true)
public List<ReportData> generateReport(ReportQuery query) {
// 复杂的查询逻辑
return reportRepository.findComplexData(query);
}
结果发现性能提升并不明显,后来才知道只读事务在某些数据库连接池配置下,效果并不理想。
另一个常见的问题是事务超时。
有些开发者喜欢把事务超时设置得很长,生怕业务逻辑执行时间过长导致事务回滚。但这样做的风险很明显,如果真出现了死锁或者其他问题,系统会长时间无响应。
java
// 这样设置必然是不太好的,极其不推荐,除非你明确知道自己在做什么
@Transactional(timeout = 300) // 5分钟超时
public void processLargeDataSet(List<Data> dataList) {
// 大量数据处理逻辑
}
更好的做法是,把大事务拆分成小事务,或者改写成批处理。
混合使用的问题
回到文章开头提到的那个同事的代码,混合使用不同的事务管理方式很容易出问题。
最常见的情况是事务传播行为的理解偏差。
java
@Service
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate;
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
// 这里又开了一个事务模板,传播行为可能和预期不一致
transactionTemplate.execute(status -> {
auditRepository.save(new OrderAudit(order));
return null;
});
}
}
这种混用的代码很蛋疼,很难理解事务的边界,调试起来也很烦。
所以在一个service类中尽量保持事务管理方式的一致性,没事少折腾。
结尾/总结
稍微总结一下。
对于新手,建议先把@Transactional
的各种特性和坑点搞明白,基本能解决大部分问题。等到真正需要更精细控制的时候,再考虑其他方案。
对于有经验的开发者,在已经熟练掌握编程时事务控制的前提下,再多关注事务的性能影响。
事务不是万金油,过度使用或者不当使用都会带来不可预料的性能问题。
最重要的是,无论选择哪种方式,都要保证代码的可读性和可维护性。
技术是为业务服务的,而代码是要给整个团队一起维护的,不要为了炫技让代码变得晦涩难懂。
希望这篇文章能帮你理清楚这三种方式的区别和使用场景。
毕竟,在数据一致性面前,再小心都不为过。
