Spring之如何选择事务

Spring之如何选择事务

因为博主之前的实习经历中有使用编程式事务的经历,因为当时是同事给的方案,所以并没有深究为什么不用编程式事务,最近刚好在面试,有面试官问到:为什么不使用声明式事务?因为现在基本很多场景下我们都是直接使用@Transactional注解直接开启事务,所以很少会去使用编程式事务。

所以今天我们就借助这个机会好好来总结一下应该如何选择。

Spring官方文档中对事务的描述如下:

翻译文档链接:传送门

Spring解决了全局和局部事务的弊端。它让应用开发者在任何环境下都能使用一个一致的编程模型。你只需写一次代码,它就可以在不同的环境中受益于不同的事务管理策略。Spring框架同时提供声明式编程式事务管理。大多数用户更喜欢声明式事务管理,我们在大多数情况下都推荐这样做

通过编程式事务管理,开发人员使用Spring框架的事务抽象,它可以在任何底层事务基础设施上运行。通过首选的声明式模型,开发人员通常很少或不写与事务管理有关的代码,因此,不依赖于Spring框架的事务API或任何其他事务API。

下面我们就带大家来学习一下什么是声明式事务和编程式事务。

1. 编程式事务

Spring框架提供了两种编程式事务管理的手段,通过使用:

  • TransactionTemplateTransactionalOperator.
  • 直接实现一个 TransactionManager

Spring团队通常推荐 TransactionTemplate 用于强制性流程中的编程式事务管理, TransactionalOperator 用于响应式代码。第二种方法类似于使用JTA的 UserTransaction API,尽管异常处理没有那么麻烦。

代码示例如下:

public class SimpleService implements Service {

// single TransactionTemplate shared amongst all methods in this instance
private final TransactionTemplate transactionTemplate;

// use constructor-injection to supply the PlatformTransactionManager
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}

public Object someServiceMethod() {
return transactionTemplate.execute(new TransactionCallback() {
// the code in this method runs in a transactional context
public Object doInTransaction(TransactionStatus status) {
updateOperation1();
return resultOfUpdateOperation2();
}
});
}
}

可以看到,编程式事务在代码中硬编码(不推荐使用) : 通过 TransactionTemplate或者 TransactionManager 手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。

2. 声明式事务

Spring框架的声明式事务管理是通过Spring面向切面编程(AOP)实现的。然而,由于事务方面的代码是随Spring框架的发布而来,并且可以以模板的方式使用,所以一般不需要理解AOP的概念来有效地使用这些代码。

@Transactional 注解来注解你的类,将 @EnableTransactionManagement 添加到你的配置中。基于注解的方法。直接在Java源代码中声明事务语义,使声明更接近于受影响的代码。没有太多的过度耦合的危险,因为无论如何,要以事务方式使用的代码几乎总是以这种方式部署。

代码示例:

// the service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {

@Override
public Foo getFoo(String fooName) {
// ...
}

@Override
public Foo getFoo(String fooName, String barName) {
// ...
}

@Override
public void insertFoo(Foo foo) {
// ...
}

@Override
public void updateFoo(Foo foo) {
// ...
}
}

(1)优点

  • 代码简洁,使用方便:从上面的示例代码中可以看到,声明式事务帮我们节省了很多的编码,会自动帮助我们进行事务的开启、提交以及回滚等操作;
  • 对代码没有侵入性:声明式事务管理是基于AOP的方式来实现的,本质就是在目标方法执行前后进行拦截,在目标方法执行前加入或者创建一个事务,在执行方法后,根据实际情况选择提交或者回滚事务。

(2)问题

  • 颗粒度问题:声明式事务有一个局限,就是最小粒度要作用在方法上。也就是说,如果想要给一部分代码添加事务,那就必须这部分代码独立拉出来作为一个方法。
  • 配置问题:因为声明式事务是通过注解的,有些时候还可以通过配置实现,这就会导致一个问题,那就是这个事务有可能被开发者忽略

首先,如果开发者没有注意到一个方法是被事务嵌套的,那么就可能会再方法中加入一些如RPC远程调用、消息发送、缓存更新、文件写入等操作。

我们知道,这些操作如果被包在事务中,有两个问题:

1、这些操作自身是无法回滚的,这就会导致数据的不一致。可能RPC调用成功了,但是本地事务回滚了,可是PRC调用无法回滚了。

2、在事务中有远程调用,就会拉长整个事务。那么久会导致本事务的数据库连接一直被占用,那么如果类似操作过多,就会导致数据库连接池耗尽。

  • 事务失效:声明式事务虽然看上去帮我们简化了很多代码,但是有一些场景会使得声明式事务失效,这也是一个比较常见的八股文了,如下:

Spring框架提供了非常方便的事务管理机制,但是在某些情况下,Spring事务会失效。

(1)方法非public。如果一个方法不是public修饰的,那么Spring无法代理这个方法,Spring官方文档中有明确的说明,@Transactional只有在修饰public方法时才会生效。修改pretected、private或者保内可见的方法均不会生效。

(2)事务方法被final、static关键字修饰。事务方法被final修改会防止子类重写该方法,从而无法进行动态代理,事务方法被static修改会使得该方法属于类而不是对象,因此也无法进行动态代理。

(3)方法自调用。当一个类中的一个方法调用同一个类中的另一个@Transactional方法时,这种情况被称为方法自调用,在这种情况下,事务控制将无法正常工作。因为事务代理对象在外部调用时才会被创建,如果一个方法在同一个类中调用另一个方法,实际上是在当前实例中进行调用,而不是通过代理对象调用。

(4)异常被捕获。Spring默认只对未被捕获的异常进行回滚,如果异常被捕获并处理了,那么Spring无法感知到异常的存在,也就无法进行回滚,因此,如果需要事务回滚,必须确保异常未被捕获并没有被处理。

(5)没有被Spring管理。只有在Spring容器中管理的Bean上的@Transactional注解才会生效,如把@Service注解注释掉,这个类就不会被加载成为一个Bean,在该对象的方法上添加@Transactional注解将不会有任何效果。

(6)数据库不支持事务。Spring事务是通过底层的数据库事务实现的。如果底层数据库不支持事务,那么Spring自然无法实现事务管理,如果需要使用事务,选择支持事务的数据库引擎,如MySQL的InnoDB引擎。

3. 如何选择

关于如何选择事务的使用方式,我们可以从@Transactional失效的场景来考虑,如果考虑到后续可能无法避免事务失效的问题,那么我建议还是最好使用编程式事务,因为硬编码其实简单粗暴,不管三七二十一,强制让事务执行。

其实关于@Transactional的用法,Java手册中也有对应的规范如下:

image-20240315194901646