美团二面:spring事务不生效的15种场景

bashendan
发布于 2023-6-8 11:55
浏览
0收藏

前言

大家好,我是田螺

日常开发中,我们经常使用到​​spring​​事务。最近星球一位还有去美团面试,被问了这么一道面试题: Spring 事务在哪几种情况下会不生效? 今天田螺哥跟大家聊聊,​​spring​​事务不生效的​​15​​种场景。

美团二面:spring事务不生效的15种场景-开源基础软件社区

1. 你的service类没有被Spring管理

//@Service (注释了@Service)
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
     @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    public void addTianLuo(TianLuo tianluo) {
        //保存tianluo实体数据库记录
        tianLuoMapper.save(tianluo);
        //保存tianluo流水数据库记录
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
  • 事务不生效的原因:上面例子中,​​@Service​​​注解注释之后,​​spring​​​事务(​​@Transactional​​​)没有生效,因为​​Spring​​​事务是由​​AOP​​​机制实现的,也就是说从​​Spring IOC​​​容器获取​​bean​​​时,​​Spring​​​会为目标类创建代理,来支持事务的。但是​​@Service​​​被注释后,你的​​service​​​类都不是​​spring​​管理的,那怎么创建代理类来支持事务呢
  • 解决方案:加上​​@Service​​注解。

2.没有在Spring配置文件中启用事务管理器

@Configuration
public class AppConfig {
    // 没有配置事务管理器
}

@Service
public class MyService {
    @Transactional
    public void doSomething() {
        // ...
    }
}
  • 事务不生效的原因:没有在​​AppConfig​​​中配置事务管理器,因此​​Spring​​​无法创建事务代理对象,导致事务不生效。即使在​​MyService​​​中添加了​​@Transactional​​​注解,该方法也不会被​​Spring​​管理的事务代理拦截。
  • 解决方案:为了解决这个问题,应该在​​AppConfig​​中配置一个事务管器。例如:

@Configuration
public class AppConfig {
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}

@Service
public class MyService {
    @Transactional
    public void doSomething() {
        // ...
    }
}

如果是​​Spring Boot​​项目,它默认会自动配置事务管理器并开启事务支持。

3. 事务方法被final、static关键字修饰

@Service
public class TianLuoServiceImpl  {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    public final void addTianLuo(TianLuo tianluo) {
         //保存tianluo实体数据库记录
        tianLuoMapper.save(tianluo);
        //保存tianluo流水数据库记录
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
  • 事务不生效的原因:如果一个方法被声明为​​final​​​或者​​static​​​,则该方法不能被子类重写,也就是说无法在该方法上进行动态代理,这会导致​​Spring​​无法生成事务代理对象来管理事务。
  • 解决方案:​​addTianLuo​事务方法不要用​​final​​​修饰或者​​static​​修饰。

4. 同一个类中,方法内部调用

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;
    
    public void addTianLuo(TianLuo tianluo){
     // 调用内部的事务方法
     this.executeAddTianLuo(tianluo);
   }

    @Transactional
    public void executeAddTianLuo(TianLuo tianluo) {
        tianLuoMapper.save(tianluo);
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
  • 事务不生效的原因: 事务是通过​​Spring AOP​​代理来实现的,而在同一个类中,一个方法调用另一个方法时,调用方法直接调用目标方法的代码,而不是通过代理类进行调用。即以上代码,调用目标​​executeAddTianLuo​​方法不是通过代理类进行的,因此事务不生效。
  • 解决方案:可以新建多一个类,让这两个方法分开,分别在不同的类中。如下:

@Service
public class TianLuoExecuteServiceImpl implements TianLuoExecuteService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;
    
    @Transactional
    public void executeAddTianLuo(TianLuo tianluo) {
        tianLuoMapper.save(tianluo);
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}

@Service
public class TianLuoAddServiceImpl implements TianLuoAddService {

    @Autowired
    private TianLuoExecuteService tianLuoExecuteService;
    
    public void addTianLuo(User user){
     tianLuoExecuteService.executeAddTianLuo(user);
   }
}

当然,有时候你也可以在该​​ Service​​​ 类中注入自己,或者通过​​AopContext.currentProxy()​​获取代理对象。

5.方法的访问权限不是public

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    private void addTianLuo(TianLuo tianluo) {
        tianLuoMapper.save(tianluo);
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
  • 事务不生效的原因:​​spring​​​事务方法​​addTianLuo​​​的访问权限不是​​public​​​,所以事务就不生效啦,因为​​Spring​​​事务是由​​AOP​​​机制实现的,​​AOP​​​机制的本质就是动态代理,而代理的事务方法不是​​public​​​的话,​​computeTransactionAttribute()​​​就会返回null,也就是这时事务属性不存在了。大家可以看下​​AbstractFallbackTransactionAttributeSource​​的源码:

美团二面:spring事务不生效的15种场景-开源基础软件社区

  • 解决方案:​​addTianLuo​​​事务方法的访问权限修改为​​public​​。

6. 数据库的存储引擎不支持事务

Spring事务的底层,还是依赖于数据库本身的事务支持。在​​MySQL​​​中,​​MyISAM​​​存储引擎是不支持事务的,​​InnoDB​​引擎才支持事务。因此开发阶段设计表的时候,确认你的选择的存储引擎是支持事务的

美团二面:spring事务不生效的15种场景-开源基础软件社区

7 .配置错误的 @Transactional 注解

@Transactional(readOnly = true)
public void updateUser(User user) {
    userDao.updateUser(user);
}
  • 事务不生效的原因:虽然使用了​​@Transactional​​​注解,但是注解中的​​readOnly=true​​​属性指示这是一个只读事务,因此在更新​​User​​实体时会抛出异常。
  • 解决方案:将​​readOnly​​​属性设置为​​false​​​,或者移除了​​@Transactional​​​注解中的​​readOnly​​属性。

8.事务超时时间设置过短

@Transactional(timeout = 1)
public void doSomething() {
    //...
}
  • 事务不生效的原因:在上面的例子中,​​timeout​​​属性被设置为​​1​​​秒,这意味着如果事务在​​1 ​​秒内无法完成,则报事务超时了。

9. 使用了错误的事务传播机制

@Service
public class TianLuoServiceImpl {
 
    @Autowired
    private TianLuoMapper tianLuoMapper;
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;
 
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public  void doInsertTianluo(TianLuo tianluo) throws Exception {
        tianLuoMapper.save(tianluo);
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
  • 事务不生效的原因:​​Propagation.NOT_SUPPORTED​​传播特性不支持事务。
  • 解决方案:选择正确的事务传播机制。

帮大家复习一下,​​Spring​​提供了七种事务传播机制。它们分别是:

  • ​REQUIRED​​(默认):如果当前存在一个事务,则加入该事务;否则,创建一个新事务。该传播级别表示方法必须在事务中执行。
  • ​SUPPORTS​​:如果当前存在一个事务,则加入该事务;否则,以非事务的方式继续执行。
  • ​MANDATORY​​:如果当前存在一个事务,则加入该事务;否则,抛出异常。
  • ​REQUIRES_NEW​​:创建一个新的事务,并且如果存在一个事务,则将该事务挂起。
  • ​NOT_SUPPORTED​​:以非事务方式执行操作,如果当前存在一个事务,则将该事务挂起。
  • ​NEVER​​:以非事务方式执行操作,如果当前存在一个事务,则抛出异常。
  • ​NESTED​​​:如果当前存在一个事务,则在嵌套事务内执行。如果没有事务,则按​​REQUIRED​​传播级别执行。嵌套事务是外部事务的一部分,可以在外部事务提交或回滚时部分提交或回滚。

10. rollbackFor属性配置错误

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional(rollbackFor = Error.class)
    public void addTianLuo(TianLuo tianluo) {
        //保存tianluo数据库记录
        tianLuoMapper.save(tianluo);
        //保存tianluo流水数据库记录
        tianLuoFlowMapper.saveFlow(tianluo);
        //模拟异常抛出
        throw new Exception();
    }
}
  • 事务不生效的原因: 其实​​rollbackFor​​​属性指定的异常必须是​​Throwable​​​或者其子类。默认情况下,​​RuntimeException​​​和​​Error​​​两种异常都是会自动回滚的。但是因为以上的代码例子,指定了​​rollbackFor = Error.class​​​,但是抛出的异常又是​​Exception​​​,而​​Exception和Error​​没有任何什么继承关系,因此事务就不生效。

美团二面:spring事务不生效的15种场景-开源基础软件社区

大家可以看一下​Transactional​注解源码哈:

美团二面:spring事务不生效的15种场景-开源基础软件社区

  • 解决方案:​​rollbackFor​​属性指定的异常与抛出的异常匹配。

11.事务注解被覆盖导致事务失效

public interface MyRepository {
    @Transactional
    void save(String data);
}

public class MyRepositoryImpl implements MyRepository {
    @Override
    public void save(String data) {
        // 数据库操作
    }
}

public class MyService {

    @Autowired
    private MyRepository myRepository;

    @Transactional
    public void doSomething(String data) {
        myRepository.save(data);
    }
}

public class MyTianluoService extends MyService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void doSomething(String data) {
        super.doSomething(data);
    }
}
  • 事务失效的原因:​​MyTianluoService​​​是​​MyService​​​的子类,并且覆盖了​​doSomething()​​​方法。在该方法中,使用了不同的传播行为​​(REQUIRES_NEW)​​​来覆盖父类的​​@Transactional​​​注解。在这种情况下,当调用​​MyTianluoService​​​的​​doSomething()​​​方法时,由于子类方法中的注解覆盖了父类的注解,​​Spring​​框架将不会在父类的方法中启动事务。因此,当​​MyRepository​​​的​​save()​​​方法被调用时,事务将不会被启动,也不会回滚。这将导致数据不一致的问题,因为在​​MyRepository​​​的​​save()​​方法中进行的数据库操作将不会回滚。

12.嵌套事务的坑

@Service
public class TianLuoServiceInOutService {

    @Autowired
    private TianLuoFlowService tianLuoFlowService;
    @Autowired
    private TianLuoMapper tianLuoMapper;

    @Transactional
    public void addTianLuo(TianLuo tianluo) throws Exception {
        tianLuoMapper.save(tianluo);
        tianLuoFlowService.saveFlow(tianluo);
    }
}

@Service
public class TianLuoFlowService {

    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional(propagation = Propagation.NESTED)
    public void saveFlow(TianLuo tianLuo) {
        tianLuoFlowMapper.save(tianLuo);
        throw new RuntimeException();
    }
}

以上代码使用了嵌套事务,如果​​saveFlow​​​出现运行时异常,会继续往上抛,到外层​​addTianLuo​​​的方法,导致​​tianLuoMapper.save​​也会回滚啦。如果不想因为被内部嵌套的事务影响,可以用​​try-catch​​包住,如下:

    @Transactional
    public void addTianLuo(TianLuo tianluo) throws Exception {
        tianLuoMapper.save(tianluo);
        try {
            tianLuoFlowService.saveFlow(tianluo);
        } catch (Exception e) {
          log.error("save tian luo flow fail,message:{}",e.getMessage());
        }
    }

13. 事务多线程调用

@Service
public class TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;

    @Autowired
    private TianLuoFlowService tianLuoFlowService;

    @Transactional
    public void addTianLuo(TianLuo tianluo) {
        //保存tianluo数据库记录
        tianLuoMapper.save(tianluo);
        //多线程调用
        new Thread(() -> {
            tianLuoFlowService.saveFlow(tianluo);
        }).start();
    }
}

@Service
public class TianLuoFlowService {

    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    public void save(TianLuo tianLuo) {
        tianLuoFlowMapper.saveFlow(tianLuo);
    }
}
  • 事务不生效原因:这是因为​​Spring​​事务是基于线程绑定的,每个线程都有自己的事务上下文,而多线程环境下可能会存在多个线程共享同一个事务上下文的情况,导致事务不生效。​​Spring​​​事务管理器通过使用线程本地变量(​​ThreadLocal​​)来实现线程安全。大家有兴趣的话,可以去看下源码哈.

在Spring事务管理器中,通过​​TransactionSynchronizationManager​​​类来管理事务上下文。​​TransactionSynchronizationManager​​​内部维护了一个​​ThreadLocal​​​对象,用来存储当前线程的事务上下文。在事务开始时,​​TransactionSynchronizationManager​​​会将事务上下文绑定到当前线程的​​ThreadLocal​​​对象中,当事务结束时,​​TransactionSynchronizationManager​​​会将事务上下文从​​ThreadLocal​​对象中移除。

美团二面:spring事务不生效的15种场景-开源基础软件社区

14.异常被捕获并处理了,没有重新抛出

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;

    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    public void addTianLuo(TianLuo tianluo) {
        try {
            //保存tianluo数据库记录
            tianLuoMapper.save(tianluo);
            //保存tianluo flow数据库记录
            tianLuoFlowMapper.saveFlow(tianluo);
        } catch (Exception e) {
            log.error("add TianLuo error,id:{},message:{}", tianluo.getId(),e.getMessage());
        }
    }

}
  • 事务不生效的原因: 事务中的异常已经被业务代码捕获并处理,而没有被正确地传播回事务管理器,事务将无法回滚。我们可以从​​spring​​​源码(​​TransactionAspectSupport​​这个类)中找到答案:

public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {

 //这方法会省略部分代码,只留关键代码哈
  @Nullable
 protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {

  if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
  
   TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
   Object retVal;
   try {
        //Spring AOP中MethodInterceptor接口的一个方法,它允许拦截器在执行被代理方法之前和之后执行额外的逻辑。
    retVal = invocation.proceedWithInvocation();
   }
   catch (Throwable ex) {
        //用于在发生异常时完成事务(如果Spring catch不到对应的异常的话,就不会进入回滚事务的逻辑)
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
   }
   finally {
    cleanupTransactionInfo(txInfo);
   }

      //用于在方法正常返回后提交事务。
   commitTransactionAfterReturning(txInfo);
   return retVal;
  }
}

在​​invokeWithinTransaction​​​方法中,当​​Spring​​​ catch到​​Throwable​​​异常的时候,就会调用​​completeTransactionAfterThrowing()​​​方法进行事务回滚的逻辑。但是,在​​TianLuoServiceImpl​​​类的​​spring​​​事务方法​​addTianLuo​​​中,直接把异常​​catch​​​住了,并没有重新​​throw​​​出来,因此 ​​Spring​​​自然就​​catch​​不到异常啦,因此事务回滚的逻辑就不会执行,事务就失效了。

  • 解决方案:在​​spring​​​事务方法中,当我们使用了​​try-catch​​,如果catch住异常,记录完异常日志什么的,一定要重新把异常抛出来,正例如下:

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;

    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional(rollbackFor = Exception.class)
    public void addTianLuo(TianLuo tianluo) {
        try {
            //保存tianluo数据库记录
            tianLuoMapper.save(tianluo);
            //保存tianluo flow数据库记录
            tianLuoFlowMapper.saveFlow(tianluo);
        } catch (Exception e) {
            log.error("add TianLuo error,id:{},message:{}", tianluo.getId(),e.getMessage());
            throw e;
        }
    }
}

15. 手动抛了别的异常

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    public void addTianLuo(TianLuo tianluo) throws Exception {
        //保存tianluo数据库记录
        tianLuoMapper.save(tianluo);
        //保存tianluo流水数据库记录
        tianLuoFlowMapper.saveFlow(tianluo);
        throw new Exception();
    }
}
  • 失效的原因:上面的代码例子中,手动抛了​​Exception​​​异常,但是是不会回滚的,因为Spring默认只处理​​RuntimeException和Error​​​,对于普通的​​Exception​​​不会回滚,除非,用​​rollbackFor​​属性指定配置。
  • 解决方案:添加属性配置​​@Transactional(rollbackFor = Exception.class)​​。

注解为事务范围的方法中,事务的回滚仅仅对于unchecked的异常有效。对于checked异常无效。也就是说事务回滚仅仅发生在,出现RuntimeException或Error的时候。通俗一点就是:代码中出现的空指针等异常,会被回滚。而文件读写、网络超时问题等,spring就没法回滚了。


文章转载自公众号:捡田螺的小男孩

标签
已于2023-6-8 11:55:32修改
收藏
回复
举报
回复
    相关推荐