同事混用@Transactional和TransactionTemplate被我怼了,三种事务管理到底怎么选?

🔥 同事混用@Transactional和TransactionTemplate被我怼了,三种事务管理到底怎么选?

三种方式,各有千秋

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会触发回滚,检查型异常需要特殊配置。

TransactionTemplate:可控的

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

@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了。这种方式最灵活,但灵活的代价就是容易出错。

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

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

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

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

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

@Service
public class 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
public class UserTransactionService {
    
    @Transactional
    public void createUserWithTransaction(User user) {
        userRepository.save(user);
        userProfileRepository.save(user.getProfile());
    }
}

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

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

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

TransactionTemplate的话:

@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呢?

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

踩过的坑

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

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

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

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

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

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

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

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

混合使用的问题

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

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

@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;
        });
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT枫斗者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值