实战:spring项目中不推荐用自带的声明式事务@Transactional

67 篇文章 0 订阅
17 篇文章 0 订阅

概叙

        事务、隔离级别、数据库并发问题,参考:科普文:数据库事务、隔离级别和并发问题(MySQL)-CSDN博客

        备注:搞不懂 csdn的审核机制,这么久了还没有审核通过,上面的链接等着再看。

        read uncommited:是最低的事务隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。
        read commited:保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。
        repeatable read:这种事务隔离级别可以防止脏读,不可重复读。但是可能会出现幻象读。它除了保证一个事务不能被另外一个事务读取未提交的数据之外还避免了以下情况产生(不可重复读)。
        serializable:这是花费最高代价但最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读之外,还避免了幻象读。


        脏读、不可重复读、幻象读概念说明:

Spring事务:

        Spring事务默认使用数据库的隔离级别,当然也可以通过@Transactional中的isolation参数调整当前Session级的隔离级别(数据库的隔离级别个人感觉粒度比较粗,Spring的隔离级别好一些 ,因为可以定义到方法上)。

        solation的参数有以下五种:

(1)solation.DEFAULT:为数据源的默认隔离级别

(2)isolation=Isolation.READ_UNCOMMITTED:未授权读取级别

(3)iIsolation.READ_COMMITTED:授权读取级别

(4)iIsolation.REPEATABLE_READ:可重复读取级别

(5)iIsolation.SERIALIZABLE:序列化级别

spring事务传播特性:

        事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring通过@Transactional(propagation = Propagation.REQUIRES_NEW)的propagation参数配置支持7种事务传播行为:

propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。
propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作

        Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。假设 ServiveX#methodX() 都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。

Spring 声明式事务@Transactionsl不生效的场景整理

        声明式事务管理是基于AOP实现的,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式来管理事务,只需在配置文件中做相关的事务规则声明或通过注解方式,就可以将事务规则应用到业务逻辑中。

        Spring的声明式事务管理在底层是建立在AOP的基础之上的。其管理事务的主要是通过Spring AOP和IOC容器实现的。其中AOP是面向切面编程,可以让我们在不修改源代码的基础上对已有方法进行增强处理。而IOC容器是Spring的核心,其负责管理所有的Java对象,包括事务管理器、数据源、业务逻辑对象等等。

1.访问权限问题

        所谓的访问权限问题也就是开发中再熟悉不过的privatedefaultprotectedpublic,它们的访问权限从左到右,依次变大。如果我们在开发过程中国呢,把某些事务方法定义了错误的访问权限,就会导致事务功能出现问题,甚至失效。例如:

@Service
public class UserService {
 @Transactionsl
 private void add(User user){
  saveUser(user);
  updateUser(user);
 }
}

        上面代码中我们可以看到对于方法add的访问修饰符被定义成了private,这样会导致事务失效,原因是Spring 要求被代理的方法必须是 **public** 的。简单粗暴来看源码是怎么搞的。如下:


 /**
  * Same signature as {@link #getTransactionAttribute}, but doesn't cache the result.
  * {@link #getTransactionAttribute} is effectively a caching decorator for this method.
  * <p>As of 4.1.8, this method can be overridden.
  * @since 4.1.8
  * @see #getTransactionAttribute
  */
 @Nullable
 protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
  // Don't allow non-public methods, as configured.
  if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
   return null;
  }

  // The method may be on an interface, but we need attributes from the target class.
  // If the target class is null, the method will be unchanged.
  Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

  // First try is the method in the target class.
  TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
  if (txAttr != null) {
   return txAttr;
  }

  // Second try is the transaction attribute on the target class.
  txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
  if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
   return txAttr;
  }

  if (specificMethod != method) {
   // Fallback is to look at the original method.
   txAttr = findTransactionAttribute(method);
   if (txAttr != null) {
    return txAttr;
   }
   // Last fallback is the class of the original method.
   txAttr = findTransactionAttribute(method.getDeclaringClass());
   if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
    return txAttr;
   }
  }

  return null;
 }

        从上述源码中可以看到AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有一个判断,如果方法的修饰符不是public的话,则返回null,并且不支持事务。

        也就是说如果我们自定义的事务方法(即目标方法)它的访问权限不是public,而是privatedefalutprotected修饰符的话,Spring都不会提供事务。

2.方法使用final修饰

在某些场景我们可能需要使用final修饰方法,为了不让子类重写等原因,但是针对普通方法而言这是没有任何问题的,但是针对需要加事务的方法则会导致事务失效。如下代码:

@Service
public class UserService {
 @Transactionsl
 private final void add(User user){
  saveUser(user);
  updateUser(user);
 }
}

上述代码中的add方法被final修饰了,从而导致了事务失效。具体原因是什么的。这就要从Spring的源码开始说起了。相比应该是都知道Spring事务的底层其实是使用了AOP,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。

但是某个方法被final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能

注意:如果某个方法是static修饰的,同样无法通过动态代理,变成事务方法。

2.1方法用static修饰

@Transactional
    public static boolean save(User user, UserService userService) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

失效原因: 原因和final一样

解决方案: 1、方法不要用static修饰

3.方法内部调用

在某些场景下,我们需要在某个service类中的某个方法中调用另外一个事务方法。比如:

@Service
public class UserService {
 @Autowired
 private UserMapper usermapper;

 public void add(User user){
  userMappper.insertUser(user);
  updateStatus(user);
 }
}

@Transactional
public void updateStatus(User user){
 doSomeThing();
}

从上面的方法中我们可以看到add()方法中直接调用了事务方法updateStatus();从前面的介绍可以直达,updateStatus()方法拥有事务的能力是因为Spring AOP生成了代理对象,但是这种方法直接调用了this对象的方法,所以updateStatus()方法不会生成事务。

由此可见,在同一类中的方法直接调用,会导致事务失效。

那么我们如何解决在同一方法中调用自己类中的另外一个方法呢?方案如下

3.1新加一个Service方法

这个方法相对简单就是将同一类中调用与被调用的两个方法拆分为两个Service。代码如下:

@Service
public class ServiceA {
 @Autowired
 private ServiceB serviceB;

 public void save(User user){
  queryData1();
  queryData2();
  serviceB.doSave(user);
 }
}

@Service
public class ServiceB{
 @Transactional(rollbackFor = Exception.class)
 public void doSave(User user) {
  addData1(user);
  updateData2(user):
 }
}


3.2在该Service中注入自己

如果不想加一个新的类,其实也可以通过在该类中注入自己也可解决事务失效的问题。代码如下:

@Service
public class ServiceA(){

 @Autowired
 private ServiceA serviceA;

 public void save(User user){
  queryData1();
  queryData2();
  serviceA.doSave(user);
 }

 @Transactional(rollbackFor = Excetion.class)
 public void doSave(User user){
  addData1();
  updateData2(user);
 }

}

可能有人看到这里便会有这样一个疑问,这种做法不会导致循环依赖的问题吗:答案是:不会。

3.3 通过AopContent类

在该Service类中使用AopContent.currentProxy()获取对象。虽然上述的方法2也是解决了该问题,但是代码看起来晦涩难懂。接下来我们可以通过AopContent来获取代理对象从而实现相同的功能,代码如下:

@Service
public class ServiceA{

 public void save(User user){
  queryData1();
  queryData2();
  ( (ServiceA)AopContent.currentProxy() ).doSave(user);
 }


 @Transactional(rollbackFor = Excetion.class)
 public void doSave(User user){
  addData1(user);
  updateData2(user);
 }
}

3.4 调用本类方法

public boolean save(User user) {
        return this.saveUser(user);
    }

    @Transactional
    public boolean saveUser(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

失效原因: 本类方法不经过代理,无法进行增强

解决方案: 1、注入自己来调用; 2、使用@EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy()

4.未被Spring管理

在我们市场开发中,还有一个细节很容易被忽略。就是如果需要使用Spring事务,是有一个前置条件,那就是对象需要交给Spring进行管理,需要创建bean实例。

通常情况下,我们通过@Contrlller @Service @Component @Repository等注解,实现将bean实例化喝依赖注入的功能。假设某个时候你再Service上没有添加@Service注解,比如

//@Service
public class UserService{

 @Transactional
 public void add(User user){
  saveData(user);
  updateData(user);
 }
}

上述代码的UserService类没有添加@Service注解,那么该类就不会交给Spring进行统一管理,同时它的add()方法也不会生成事务。

5.多线程调用

在实际的应用开发中。我们通常使用多线程的场景还是很多的。如果Spring事务用在多线程场景下。同样会有问题。代码如下:

@Slf4j
@Service
public class UserService{
 @Autowired
 private UserMapper userMapper;
 @Autowired
 private RoleService roleService;

 @Transactional
 public void add(User user) throws Exception{
  userMapper.insertUser(user);
  new Thread( ()-> {
   roleService.doOtherThing();
  }).start();
 }
}


@Service
public class RoleService{

 @Transactional
 public void doOtherThing(){
  System.out.println("保存roles数据");
 }

}


@Transactional(rollbackFor = Exception.class)
    public boolean save(User user) throws ExecutionException, InterruptedException {

        Future<Boolean> future = executorService.submit(() -> {
            boolean isSuccess = userService.save(user);
            try {
                int i = 1 % 0;
            } catch (Exception e) {
                throw new Exception();
            }
            return isSuccess;
        });
        return future.get();


    }
失效原因: 因为spring的事务是通过数据库连接来实现,而数据库连接spring是放在threadLocal里面。同一个事务,只能用同一个数据库连接。而多线程场景下,拿到的数据库连接是不一样的,即是属于不同事务

从上面的例子,我们可以看到事务方法add()中,调用了事务方法doOtherThing(),但是事务方法doOtherThing()是在另外一个线程中调用的。这样会导致两个方法不在一个线程中。获取的数据库连接也就不一致,从而是两个不同的事务。如果doOtherThing()方法中抛出了异常,add()方法是不可能回滚的。

如果看过Spring源码的小伙伴应该知道Spring的事务是通过数据库的连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。

private static final ThreadLocal<Map<Object,Object>> resources = new NamedThreadLocal<>("Transactional resources");

我们说的同一个事务,其实指同一个数据库连接,只有拥有同一个数据库连接才能同事提交和回滚。如果在不同的线程中,拿到的数据库连接肯定是不一样的。所以事务也是不同的。

6.表不支持事务

众所周知,在MySQL 5.x之前,默认的数据库引擎是myisam

它的优缺点就不说了:索引文件和数据文件是分开存储的,对于查多写少的表操作,性能要比InnoDB要更好。在一些老的项目中用它的有很多。

创建一个MyIsam引擎的表:

CREATE TABLE `category` (
 `id` bigint NOT NULL AUTO_INCREMENT,
 `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
 `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
 `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
 `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin


虽然MyIsam引擎好用,但是有一个致命的缺点,那就是不支持事务。如果是单表操作还好,不会出现太大的问题,但是如果是跨表操作,由于其不支持事务,数据极有可能出现不完整的情况。

此外MyIsam还不支持 行锁外键

所以时间的业务场景中,MyIsam的使用场景不多,在MySQL 5.x之后,MyIsam已经逐渐的退出了历史舞台,取而代之的是引擎InnoDB

所以在实际的开发中如果事务没有生效,有可能就是因为你的表的引擎不支持事务。

7.未开启事务

有些时候,事务没有生效的根本原因是没有开启事务。

如果你使用的是Spring Boot项目,那么很幸运,因为Spring Boot已经通过DataSoureTransactionManagerAutoConfiguration类,默认开启了事务,你只需要配置spring.datasource的相关参数即可。

如果是Spring项目则需要一下配置信息:

<!-- 配置事务管理器 --> 
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 
    <property name="dataSource" ref="dataSource"></property> 
</bean> 
<tx:advice id="advice" transaction-manager="transactionManager"> 
    <tx:attributes> 
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes> 
</tx:advice> 
<!-- 用切点把事务切进去 --> 
<aop:config> 
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> 
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 
</aop:config> 

注意:如果在pointcut标签中的切入点匹配规则,配错了的话,有些类的事务也不会生效。

以上说的都是单纯的事务没有生效。但是在实际的开发过程中还存在另外一种情况,就是事务生效了,但是没有回滚,或者说事务执行没有达到预期。

8.Spring中事务未生效的场景之事务未回滚

Spring的事务不回滚

8.1.错误的传播特性

说到事务的传播特性,首先应该知道事务的传播特性有哪些:

事务的传播行为类型

说明

PROPAGATION_REQUIRED

如果当前没有事务,就新建一个事务,如果已经存在一个事务,加入到这个事务中,这是最常见的选择。

PROPAGATION_SUPPORTS

支持当前事务,如果当前没有事务,则以非事务的方式运行

PROPAGATION_MANDATORY

使用当前事务,如果当前没有事务,就抛出异常

PROPAGATION_REQUIRED_NEW

新建事务,如果当前存在事务,把当前事务挂起

PROPAGATION_NOT_SUPPORTED

以非事务的方式执行操作,如果当前存在事务,则把事务挂起

PROPAGATION_NEVER

以非事务的方式执行,如果当前存在事务,则抛出异常

PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,执行与PROPAGATION_REQUIRED类似的操作

如果在编写代码时将事务的传播特性编写出错。比如:

@Service
public class UserService {

 @Transactional(propagation = Propagation.NEVER)
 public void doSave(User user){
  saveData(user);
  updateData(user);
 }
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
    public boolean save(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }
失效原因: 使用的传播特性不支持事务

我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。

目前只有三种事务传播特性才会新建事务REQUIRED REQUIRED_NEW NESTED

8.2.自己吞了异常

在开发过程中,有可能我们在事务中使用了try{}catch()了异常。比如:


@Slf4j
@Service
public class UserService {

 @Transactional
 public void add(User user){
  try{
   saveData(user);
   updateData(user);
  } catch (Exception e){
   log.error(e.getMessage(),e);
  }
 }

}

如果你的代码也是按照上述代码编写的,那么Spring事务是不会回滚的,因为开发者自己捕获了异常,同时没有手动抛出,欢聚还说就是自己把异常吞掉了。如果想要Spring能够正常回滚,则必须要抛出它能够处理的异常,如果没有抛出异常,Spring则会认为程序是正常的。

8.3.手动抛出了别的异常

即使开发者在编写过程中,没有手动抛出异常;但是如果出现的异常不正确,Spring事务也不会回滚。

@Slf4j
@Service
public class UserService{

 @Transactional
 public void add(User user) throws Exception {
  try {
   saveData(user);
   updateData(user);
  } catch(Exception e) {
   log.error(e.getMessage(), e);
   throw new Exception(e);
  }
 }

}

上述这种情况,开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。因为Spring事务,默认情况下只会回滚RunTimeException,和Error(错误),对于普通的Exception(非运行时异常),它是不会回滚的。

8.4.自定义了回滚异常

在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:

@Slf4j
@Service
public class UserService {
    
    @Transactional(rollbackFor = BusinessException.class)
    public void add(User user) throws Exception {
       saveData(user);
       updateData(user);
    }
}


如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。

即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。

rollbackFor默认值为UncheckedException,包括了RuntimeException和Error. 当我们直接使用@Transactional不指定rollbackFor时,Exception及其子类都不会触发回滚。

所以,建议一般情况下,将该参数设置成:Exception或Throwable。

8.5.嵌套事务回滚多了
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel user) throws Exception {
        userMapper.insertUser(user);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}


这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing()方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。但事实是,insertUser也回滚了。

这是为什么呢?

因为doOtherThing()方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

如何才能只回滚保存点呢?

代码语言:javascript

复制

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(User user) throws Exception {

        userMapper.insertUser(userModel);
        try {
            roleService.doOtherThing();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}


可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

好了以上就是整理的Spring事务在开发过程中会出现的诡异的情况。

Spring 推荐使用编程式事务

        声明式事务管理利用@Transactional注解简化了事务管理的复杂性,免去了手动编写事务管理代码的繁琐。但是,当事务函数内部需要捕获异常时,由于@Transactional的默认行为是在遇到运行时异常时回滚,如果异常被捕获并未再次抛出,事务可能无法正确回滚,这是其主要的缺点。还有一个原因是,很多时候需要用多线程来提升效率,而声明式事务在多线程的支持上是天生不足。

        编程式事务管理要求开发者手动编写事务管理的代码,尽管这增加了开发的工作量,但它为开发者提供了更大的灵活性。例如,开发者可以根据特定的业务逻辑,在事务中的任意位置进行提交或回滚操作。此外,当某些特定的异常发生时,编程式事务管理允许开发者精确地决定是提交还是回滚事务,这种控制能力是编程式事务管理的明显优势。

编程式事务管理

        编程式事务管理是通过编程代码手动管理事务,包括开启事务、提交事务、回滚事务等。Spring的编程式事务管理可以通过PlatformTransactionManager接口和TransactionDefinition接口来实现。我们可以通过实现PlatformTransactionManager接口来自定义我们的事务管理器,也可以通过TransactionTemplate类来简化编程式事务管理的代码。

        Spring框架提供了两种编程式事务管理的方法:

1)使用TransactionTemplate

2)使用TransactionManager

        Spring官方推荐使用TransactionTemplate来进行编程式事务管理,给出的理由是TransactionManager尽管减少了部分编码,但和使用JTA API的区别本质不大。通过对这两种方式的研究比较,作者也推荐使用TransactionTemplate的方式。

代码演示

接下来通过代码进行演示,假设你在Spring的配置文件中配置事务管理器,你使用的是基于JDBC的数据源,配置可能如下:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">  
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>  
    <property name="url" value="jdbc:mysql://localhost:3306/test"/>  
    <property name="username" value="root"/>  
    <property name="password" value="password"/>  
</bean>  
  
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
    <property name="dataSource" ref="dataSource"/>  
</bean>  
  
<tx:annotation-driven transaction-manager="transactionManager"/>

首先是,声明式事务管理,在你的业务逻辑类中使用@Transactional注解来声明需要事务管理的方法。例如:

import org.springframework.transaction.annotation.Transactional;  
import org.springframework.stereotype.Service;  
  
@Service  
public class UserService {  
    private final UserRepository userRepository;  
  
    @Autowired  
    public UserService(UserRepository userRepository) {  
        this.userRepository = userRepository;  
    }  
  
    @Transactional  
    public void createUser(User user) {  
        userRepository.save(user);  
        // ... 其他业务逻辑,如发送通知等  
    }  
}

在上述代码中,createUser方法上的@Transactional注解告诉Spring该方法需要在事务中执行。Spring将负责在方法开始执行时开启事务,并在方法执行完毕时提交或回滚事务。这种方式无需手动编写事务管理代码,而是通过注解来声明事务规则,实现了声明式事务管理。

接着,是编程式事务管理,编程式事务管理需要手动编写代码来管理事务。以下是一个使用Spring的TransactionTemplate类进行编程式事务管理的示例:

import org.springframework.transaction.TransactionStatus;  
import org.springframework.transaction.support.TransactionCallbackWithoutResult;  
import org.springframework.transaction.support.TransactionTemplate;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
  
@Service  
public class UserService {  
    private final TransactionTemplate transactionTemplate;  
    private final UserRepository userRepository;  
  
    @Autowired  
    public UserService(TransactionTemplate transactionTemplate, UserRepository userRepository) {  
        this.transactionTemplate = transactionTemplate;  
        this.userRepository = userRepository;  
    }  
  
    public void createUser(User user) {  
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {  
            @Override  
            protected void doInTransactionWithoutResult(TransactionStatus status) {  
                try {  
                    userRepository.save(user);  
                    // 其他业务逻辑  
                    status.setRollbackOnly(); // 设置回滚事务,如果需要的话  
                } catch (Exception e) {  
                    status.setRollbackOnly(); // 发生异常时回滚事务  
                    throw e; // 重新抛出异常,由上层处理  
                }  
            }  
        });  
    }  
}

在上述代码中,createUser方法使用TransactionTemplate类来执行需要在事务中运行的代码。通过调用transactionTemplate.execute方法,传入一个实现TransactionCallbackWithoutResult接口的匿名类,在匿名类的doInTransactionWithoutResult方法中编写需要在事务中执行的代码。这种方式需要手动管理事务的开启、提交和回滚,实现了编程式事务管理。这种方式更加灵活,可以根据具体需求进行更精细的事务控制。

2. 使用背景

        大多数情况下,我们在业务场景中可以使用Spring提供的@Transactional注解来把事务的管理交给框架来处理,框架通过AOP的一个Aspect来创建和维护事务,帮我们把事务管理和业务逻辑分开。大部分的编程也是这么做的,那为什么还需要编程式事务管理呢,下面是实际开发中遇到的一个问题。

2.1 问题分析

        电子商务网站调用第三方支付接口,根据支付信息更新系统订单状态,一个方法中混合了两种不同类型的I/O操作,比如下面的代码:

    @Transactional
    public void payment(){
        // 保存请求记录到本地数据库
        savePaymentRequest();
        // 调用第三方支付接口
        callWechatPaymentAPI();
        // 更新本地数据库支付状态
        updatePaymentState();
    }

        上面的代码块在一个事务中执行,看起来运行正常,先保存支付信息到本地数据库,然后调用第三方支付接口支付,根据支付结果更新本地支付状态。但是如果支付接口超时,或者网络延迟,调用接口时间很长,这个事务会一直持有当前数据库的连接,如果接口调用频繁,数据库连接池的连接很快就会被完全占用,后来的请求会因为请求不到数据库连接而失败。

        在实际的编程中,我们尽量把调用外部API的请求和需要本地事务的方法分开来执行,如果不得不在一起,就需要使用编程式事务来手动控制事务。

2.2 @Transactional 失效

        除了上述业务场景中@Transactional注解带来的问题外,该注解在以下几种场景下也会失效,编程式需要注意:

1)@Transactional注解在非public的方式上;

    @Transactional
    void transactionalMethod(){
        // 非public修饰的方法,事务不生效
    }

        @Transactional的事务管理是通过代理实现的,Spring在启动的时候会扫描有该注解的方法,框架对非public的方法未实现代理。

2)在类的内部调用类内的用@Transactional标注的方法

@Service
public class PaymentService {
    public void doBizLogic(){
        // 调用类内部使用@Transactional标注的方法,事务无效
        updateBizData();
    }
    
    @Transactional
    public void updateBizData(){
        // 更新数据操作
    }
}

        类内部的方法不通过代理调用,而是通过类的this调用,调用的是自身,代理方法未被调用。

3)事务方法内的异常被catch之后未抛出新的异常

    @Transactional
    public void updateBizData(){
        // 异常被捕捉到之后未抛出新的异常,事务失效
        try{
            // 更新数据
        } catch (Exception e){
            e.printStackTrace();
        }
    }

代理方法看到的是无异常,所以不会回滚事务。

3. TransactionTemplate事务管理

        TransactionTemplate和Spring的其它template很类似(比如JdbcTemplate, RestTemplate),提供了一些基于Callback的API方法,使用TransactionTemplate需要先注入一个PlatformTransactionManager

如下面的示例所示,使用 TransactionTemplate或多或少的会把Spring的事务管理逻辑和您的业务代码耦合。请结合业务场景选择合适的模式管理事务。
@Service
public class PaymentService {
    // 该类实例上的所有方法共享一个TransactionTemplate
    private final TransactionTemplate transactionTemplate;
    
    // 在构造方法上注入PlatformTransactionManager
    @Autowired
    public PaymentService(PlatformTransactionManager platformTransactionManager){
        this.transactionTemplate = new TransactionTemplate(platformTransactionManager);
    }
}

3.1 有返回参数的事务

        TransactionTemplate提供了一个execute()方法来执行事务,在该方法中执行的代码在同一个事务中。可以向execute方法中传递一个TransactionCallback的实现类,也可使用匿名类直接在匿名类中编写事务代码:

@Service
public class PaymentService {
    // 该类实例上的所有方法共享一个TransactionTemplate
    private final TransactionTemplate transactionTemplate;

    // 在构造方法上注入PlatformTransactionManager
    @Autowired
    public PaymentService(PlatformTransactionManager platformTransactionManager){
        this.transactionTemplate = new TransactionTemplate(platformTransactionManager);
    }

    public Order payOrder(Order order){
       Order result = transactionTemplate.execute(new TransactionCallback<Order>() {
            // 在该方法中的代码在一个事务中
            @Override
            public Order doInTransaction(TransactionStatus status) {
                createOrder(order);
                updateOrder(order);
                return order;
            }
        });
       return result;
    }
    private void createOrder(Order order){};
    private void updateOrder(Order order){};
}

execute方法保证了事务执行的原子性,上面例子中的任何一个方式执行失败,事务就会回滚。

3.2 无返回参数的事务

如果事务不需要返回参数,Spring提供了TransactionCallbackWithoutResult类,通过把该类作为参数传递给execute方法,可以更方便的实现该方法:

public void payOrderWithoutReturnResult(Order order){
        // 使用TransactionCallbackWithoutResult类型的匿名类
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                updateOrder(order);
                updateOrder(order);
            }
        });
}

3.3 事务的手动回滚

在TransactionTemplate.execute方法中,可以通过在传递给execute方法中的Calllback匿名类中的TransactionStatus实现手动回滚事务,调用TransactionStatus的setRollbackOnly方法(有参数返回的事务和无返回参数的事务操作相同)。

public void payOrderWithoutReturnResult(Order order){
        // 使用TransactionCallbackWithoutResult类型的匿名类
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    updateOrder(order);
                    updateOrder(order);
                }catch (Exception e){
                    // 记录错误日期
                    // 回滚事务
                    status.setRollbackOnly();
                }
            }
        });
}

3.4 事务参数设定

在不指定的情况下,TransactionTemplate使用默认的事务参数设定(默认设定参数),也可以通过编码的方式改变事务的默认参数,比如设定事务的传播模式,隔离基本,超时时间等。

    // 该类实例上的所有方法共享一个TransactionTemplate
    private final TransactionTemplate transactionTemplate;

    // 在构造方法上注入PlatformTransactionManager
    @Autowired
    public PaymentService(PlatformTransactionManager platformTransactionManager){
        this.transactionTemplate = new TransactionTemplate(platformTransactionManager);
        // 事务的隔离级别
        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
        // 事务超时时间20秒
        transactionTemplate.setTimeout(20);
        // 事务的传播行为
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY);
    }

TransactionTemplate的设定值是针对整个实例的,如果需要针对事务采取不同类型的配置,需要另外新创建一个TransactionTemplate实例。

4. TransactionManager事务管理

PlatformTransactionManager是更低一层级的事务管理器,@Transactional注解和TransactionTemplate都使用它来管理事务。

4.1 PlatformTransactionManager的注入

在需要使用事务管理的类中需要注入PlatformTransactionManager的bean实例,注入方式和使TransactionTemplate相同。

@Service
public class CartService {
    private final PlatformTransactionManager platformTransactionManager;
    
    // 构造方法上注入PlatformTransactionManager
    @Autowired
    public CartService(PlatformTransactionManager platformTransactionManager){
        this.platformTransactionManager = platformTransactionManager;
    }
}

4.2 PlatformTransactionManager事务管理的使用

因为PlatformTransactionManager是低一级的事务管理方法,使用它之前需要指定它对事务管理的各种参数设定,这种操作是通过TransactionDefinitionTransactionStatus这两个类实现的。虽然和TransactionTemplate相比需要手动指定这些设置,同时也带来了便利,可以在每个事务方法中指定不同的配置,满足不同事务管理的需要,不像TransactionTemplate那样,需要创建两个对象实例。

@Service
public class CartService {
    private final PlatformTransactionManager platformTransactionManager;

    // 构造方法上注入PlatformTransactionManager
    @Autowired
    public CartService(PlatformTransactionManager platformTransactionManager){
        this.platformTransactionManager = platformTransactionManager;
    }

    public void registerOrder(Order order){
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        // 事务的名字
        def.setName("registerOrderTransaction");
        // 事务的隔离级别
        def.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
        // 事务超时时间10秒
        def.setTimeout(10);

        TransactionStatus status = platformTransactionManager.getTransaction(def);

        try {
            // 需要事务的代码
            // 更新用户购买信息
            // 更新购物车信息
            // ....
        } catch (Exception e){
            e.printStackTrace();
            // 异常回滚事务
            platformTransactionManager.rollback(status);
        }
        
        // 正常执行,提交事务
        platformTransactionManager.commit(status);
    }
}

5. 总结

以上的事务管理适合大多数情况下需要编程式管理事务的情况,使用哪种方式需要架构师和程序员根据业务类型选择。Spring还提供了TransactionalOperator 和 ReactiveTransactionManager的方式来实现编程式事务,参考文章[2]中有说明,有兴趣的可以去查看Spring的官方网站。

6. 参考文章

[1] 11.6 Programmatic transaction management

[2] Programmatic Transaction Management

[3] https://www.baeldung.com/spring-pro

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

-无-为-

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

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

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

打赏作者

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

抵扣说明:

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

余额充值