1. 先搞清楚:TransactionTemplate 到底解决什么问题
1.1 从声明式事务的边界谈起
如果你平时用 Spring Boot,绝大多数事务场景都是用@Transactional注解解决的。把注解往方法上一放,Spring 的动态代理就会在方法执行前开启事务、方法执行后提交或回滚。这套机制确实方便,尤其是配合 Spring AOP 那套声明式能力,业务代码里几乎看不到事务相关的样板代码。
但是用了几年之后你会发现,注解式事务在一些边界场景里特别拧巴。最常见的就是同类内部调用问题:一个 Service 类里有方法 A 和方法 B,A 调用了 B,如果 B 上有@Transactional,B 是不会被 Spring 的 AOP 代理拦截的,因为这个调用发生在目标对象内部,没有经过代理对象。你在面试题里会看到“事务失效场景”这一类问题,说的就是它。
再比如批量处理场景,要求每 500 条提交一个批次,失败后只回滚当前批次,这种粒度用@Transactional很难优雅表达。你总不能把一个注解切到“每 N 条一轮”的方法上吧?要么就得把内部逻辑拆成一堆内部类或者自注入。这类问题用 TransactionTemplate 处理起来会自然得多。
1.2 TransactionTemplate 的核心价值
TransactionTemplate 是 Spring 提供的编程式事务管理模板类,核心逻辑就是你把一段业务代码包装成一个回调对象,交给它去执行,事务的开启、提交、回滚全部由模板统一完成。它和原生TransactionManager的编程式事务相比,省掉了大量try/catch和手工commit/rollback的重复代码。
它真正解决的是三个问题:一是事务边界的精确控制,你可以在方法内部任意位置控制事务的起点和终点,而不是只能跟着整个方法走;二是避免自调用导致的 AOP 代理失效,因为你用的是模板类,事务逻辑和代理无关;三是可以通过编程方式动态设置事务的隔离级别、传播行为、超时时间等属性,而不是像注解那样在写代码时就要把属性固定死。
所以我觉得 TransactionTemplate 不是一个用来替代@Transactional的东西,而是你在遇到边界场景时的补充工具。对于掌握 Spring 底层的同学来说,它也是理解 Spring 事务抽象的一把钥匙——你亲手调用过事务管理器,再去读TransactionInterceptor源码,很多概念一下子就通了。
2. TransactionTemplate 的设计与底层原理
2.1 类结构和方法入口
先看它的类结构。TransactionTemplate 实现了TransactionOperations接口,这个接口在 Spring 5 之后提供了一个默认的executeWithoutResult方法,这个点后面再细说。核心属性只有几个:TransactionManager(实际是PlatformTransactionManager)、一组TransactionDefinition属性,包括传播行为、隔离级别、超时时间、是否只读、事务名称。
public class TransactionTemplate extends DefaultTransactionDefinition implements TransactionOperations, InitializingBean { private PlatformTransactionManager transactionManager; @Override public <T> T execute(TransactionCallback<T> action) throws TransactionException { if (this.transactionManager == null) { throw new IllegalStateException("No PlatformTransactionManager set"); } TransactionStatus status = this.transactionManager.getTransaction(this); T result; try { result = action.doInTransaction(status); } catch (RuntimeException | Error ex) { rollbackOnException(status, ex); throw ex; } catch (Throwable ex) { rollbackOnException(status, ex); throw new UndeclaredThrowableException(ex); } this.transactionManager.commit(status); return result; } }这段代码是整个模板的核心骨架。你可能已经注意到几个信息量很大的点:
- 它继承的是
DefaultTransactionDefinition,这意味着 TransactionTemplate 本身就是一份“事务定义”,事务管理器在getTransaction时会把模板对象作为TransactionDefinition参数传入。 - 回调里抛出的
RuntimeException和Error会导致回滚;检查型异常(Throwable中非运行时异常部分)不会直接导致回滚,而是先做rollbackOnException判断,这个判断逻辑和@Transactional的 rollbackFor 规则是相通的。 - 任何异常抛出后,模板都会立刻把异常重新抛给调用方,而不是吞掉异常继续执行。这一点对业务补偿和事后告警非常重要。
2.2 事务属性是如何传递的
transactionManager.getTransaction(this)这行代码是理解 Spring 事务传播机制的关键。这里的this就是 TransactionTemplate 实例,它在继承DefaultTransactionDefinition时已经带了默认的传播行为PROPAGATION_REQUIRED和默认隔离级别ISOLATION_DEFAULT。事务管理器拿到这个 definition 之后,会结合当前线程中已经存在的事务上下文,决定是新建一个事务、挂起当前事务、还是直接参与当前事务。
以DataSourceTransactionManager为例,它内部会调用AbstractPlatformTransactionManager.getTransaction()。这个方法先根据 definition 判断传播行为:如果是PROPAGATION_REQUIRED且当前线程已经有激活的事务,就直接复用现有事务,并把事务状态里的newTransaction标记为 false;如果当前没有事务,才真正去数据源连接上发起一个事务。
这个设计解释了为什么内部嵌套调用 TransactionTemplate 时,默认情况下它们会共享同一个物理事务。你在嵌套回调里抛异常,外层也可能受影响,回滚范围取决于你设置的传播行为和异常有没有被捕获。这个在后面的高级用法里会专门讲。
2.3 为什么它天然规避了“自调用失效”
回到最常见的面试场景。@Transactional是基于 Spring AOP 动态代理实现的,它在TransactionAspectSupport.invokeWithinTransaction里完成事务开启、提交和回滚。自调用失效的原因很简单:this.methodB()调用根本没有经过代理对象,AOP 拦不到,invokeWithinTransaction也没机会执行。
TransactionTemplate 不存在这个问题。它的事务管理和业务对象是组合关系,而不是切面关系。你在业务代码里调用transactionTemplate.execute(...),模板内部直接向事务管理器要事务,跟外部有没有代理、代理拦不拦得到完全没有关系。所以在那些因为自调用而失效的场景里,你只需要把方法内部的逻辑挪进execute回调里,事务就立刻恢复正常了。
我接过不少排查事务失效的活儿,一半以上最后都落在自调用上。如果项目里暂时没法改造成内部代理调用(比如注入自身或者拆分 Service),用 TransactionTemplate 是最快的止血方案。
3. TransactionTemplate 的基础用法与配置
3.1 初始化 TransactionTemplate
在 Spring Boot 项目里,推荐用配置类创建一个 Bean,把事务管理器注入进去。
@Configuration public class TransactionConfig { @Bean public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { TransactionTemplate template = new TransactionTemplate(); template.setTransactionManager(transactionManager); // 可选:模板级默认事务属性 template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); template.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT); template.setTimeout(30); return template; } }然后你在业务 Service 里直接注入这个 Bean 就行。要注意如果项目里同时有多个PlatformTransactionManager(比如多数据源的分布式事务场景),一定要用限定的名称注入,不然 Spring 容器会因为找不到唯一的PlatformTransactionManager而启动报错。
也可以不注册成全局 Bean,而是在具体 Service 里局部 new 一个,然后通过setTransactionManager指定事务管理器。这种写法在单元测试和独立小工具类里很常见,缺点是每次都要配一遍属性。
这里我建议模板级属性只设置PROPAGATION_REQUIRED这种默认值。隔离级别、超时等更细粒度的属性尽量放到具体的execute调用前动态设置,否则全局 Template 的属性会影响所有用它开启事务的地方,很容易误伤。
3.2 execute 与 executeWithoutResult 两种执行方式
execute是需要返回值的场景。你可以在回调里执行查询或者写操作,然后把结果通过 return 返回给调用方。
public Order createOrder(OrderCreateDTO dto) { return transactionTemplate.execute(status -> { Order order = new Order(); order.setUserId(dto.getUserId()); order.setAmount(dto.getAmount()); orderMapper.insert(order); // 同一个事务内维护订单快照 orderSnapshotMapper.insert(buildSnapshot(order)); return order; }); }如果回调里的业务逻辑不关心返回值,用executeWithoutResult更贴切,代码意图也更明确。这个方法是 Spring 5 在TransactionOperations接口上提供的默认方法,底层还是走 execute,只是把返回类型封成了Void,你不需要自己 return null。
public void deductStock(Long skuId, Integer count) { transactionTemplate.executeWithoutResult(status -> { Stock stock = stockMapper.selectBySkuIdForUpdate(skuId); if (stock.getAvailableCount() < count) { throw new IllegalStateException("库存不足"); } stockMapper.deduct(skuId, count); stockLogMapper.insert(...); }); }无论哪种方式,核心都是“设置一个事务边界,把业务逻辑当回调放进边界里”。事务开启后,你在这个回调里执行的多个 DAO 操作共享同一个物理事务。
3.3 回调里的代码写法和状态使用
回调参数是TransactionStatus status,它有非常重要的作用。最常见的用途是判断当前事务是否是新建事务,或者是不是只读事务。不过在大多数场景中,我们用得最多的是它的setRollbackOnly()方法。
看这个例子:你需要先插入一条主数据,再插入若干条明细,如果某条明细不合法,你希望整个事务回滚,而不是跳过那条明细继续执行。
transactionTemplate.executeWithoutResult(status -> { billMapper.insert(mainBill); for (BillItem item : items) { if (!checkItem(item)) { status.setRollbackOnly(); return; } billItemMapper.insert(item); } });注意,这里不能依靠抛出异常来触发回滚,因为代码在循环里,你希望的是主动标记回滚但不抛出异常中断流程。setRollbackOnly做的事情就是把当前事务标记为“只能回滚”,即使后续代码没有抛异常,事务管理器在提交阶段看到这个标记也会执行回滚。
这个 API 也经常用在“校验失败但不想通过异常控制流”的场景,比如你在事务里做一步检查,检查不过就标记回滚,然后 return,让外层正常结束。相比抛自定义异常,这种方式对调用方更友好,不会污染异常栈,也避免了一些粗心的 catch 把异常吞掉导致事务不按预期回滚的问题。
4. 高级用法:传播行为、保存点与批量模式
4.1 用传播行为控制嵌套事务的真实边界
TransactionTemplate 和@Transactional一样,支持七种传播行为。但在编程式事务里,传播行为的意义更直接:你在回调里再次调用 transactionTemplate.execute,内层如何参与外层事务,完全由内层模板的传播行为决定。
PROPAGATION_REQUIRED是默认值。它表示当前有事务就加入,没有就新建。如果你在一个事务里调用另一个事务模板,两个会被合并成同一个物理事务,任何一个环节回滚,整体都回滚。
PROPAGATION_REQUIRES_NEW表示必须开启一个新事务,外层当前事务如果有的话会被挂起。内层提交成功后立即释放连接资源,外层后面再回滚不会影响内层已经提交的数据。这个特性适合记录“审计日志”这类不希望跟着业务回滚一起消失的场合,但代价是你要接受两段事务之间数据状态不一致的窗口,而且它会短暂占用另一个数据库连接,连接池不够大的时候要小心。
PROPAGATION_NESTED在底层是依赖数据库的保存点机制实现的。内层事务回滚时,只回滚到保存点位置,外层还能继续提交。MySQL 的 InnoDB 引擎是支持保存点的,这是很多“部分成功、部分失败”需求的标准解法。
下面的代码演示了三种传播行为的差异,理解了这段,你基本就掌握了嵌套事务的边界。
// 外层模板 transactionTemplate.execute(status -> { userMapper.insert(userA); try { // 内层模板,默认 REQUIRED nestedTransactionTemplate.execute(innerStatus -> { userMapper.insert(userB); throw new RuntimeException("B 失败"); }); } catch (RuntimeException e) { // 这里 catch 住异常后,外层还能继续 } userMapper.insert(userC); return "done"; });三个模板是否生效,取决于传播行为:
- 内层是
REQUIRED:B 和 A、C 在同一事务,RuntimeException 抛出后,整个事务被标记 rollback-only,即使外层 catch 住异常,最终提交也会报UnexpectedRollbackException。 - 内层是
REQUIRES_NEW:B 事务独立提交或回滚,A 和 C 提交成功,A、C 不受 B 影响。 - 内层是
NESTED:B 回滚到保存点,A 成功,C 成功。
这个表可以收藏:
| 传播行为 | 内层回滚影响 | 外层 catch 后能否继续提交 |
|---|---|---|
| REQUIRED | 整个外层同步被标记回滚 | 不能,最终抛 UnexpectedRollbackException |
| REQUIRES_NEW | 只回滚内层新事务 | 能,外层和已提交的内层互不影响 |
| NESTED | 只回滚到保存点 | 能,外层其余操作正常提交 |
4.2 用 TransactionStatus 操作保存点
Spring 的事务抽象里,保存点的 API 在ObjectTransactionManager和SavepointManager里面。TransactionStatus接口继承了SavepointManager,所以你可以直接在回调里建保存点、回滚到保存点、释放保存点。
transactionTemplate.executeWithoutResult(status -> { orderMapper.insert(order); Object savepoint = status.createSavepoint(); try { detailMapper.insert(detailBadRecord); // 模拟失败明细 } catch (DataAccessException e) { status.rollbackToSavepoint(savepoint); } // 这里继续写其他数据 logMapper.insert(log); });这种写法不像NESTED传播行为那样需要额外定义一个嵌套模板,适合在“同一个事务里,想让部分数据回滚但又不希望整体失败”的场景。使用前提是底层事务管理器支持保存点,JpaTransactionManager和DataSourceTransactionManager都支持,但要确认一下你用的数据库驱动和方言支不支持。
有一点要特别留意:保存点存在期间,事务的某些隔离性保证是会变化的。比如在 MySQL 默认的可重复读隔离级别下,如果你在保存点之前已经做了某些查询,回滚到保存点后,之前读到的行版本信息不一定完全恢复原样。绝大多数业务不会碰到这种边界,但如果你做的事务逻辑特别敏感,建议先做一次小规模实验验证行为,别直接上生产。
4.3 批量导入/分块提交的落地写法
这是 TransactionTemplate 最值得讲的场景之一。批量数据处理时,经常要求一批数据全部成功则整体提交,若数据中有脏数据则跳过或记录,几十万条数据又不能开成一个超大事务,否则锁范围太大、回滚段膨胀得厉害。结合 REQUIRES_NEW 或者每次重新获取一个 Template,就能实现“分块提交”。
我写过一个比较通用的模板:
public void batchImport(List<Item> items, int batchSize) { for (int i = 0; i < items.size(); i += batchSize) { List<Item> batch = items.subList(i, Math.min(i + batchSize, items.size())); boolean success = false; try { transactionTemplate.executeWithoutResult(status -> { for (Item item : batch) { insertItem(item); } }); success = true; } catch (RuntimeException e) { // 第 i 批次失败,记录错误,继续后面批次 log.error("batch {} import failed, error: {}", i / batchSize, e.getMessage()); } if (success) { log.info("batch {} imported", i / batchSize); } } }这一段唯一要小心的是subList返回的是原列表的视图,不是副本。如果你在循环里对原列表做了 remove 操作,subList会抛ConcurrentModificationException。稳妥做法是new ArrayList<>(items.subList(...)),不过只是读取的话问题不大。
业务对这种“一批失败继续下一批”的容忍度通常是有前提的,你要和产品确认清楚失败批次是否需要精确计数和补偿。我建议每次失败都把批次索引和异常信息写入一张任务表,后面由一个带@Scheduled的重试任务扫描这张表继续处理,这样整个导入流程会可控很多。
4.4 线程池与事务的注意事项
先说结论:Spring 的事务上下文是绑定在线程上的,默认情况下你在主线程开启事务,然后往线程池里提交任务,子线程里执行的数据库操作不会自动加入主线程的事务。很多人第一次在这个场景踩坑,都是因为误以为和局部线程变量一样父子线程共享。
所以,如果你有“事务内异步处理”的需求,要明确告诉自己是分裂成了多个独立事务。最简单的处理方式是把子线程要执行的逻辑单独包装成一个事务模板,或者用TransactionTemplate配合每个任务的异常处理。
public void processInParallel(List<Item> items) { items.parallelStream().forEach(item -> { transactionTemplate.executeWithoutResult(status -> { itemMapper.update(item); }); }); }这里有个容易忽略的性能问题:并发度很高时,每个线程都会向连接池申请一个连接,如果连接池最大连接数是 20,而 items 有 1000 个,那么大量的线程其实是在排队等待获取连接。你看到的现象是 CPU 占用不高但任务完成很慢,此时排查的方向应该是数据库连接池的活跃连接数。
也要注意连接隔离级别,如果你在主线程里通过某个连接执行了select ... for update,异步子线程拿到的连接可能不是同一个,所以行锁依然会阻塞子线程,但事务隔离却互不相通。要做到真正的“分布式事务+异步”,还是需要考虑本地消息表、事务消息或者引入分布式事务框架,靠 TransactionTemplate 是解决不了的。
5. 与 @Transactional 混用时的那些坑
5.1 事务属性被覆盖或合并
混用不是不行,但事务模板的默认属性会和注解属性合并,而不是一方完全覆盖另一方。最经典的问题是超时时间:外层方法加了@Transactional(timeout = 5),内层模板没设置超时,模板继承的是DefaultTransactionDefinition默认的 -1,即不超时;但当前线程已经存在一个事务,Spring 在applyTransactionTimeout时会用外层事务的 timeout 重置 JDBC 连接的查询超时。如果你显式设置了模板的超时时间,它的单位是秒,事务管理器取的是当前线程事务已经消耗的时间和模板设置值之间的剩余时间。
所以不要以为“内层模板没有配置超时就不会超时”,只要外层异常语句或模板设置了超时,整个事务就会按超时机制执行。这种隐式关联经常在排查问题时把人绕晕,表面上是内层模板超时了,实际罪魁祸首是外层注解。
还有只读标记。@Transactional(readOnly = true)的事务,内层 TransactionTemplate 即使不做任何设置,也会继承当前事务的只读状态。如果你在只读事务里执行了 INSERT,最终会在 flush 阶段报错。反过来,外层没设置只读,内层模板设置了只读,也只影响当前事务的某些数据库连接设置,不一定真正阻断写操作,这个和具体数据库方言有关,不要过度依赖它做写保护。
5.2 异常被吞导致提交
这个坑在我做培训答疑时反复出现。代码长这样:
@Transactional public void process(Order order) { orderMapper.update(order); try { transactionTemplate.executeWithoutResult(status -> { auditMapper.insert(order); }); } catch (Exception e) { log.warn("audit insert failed", e); } }外层是@Transactional,内层默认传播行为是REQUIRED。所以外层和内层共用一个物理事务。内层抛异常时,Spring 并不会把事务状态恢复原样,而是会把当前事务标记为rollback-only。你在内层把异常 catch 了,外层方法正常返回,但提交时事务管理器发现已经被标记 rollback-only,于是抛出UnexpectedRollbackException。
这个异常常常出现在外层方法之后的某个 AOP 拦截位置,很多人根本看不到自己代码里的 catch 已经吞掉了异常,直到测试环境发现“无论如何都写入失败”或者“莫名抛 UnexpectedRollbackException”。
解决思路就两种:一是内层用REQUIRES_NEW,让内层事务独立,不会把外层标记成 rollback-only;二是别吞掉异常,让它一直抛到外层事务边界。如果你想“内层失败但不影响外层”,就老老实实用新的物理事务,别指望同一个事务里有选择性地提交部分数据,那不是事务的语义。
5.3 经典报错速查
| 报错信息 | 可能原因 | 处理建议 |
|---|---|---|
| UnexpectedRollbackException | 事务内某处被标记 rollback-only,但异常被吞 | 检查内层事务异常是否被捕获,或调整传播行为 |
| Transaction is already completed | 代码里手动调用了 commit/rollback | 不要手动操作事务状态,让模板统一管理 |
| Transaction synchronization is not active | 当前线程没有事务上下文就调用了需要事务的操作 | 检查是否绕过了模板直接操作了 JDBC,或看是否在事务提交后异步访问了本地资源 |
| IllegalStateException: No PlatformTransactionManager set | TransactionTemplate 没有注入事务管理器 | 配置 Bean 时调用setTransactionManager |
| PreparedStatementCallback: Connection is read-only | 事务或连接被标记只读 | 检查外层事务或者数据源配置的 readOnly 属性 |
还有一个不太起眼但很折腾的消息:CannotAcquireLockException。它不是 TransactionTemplate 直接抛的,而是事务里执行for update时锁等待超时。很多人因为事务里存在外部 HTTP 调用导致连接长时间不释放,锁等待时间超长,最后数据库报警。这个不是模板的问题,而是在事务里做了重活导致的,要小心。
5.4 排查事务执行状态的手段
快速确认一个事务到底有没有开启、是不是新建事务,最好的办法是在回调里直接打印状态:
transactionTemplate.executeWithoutResult(status -> { System.out.println("isNewTransaction: " + status.isNewTransaction()); System.out.println("hasSavepoint: " + status.hasSavepoint()); System.out.println("isRollbackOnly: " + status.isRollbackOnly()); });isNewTransaction为 true 说明当前事务是由这个模板新建的,为 false 说明它是加入了一个已经存在的事务。这个信息在排查嵌套事务时特别有价值。
也可以借助日志查看 Spring 的事务同步机制。如果你用的是DataSourceTransactionManager,把日志级别调到 DEBUG,查看org.springframework.jdbc.datasource.DataSourceTransactionManager和org.springframework.transaction.support.AbstractPlatformTransactionManager的输出。日志会显示类似Creating new transaction with name [...]: PROPAGATION_REQUIRED, ISOLATION_DEFAULT或Participating in existing transaction这样的关键行。看到 Participating 你就要清楚,这个模板是在加入一个已有事务,而不是新起了一个。
除此之外,在编码时注意观察调用栈深度。如果同一次请求里事务模板的execute被调用了很多层,相互嵌套,要留意事务上下文在线程里的传递状态。用TransactionSynchronizationManager.getCurrentTransactionName()这个方法可以直接在代码里拿到当前事务的名称,通常是外层方法全限定名,这也是排查哪个事务在起作用的快捷方式。
6. 针对不同规模项目的使用建议
6.1 什么时候应该优先选 TransactionTemplate
我总结出一个比较实用的判断标准:当某个事务代码段会被多次复用时,事务模板更适合。你可以在一个 Service 里定义多个不同的事务方法,各自设置不同的事务属性和边界。而@Transactional更适合那种方法级需求稳定不变、没有嵌套控制的场景。
比如做订单超时关单逻辑,扫描一批超时订单,逐条进行关单操作,单条失败不能影响整个批次,这种逻辑用 TransactionTemplate 分块提交比在方法上加一个大事务注解稳健得多。再比如你有两条写入操作,第一条是业务主数据,第二条是发送 MQ 消息的事务表,必须一起成功或回滚,这种对原子性敏感但又不想把整个方法拖入事务的场景,也建议用模板控制到最精确的代码块。
反过来说,如果你只是一个简单的 CRUD 操作、更新一张表,没有复杂的嵌套和批量控制,直接用@Transactional完全没问题,没必要为了用模板而用模板,代码反而多了一层回调。
6.2 事务内避免做的事
以我这么多年排查事务问题的经验,下面这几类操作千万别放进事务里,无论你用注解还是模板:
第一,外部网络调用。事务和 RPC 调用的时间边界绑在一起,数据库连接会被你无谓地占住,一旦第三方接口响应慢,连接池会被耗尽,跟着数据库连接数暴涨。这种问题往往不是代码逻辑错,而是资源被活活拖垮的。
第二,耗时的文件读写和批量计算。如果你事务里做一个大文件的解析再写入数据库,这个事务的执行时间会特别长,数据库层面会积累大量行锁和 undo 日志,影响其他正常事务。
第三,消息发送,尤其是同步发送。Maven 消费者如果正好在高峰期,发送超时会导致事务一直挂着,最终回滚或者导致连接泄漏。事务消息的正确姿势是把消息体写入本地消息表,和业务表在同一个事务里提交,再由独立任务异步投递。
6.3 配合 AOP 做事务模板的统一增强
如果不想在每个 Service 里重复注入模板,可以自己定义一个注解 + AOP 切面,在目标方法执行前后分别调用事务模板,实现“自定义的声明式事务”。这个思路很适合老项目改造成本高、又希望统一事务逻辑的场景。
简单示意:定义一个@BizTransaction注解,切面内部拿到方法上的注解配置,用事务模板包一层调用。本质上你是在 AOP 内部手动调用TransactionTemplate.execute,这样既保留了注解的直观,又绕开了@Transactional对 self-invocation 的限制,还可以自己扩展一些业务属性,比如事务名称、重试次数。
这种方案用起来非常顺手,我帮一个同事改造过一套老代码。原来事务失效的根源就是方法自调用,没法定点给内层方法动刀,因为改动范围太大。用这个 AOP + TransactionTemplate 的组合,只加注解就把事务边界控制住了,成本比把整个 Service 重构掉低了一个量级。如果你也有同样的历史包袱,值得考虑。
6.4 事务名称与监控的配合
TransactionTemplate 可以通过setName设置事务名称,这个名字会出现在日志里,也会出现在TransactionSynchronizationManager.getCurrentTransactionName()的返回里。很多项目为了监控事务执行时间,会结合 Spring Boot Actuator 或者 Micrometer 自定义一个Timer指标,把事务名称作为一个标签维度。这样你在监控看板上就能直接看到某个事务模板的平均耗时、P99 耗时、成功失败次数,比大海捞针地去翻日志高效得多。
我之前维护的一个批处理系统中,就是把每个批次的事务模板设置了不同的名称,然后在事务回调外面套了一层监控记录:
TransactionTemplate template = new TransactionTemplate(transactionManager); template.setName("batch.order.close"); long start = System.currentTimeMillis(); try { template.executeWithoutResult(status -> { ... }); } finally { monitor.record("batch.order.close", System.currentTimeMillis() - start); }线上整套链路跑起来之后,通过监控平台查耗时,哪一步慢、哪一步失败率高一目了然。事务命名这个操作很不起眼,但它是区分事务边界、配合排查和生产监控的重要元数据,不要忽略。
回到开头的问题,TransactionTemplate 不是 Spring 事务体系里花哨的部分,但它是最接近底层事务管理器的开发接口之一。我个人在实际项目里使用下来,最大的感受是:它让事务控制从“注解的声明”变成了“代码的逻辑”,边界变得可以编程、可以动态调整、可以方便地和批处理、异步任务、补偿机制结合。如果你还没认真用过它,建议下次遇到事务边界必须精确控制、自调用导致事务失效、或者批量任务需要分段提交这几个场景时,第一时间想起它。试一次,你就会发现这比在注解和 AOP 的坑里反复试探要省心得多。