⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7522686616356356122 「一只叫煤球的猫」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

引言

“只要你不考虑事务的问题,总有一天事务会来考虑你。”

忘记是哪位哲人说的这句话了,在最近的一次code review中,我逐渐体会到了这句话的分量。

当我看到同事在一个复杂的业务逻辑中混用了@Transactional注解和TransactionTemplate时,就预感到可能会有问题。

我问他为什么这样设计,他的回答让我有些意外:”反正都能实现事务,应该没什么区别吧?”

这个回答让我意识到,很多开发者对Spring的事务管理机制其实理解得并不深入。大家往往知道加个@Transactional就能开启事务,但对于什么时候该用声明式、什么时候该用编程式,以及它们背后的工作原理,却很少深究。

事务管理看似简单,实则暗藏玄机。

今天我们就来深入聊聊Spring中的三种事务管理方式,以及它们各自的适用场景和潜在的风险。

正文

三种方式,各有千秋

Spring给我们提供了三种处理事务的方式:@Transactional注解、TransactionTemplate和直接使用TransactionManager。

就像武侠小说里的三种兵器,每种都有自己的招式和适用场景。

@Transactional:最省心的

提到事务管理,基本上都会想到去用@Transactional。

确实,这玩意儿用起来简单粗暴,在方法上加一个注解就完事了。

@Service
public class UserService {

@Transactional
public void createUser(User user) {
userRepository.save(user);
// 如果这里抛异常,上面的操作会回滚
sendWelcomeEmail(user);
}
}

最大的优点就是无侵入。

业务代码依然干净,事务逻辑完全交给Spring在背后搞定了。但是,这种黑盒式的便利必然有代价。

Spring通过AOP来实现声明式事务,给方法外面包了一层代理,这就会导致一些让人头疼的问题:

内部方法调用失效:这是最容易踩的坑!当同一个类内部的方法调用带有@Transactional注解的方法时,事务不会生效。因为内部调用不会走代理。

@Service  
public class UserService {
public void batchCreate(List<User> users) {
for (User user : users) {
createUser(user); // ❌ 这里事务不会生效!
}
}

@Transactional
public void createUser(User user) {
userRepository.save(user);
}
}

只对public方法生效:私有方法或者protected方法上加@Transactional毫无卵用。

要匹配异常类型:默认只有RuntimeException和Error会触发回滚,检查型异常需要特殊配置。

可以看这篇文章:

https://juejin.cn/post/7496344493709885455

虽然这些都是老生常谈的问题了,但在实际开发中,还是有太多因为这种细节出bug的情况。

TransactionTemplate:可控的

TransactionTemplate算是声明式和编程式事务的一个平衡点。它既保持了一定的灵活性,又不会让代码变得过于复杂。

@Service
publicclass 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);
returnnull;
} catch (InsufficientFundsException e) {
// 可以根据业务逻辑决定是否回滚
status.setRollbackOnly();
throw e;
}
});
}
}

用TransactionTemplate的好处是你可以精确控制事务的边界,也不用担心方法调用的问题。

而且还可以在事务执行过程中获取到事务的状态信息,做点更细致的控制。

但这种方式也有些问题。

最明显的就是代码的可读性会变差一些,业务逻辑和事务逻辑会混在一起。

另外,如果业务逻辑比较复杂,嵌套层次深的话,代码也会变得不太好维护。

当然一般来说都会在额外封装一些方法以供业务侧来调用。

TransactionManager:最灵活的

如果想要完全的控制权,那就得直接用TransactionManager了。这种方式最灵活,但灵活的代价就是容易出错。

@Service
publicclass 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的话,可能会这样写:

@Service
publicclass 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());
}
}

⚠️ 上面的代码中,事务是不会生效的!

这是因为importUsers方法调用同一个类中的createUserWithTransaction方法,属于内部方法调用。Spring AOP通过代理实现事务,而同一个类内部的方法调用不会走代理,所以@Transactional注解完全失效。

通俗的做法是将有事务的方法抽取到另一个Service中:

@Service
publicclass UserImportService {

@Autowired
private UserTransactionService userTransactionService;

public void importUsers(List<User> users) {
for (User user : users) {
try {
userTransactionService.createUserWithTransaction(user); // ✅ 这样才会生效
} catch (Exception e) {
log.error("导入用户失败: {}", user.getUsername(), e);
}
}
}
}

@Service
publicclass UserTransactionService {

@Transactional
public void createUserWithTransaction(User user) {
userRepository.save(user);
userProfileRepository.save(user.getProfile());
}
}

这种方式的问题是需要额外创建一个Service类,增加了代码复杂度。

当然还有另一种方式使用Spring的 AopContext.currentProxy() 拿到当前代理对象再去调用,或者用 ApplicationContext 去从容器里获取代理对象,但始终觉得不太优雅。

这也是为什么有时候TransactionTemplate会是更好的选择。

用TransactionTemplate的话:

@Service
publicclass 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());
returnnull;
});
} catch (Exception e) {
log.error("导入用户失败: {}", user.getUsername(), e);
}
}
}
}

这种方式就很清晰,事务边界在代码中一目了然,而且不用担心方法调用的问题。

那TransactionManager呢?

写业务的时候基本不太会用到,所以咱也不多费口舌了。

踩过的坑

说到事务管理,我自己也踩过不少坑。

印象最深的一次是在处理一个报表统计的功能时,由于数据量比较大,我想着用只读事务来提高性能:

@Transactional(readOnly = true)
public List<ReportData> generateReport(ReportQuery query) {
// 复杂的查询逻辑
return reportRepository.findComplexData(query);
}

结果发现性能提升并不明显,后来才知道只读事务在某些数据库连接池配置下,效果并不理想。

另一个常见的问题是事务超时。

有些开发者喜欢把事务超时设置得很长,生怕业务逻辑执行时间过长导致事务回滚。但这样做的风险很明显,如果真出现了死锁或者其他问题,系统会长时间无响应。

// 这样设置必然是不太好的,极其不推荐,除非你明确知道自己在做什么
@Transactional(timeout = 300) // 5分钟超时
public void processLargeDataSet(List<Data> dataList) {
// 大量数据处理逻辑
}

更好的做法是,把大事务拆分成小事务,或者改写成批处理。

混合使用的问题

回到文章开头提到的那个同事的代码,混合使用不同的事务管理方式很容易出问题。

最常见的情况是事务传播行为的理解偏差。

@Service
publicclass OrderService {

@Autowired
private TransactionTemplate transactionTemplate;

@Transactional
public void processOrder(Order order) {
orderRepository.save(order);

// 这里又开了一个事务模板,传播行为可能和预期不一致
transactionTemplate.execute(status -> {
auditRepository.save(new OrderAudit(order));
returnnull;
});
}
}

这种混用的代码很蛋疼,很难理解事务的边界,调试起来也很烦。

所以在一个service类中尽量保持事务管理方式的一致性,没事少折腾。

结尾/总结

稍微总结一下。

对于新手,建议先把@Transactional的各种特性和坑点搞明白,基本能解决大部分问题。等到真正需要更精细控制的时候,再考虑其他方案。

对于有经验的开发者,在已经熟练掌握编程时事务控制的前提下,再多关注事务的性能影响。

事务不是万金油,过度使用或者不当使用都会带来不可预料的性能问题。

最重要的是,无论选择哪种方式,都要保证代码的可读性和可维护性。

技术是为业务服务的,而代码是要给整个团队一起维护的,不要为了炫技让代码变得晦涩难懂。

希望这篇文章能帮你理清楚这三种方式的区别和使用场景。

毕竟,在数据一致性面前,再小心都不为过。

文章目录
  1. 1. 引言
  2. 2. 正文
    1. 2.1. 三种方式,各有千秋
      1. 2.1.1. @Transactional:最省心的
      2. 2.1.2. TransactionTemplate:可控的
      3. 2.1.3. TransactionManager:最灵活的
    2. 2.2. 该选哪个?
    3. 2.3. 一个例子
    4. 2.4. 踩过的坑
    5. 2.5. 混合使用的问题
  3. 3. 结尾/总结