Spring事务介绍以及事务失效的场景 - Java技术债务


事务的实现

  1. Spring事务底层是基于数据库事务和AOP机制的
  2. 首先对于使用了@ Transactiona注解的Bean,Spring会创建一个代理对象作为Bean
  3. 当调用代理对象的方法时,会先判断该方法上是否加了@ Transactional注解
  4. 如果加了,那么则利用事务管理器创建一个数据库连接
  5. 并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的一步
  6. 然后执行当前方法,方法中会执行sql
  7. 执行完当前方法后,如果没有出现异常就直接提交事务
  8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
  9. Spring事务的隔离级别对应的就是数据库的隔离级别
  10. Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
  11. Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql

事务的传播行为(propagation behavior)

指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。

Spring默认事务传播行为是:propagation_required

Spring事务介绍以及事务失效的场景 - Java技术债务

事务的隔离级别

  1. TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别.
  2. TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据.该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别.比如PostreSQL实际上并没有此级别.
  3. TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据.该级别可以防止脏读,这也是大多数情况下的推荐值.
  4. TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同.该级别可以防止脏读和不可重复读.
  5. TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读.但是这将严重影响程序的性能,通常情况下也不会用到该级别.

事务失效的原因

在某些业务场景下,如果一个请求中,需要同时写入多张表的数据。为了保证操作的原子性 (要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。

但是如果使用不当会导致事务失效,以下是失效的场景

访问权限

java的访问权限主要有四种:public、protected、default、private,它们的权限从左到右,依次变小。把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题,例如:

@Service
public class UserService {
    
    @ Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

add方法的访问权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须是public的。在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class targetClass) {
    // Don't allow no-public methods as required.
    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;
  }

也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是public,而是private、default或protected的话,spring则不会提供事务功能。

方法被final修饰

有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final,例如:

@Service
public class UserService {

    @ Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

add方法被定义成了final的,这样会导致事务失效。spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。

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

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

方法内部调用

同一个类中不同方法互相调用,导致事务失效,比如:

@Service
public class UserService {
    @ Autowired
    private UserMapper userMapper;
    @ Transactional
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }
    @ Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
}

在事务方法add中,直接调用事务方法updateStatus

从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象(并不是代理对象,而是当前对象)的方法,所以updateStatus方法不会生成事务。

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

解决办法:新加一个XxxHelpService类

新加一个XxxHelpService类,把@ Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:

@Servcie
public class ServiceA {
   @ Autowired
   prvate ServiceB serviceB;
   public void save(User user) {
         queryData1();
         queryData2();
         serviceB.doSave(user);
   }
 }
 @Servcie
 public class ServiceB {
    @ Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

解决办法:在该Service类中注入自己

如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。

由于Spring的三级缓存,所以并不会出现循环依赖的问题。关于Spring的三级缓存的问题请看:

Spring中Bean的循环依赖 - Java技术债务

具体代码如下:

@Servcie
public class ServiceA {
   @ Autowired
   prvate ServiceA serviceA;

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

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

解决办法:通过AopContent类

在该Service类中使用AopContext.currentProxy()获取代理对象

上面的方法确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。具体代码如下:

@Servcie
public class ServiceA {

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

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

未被Spring管理的类

使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

通常情况下,我们通过@ Controller、@ Service、@ Component、@ Repository等注解,可以自动实现bean实例化和依赖注入的功能。

//@Service
public class UserService {
    @ Transactional
    public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }    
}

这个一般不会直接导致事务失效才发现问题,一般情况在使用@ Autowired或者@ Resource时就会发现类并没有注入到Spring容器中。

多线程调用

多线程的使用场景还是挺多的。如果spring事务用在多线程场景中,会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。

spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。

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

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

表不支持事务

在mysql5之前,默认的数据库引擎是myisam ,索引文件和数据文件是分开存储的,对于查多写少的单表操作,虽然性能比innodb更好,但是并不支持事务。

未开启事务

如果你使用的是Spring Boot项目,会通过DataSourceTransactionManagerAutoConfiguration类,默默的帮你开启了事务。你所要做的事情很简单,只需要配置spring.datasource 相关参数即可。

但如果你使用的还是传统的Spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。

 
 
     
 
 
     
        
     
 
 
 
     
     

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

事务传播特性使用不当

通过上面的[事务的传播行为](https://cuizb.top/myblog/article/1667390158#事务的传播行为(propagation behavior))知道,在使用@ Transactional注解时,是可以指定propagation参数的

如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:

@Service
public class UserService {

    @ Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

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

目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。

手动捕获异常

在代码中手动try…catch了异常。比如:

@Slf4j
@Service
public class UserService {
    @ Transactional
    public void add(UserModel userModel) {
        try {
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

这种情况下spring事务当然不会回滚,因为捕获了异常,没有手动抛出,换句话说就是把异常吞掉了。

如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。

抛出非RuntimeException异常

即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。

自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。

因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。

@Slf4j
@Service
public class UserService {
    
    @ Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

自定义事务回滚异常

在使用@ Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。

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

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

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

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

所以,建议一般情况下,将该参数设置成:ExceptionThrowable

总结

规范开发,规范使用Spring 避免不必要的麻烦。


Be in awe of every code modification

   登录后才可以发表呦...

专注分享Java技术干货,包括
但不仅限于多线程、JVM、Spring Boot
Spring Cloud、 Redis、微服务、
消息队列、Git、面试题 最新动态等。

想交个朋友吗
那就快扫下面吧


微信

Java技术债务

你还可以关注我的公众号

会分享一些干货或者好文章

Java技术债务