一个@Transaction哪里来这么多坑?

点击蓝色“程序员DMZ ”关注我哟程序员

好看记得加个“星标”哈!web

前言

在以前的文章中已经对Spring中的事务作了详细的分析了,这篇文章咱们来聊一聊日常工做时使用事务可能出现的一些问题(本文主要针对使用@Transactional进行事务管理的方式进行讨论)以及对应的解决方案sql

  1. 事务失效
  2. 事务回滚相关问题
  3. 读写分离跟事务结合使用时的问题

事务失效

事务失效咱们通常要从两个方面排查问题数据库

数据库层面

数据库层面,数据库使用的存储引擎是否支持事务?默认状况下MySQL数据库使用的是Innodb存储引擎(5.5版本以后),它是支持事务的,可是若是你的表特意修改了存储引擎,例如,你经过下面的语句修改了表使用的存储引擎为MyISAM,而MyISAM又是不支持事务的缓存

alter table table_name engine=myisam;

这样就会出现“事务失效”的问题了微信

「解决方案」:修改存储引擎为Innodb架构

业务代码层面

业务层面的代码是否有问题,这就有不少种可能了app

  1. 咱们要使用Spring的声明式事务,那么须要执行事务的Bean是否已经交由了Spring管理?在代码中的体现就是类上是否有 @ServiceComponent等一系列注解

「解决方案」:将Bean交由Spring进行管理(添加@Service注解)框架

  1. @Transactional注解是否被放在了合适的位置。在上篇文章中咱们对Spring中事务失效的原理作了详细的分析,其中也分析了Spring内部是如何解析 @Transactional注解的,咱们稍微回顾下代码:
注解解析

代码位于:AbstractFallbackTransactionAttributeSource#computeTransactionAttribute编辑器

也就是说,默认状况下你没法使用@Transactional对一个非public的方法进行事务管理

「解决方案」:修改须要事务管理的方法为public

  1. 出现了自调用。什么是自调用呢?咱们看个例子
@Service
public class DmzService {
 
 public void saveAB(A a, B b) {
  saveA(a);
  saveB(b);
 }

 @Transactional
 public void saveA(A a) {
  dao.saveA(a);
 }
 
 @Transactional
 public void saveB(B b){
  dao.saveB(a);
 }
}

上面三个方法都在同一个类DmzService中,其中saveAB方法中调用了本类中的saveAsaveB方法,这就是自调用。在上面的例子中saveAsaveB上的事务会失效

那么自调用为何会致使事务失效呢?咱们知道Spring中事务的实现是依赖于AOP的,当容器在建立dmzService这个Bean时,发现这个类中存在了被@Transactional标注的方法(修饰符为public)那么就须要为这个类建立一个代理对象并放入到容器中,建立的代理对象等价于下面这个类

public class DmzServiceProxy {

    private DmzService dmzService;

    public DmzServiceProxy(DmzService dmzService) {
        this.dmzService = dmzService;
    }

    public void saveAB(A a, B b) {
        dmzService.saveAB(a, b);
    }

    public void saveA(A a) {
        try {
            // 开启事务
            startTransaction();
            dmzService.saveA(a);
        } catch (Exception e) {
            // 出现异常回滚事务
            rollbackTransaction();
        }
        // 提交事务
        commitTransaction();
    }

    public void saveB(B b) {
        try {
            // 开启事务
            startTransaction();
            dmzService.saveB(b);
        } catch (Exception e) {
            // 出现异常回滚事务
            rollbackTransaction();
        }
        // 提交事务
        commitTransaction();
    }
}

上面是一段伪代码,经过startTransactionrollbackTransactioncommitTransaction这三个方法模拟代理类实现的逻辑。由于目标类DmzService中的saveAsaveB方法上存在@Transactional注解,因此会对这两个方法进行拦截并嵌入事务管理的逻辑,同时saveAB方法上没有@Transactional,至关于代理类直接调用了目标类中的方法。

咱们会发现当经过代理类调用saveAB时整个方法的调用链以下:

实际上咱们在调用saveAsaveB时调用的是目标类中的方法,这种清空下,事务固然会失效。

常见的自调用致使的事务失效还有一个例子,以下:

@Service
public class DmzService {
 @Transactional
 public void save(A a, B b) {
  saveB(b);
 }
 
 @Transactional(propagation = Propagation.REQUIRES_NEW)
 public void saveB(B b){
  dao.saveB(a);
 }
}

当咱们调用save方法时,咱们预期的执行流程是这样的

也就是说两个事务之间互不干扰,每一个事务都有本身的开启、回滚、提交操做。

但根据以前的分析咱们知道,实际上在调用saveB方法时,是直接调用的目标类中的saveB方法,在saveB方法先后并不会有事务的开启或者提交、回滚等操做,实际的流程是下面这样的

因为saveB方法其实是由dmzService也就是目标类本身调用的,因此在saveB方法的先后并不会执行事务的相关操做。这也是自调用带来问题的根本缘由:「自调用时,调用的是目标类中的方法而不是代理类中的方法」

「解决方案」

  1. 本身注入本身,而后显示的调用,例如:

    @Service
    public class DmzService {
     // 本身注入本身
     @Autowired
     DmzService dmzService;
     
     @Transactional
     public void save(A a, B b) {
      dmzService.saveB(b);
     }

     @Transactional(propagation = Propagation.REQUIRES_NEW)
     public void saveB(B b){
      dao.saveB(a);
     }
    }

    这种方案看起来不是很优雅

  2. 利用AopContext,以下:

    @Service
    public class DmzService {

     @Transactional
     public void save(A a, B b) {
      ((DmzService) AopContext.currentProxy()).saveB(b);
     }

     @Transactional(propagation = Propagation.REQUIRES_NEW)
     public void saveB(B b){
      dao.saveB(a);
     }
    }

    使用上面这种解决方案须要注意的是,须要在配置类上新增一个配置

    // exposeProxy=true表明将代理类放入到线程上下文中,默认是false
    @EnableAspectJAutoProxy(exposeProxy = true)

    我的比较喜欢的是第二种方式

这里咱们作个来作个小总结

总结

一图胜千言

事务失效的缘由

事务回滚相关问题

回滚相关的问题能够被总结为两句话

  1. 想回滚的时候事务却提交了
  2. 想提交的时候被标记成只能回滚了(rollback only)

先看第一种状况:「想回滚的时候事务却提交了」。这种状况每每是程序员对Spring中事务的rollbackFor属性不够了解致使的。

Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其余异常不会触发回滚事务,已经执行的SQL会提交掉。若是在事务中抛出其余类型的异常,但却指望 Spring 可以回滚事务,就须要指定rollbackFor属性。

对应代码其实咱们上篇文章也分析过了,以下:

回滚代码

以上代码位于:TransactionAspectSupport#completeTransactionAfterThrowing方法中

默认状况下,只有出现RuntimeException或者Error才会回滚

public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}

因此,若是你想在出现了非RuntimeException或者Error时也回滚,请指定回滚时的异常,例如:

@Transactional(rollbackFor = Exception.class)

第二种状况:「想提交的时候被标记成只能回滚了(rollback only)」

对应的异常信息以下:

Transaction rolled back because it has been marked as rollback-only

咱们先来看个例子吧

@Service
public class DmzService {

 @Autowired
 IndexService indexService;

 @Transactional
 public void testRollbackOnly() {
  try {
   indexService.a();
  } catch (ClassNotFoundException e) {
   System.out.println("catch");
  }
 }
}

@Service
public class IndexService {
 @Transactional(rollbackFor = Exception.class)
 public void a() throws ClassNotFoundException{
  // ......
  throw new ClassNotFoundException();
 }
}

在上面这个例子中,DmzServicetestRollbackOnly方法跟IndexServicea方法都开启了事务,而且事务的传播级别为required,因此当咱们在testRollbackOnly中调用IndexServicea方法时这两个方法应当是共用的一个事务。按照这种思路,虽然IndexServicea方法抛出了异常,可是咱们在testRollbackOnly将异常捕获了,那么这个事务应该是能够正常提交的,为何会抛出异常呢?

若是你看过我以前的源码分析的文章应该知道,在处理回滚时有这么一段代码

rollBackOnly设置

在提交时又作了下面这个判断(这个方法我删掉了一些不重要的代码

commit_rollbackOnly

能够看到当提交时发现事务已经被标记为rollbackOnly后会进入回滚处理中,而且unexpected传入的为true。在处理回滚时又有下面这段代码

抛出异常

最后在这里抛出了这个异常。

以上代码均位于AbstractPlatformTransactionManager

总结起来,「主要的缘由就是由于内部事务回滚时将整个大事务作了一个rollbackOnly的标记」,因此即便咱们在外部事务中catch了抛出的异常,整个事务仍然没法正常提交,而且若是你但愿正常提交,Spring还会抛出一个异常。

「解决方案」:

这个解决方案要依赖业务而定,你要明确你想要的结果是什么

  1. 内部事务发生异常,外部事务catch异常后,内部事务自行回滚,不影响外部事务

将内部事务的传播级别设置为nested/requires_new都可。在咱们的例子中就是作以下修改:

// @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void a() throws ClassNotFoundException{
   // ......
   throw new ClassNotFoundException();
}

虽然这二者都能获得上面的结果,可是它们之间仍是有不一样的。当传播级别为requires_new时,两个事务彻底没有联系,各自都有本身的事务管理机制(开启事务、关闭事务、回滚事务)。可是传播级别为nested时,实际上只存在一个事务,只是在调用a方法时设置了一个保存点,当a方法回滚时,其实是回滚到保存点上,而且当外部事务提交时,内部事务才会提交,外部事务若是回滚,内部事务会跟着回滚。

  1. 内部事务发生异常时,外部事务catch异常后,内外两个事务都回滚,可是方法不抛出异常
@Transactional
public void testRollbackOnly() {
   try {
      indexService.a();
   } catch (ClassNotFoundException e) {
      // 加上这句代码
      TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
   }
}

经过显示的设置事务的状态为RollbackOnly。这样当提交事务时会进入下面这段代码

显示回滚

最大的区别在于处理回滚时第二个参数传入的是false,这意味着回滚是回滚是预期之中的,因此在处理完回滚后并不会抛出异常。

读写分离跟事务结合使用时的问题

读写分离通常有两种实现方式

  1. 配置多数据源
  2. 依赖中间件,如 MyCat

若是是配置了多数据源的方式实现了读写分离,那么须要注意的是:「若是开启了一个读写事务,那么必须使用写节点」「若是是一个只读事务,那么可使用读节点」

若是是依赖于MyCat等中间件那么须要注意:「只要开启了事务,事务内的SQL都会使用写节点(依赖于具体中间件的实现,也有可能会容许使用读节点,具体策略须要自行跟DB团队确认)」

基于上面的结论,咱们在使用事务时应该更加谨慎,在没有必要开启事务时尽可能不要开启。

通常咱们会在配置文件配置某些约定的方法名字前缀开启不一样的事务(或者不开启),但如今随着注解事务的流行,好多开发人员(或者架构师)搭建框架的时候在service类上加上了@Transactional注解,致使整个类都是开启事务的,这样严重影响数据库执行的效率,更重要的是开发人员不重视、或者不知道在查询类的方法上面本身加上@Transactional(propagation=Propagation.NOT_SUPPORTED)就会致使,全部的查询方法实际并无走从库,致使主库压力过大。

其次,关于若是没有对只读事务作优化的话(优化意味着将只读事务路由到读节点),那么@Transactional注解中的readOnly属性就应该要慎用。咱们使用readOnly的本来目的是为了将事务标记为只读,这样当MySQL服务端检测到是一个只读事务后就能够作优化,少分配一些资源(例如:只读事务不须要回滚,因此不须要分配undo log段)。可是当配置了读写分离后,可能会可能会致使只读事务内全部的SQL都被路由到了主库,读写分离也就失去了意义。

总结

本文为事务专栏最后一篇啦!这篇文章主要是总结了工做中事务相关的常见问题,想让你们少走点弯路!但愿你们能够认真读完哦,有什么问题能够直接在后台私信我或者加我微信!

这篇文章也是整个Spring系列的最后一篇文章,以后可能会出一篇源码阅读心得,跟你们聊聊如何学习源码。

另外今年也给本身定了个小目标,就是完成SSM框架源码的阅读。目前来讲Spring是完成,接下来就是SpringMVC跟MyBatis。

在分析MyBatis前,会从JDBC源码出发,而后就是MyBatis对配置的解析、MyBatis执行流程、MyBatis的缓存、MyBatis的事务管理以及MyBatis的插件机制。

在学习SpringMVC前,会从TomCat出发,先讲清楚TomCat的原理,咱们再来看SpringMVC。整个来讲相比于Spring源码,我以为应该不算特别难。

但愿在这个过程当中能够跟你们一块儿进步!!!

我叫DMZ,一个陪你一块儿慢慢进步的小菜鸟~!

往期精选


Spring事务源码分析专题(一)JdbcTemplate使用及源码分析

Spring事务源码分析专题(二)Mybatis的使用及跟Spring整合原理分析

Spring事务专题(三)事务的基本概念,Mysql事务处理原理

Spring事务专题(四)Spring中事务使用、抽象机制及模拟Spring事务实现

本文分享自微信公众号 - 程序员DMZ(programerDmz)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索