写点什么

阿里一面 五问 @Transactional

用户头像
skow
关注
发布于: 3 小时前
阿里一面 五问 @Transactional

1 问:@Transactional 注解可以作用于哪些地方?

  • 作用于类:当把 @Transactional 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息,需注意,只有 public 方法才会生效,此处是因为 AOP 的特性原因

  • 作用于方法:当类配置了 @Transactional,方法也配置了 @Transactional,会优先采取方法上的 @Transactional,此优先级较高,当然我们一般建议,都是放在方法上,不建议配置在类上

  • 作用于接口:不推荐这种使用方法,因为一旦标注在 Interface 上并且配置了 Spring AOP 使用 CGLib 动态代理,将会导致 @Transactional 注解失效

2 问:@Transactional 具有哪些属性

value

指定使用的事务管理器,在多数据源的时候发挥用处

rollbackFor

这个属性,也是我们最经常使用到的属性,我们可以定义一个或多个异常的 classes ,它们必须是 Throwable 的子类,指示哪些异常类型必须导致事务回滚。


默认情况下,事务将在 RuntimeException 和 Error 上回滚,但不会在 CheckedException 上回滚


// 指定单一异常类@Transactional(rollbackFor=RuntimeException.class)
// 指定多个异常类@Transactional(rollbackFor={RuntimeException.class, Exception.class})
复制代码
rollbackForClassName

这个属性其实和 rollbackFor 有着异曲同工之妙,可定义 0 个或多个异常名称(对于必须是 Throwable 子类的异常),要指示哪些异常类型必须导致事务回滚。


可以是完全限定类名的子字符串,但是目前不支持通配符。


例如,ServletException 的值将匹配 javax.servlet.ServletException 及其子类

noRollbackFor

这个属性需定义 0 个或多个异常 Classes ,它们必须是 Throwable 子类,指示哪些异常类型不可导致事务回滚,这是构建回滚规则的首选方法

noRollbackForClassName

这个属性和 noRollbackFor 有着异曲同工之妙,不展开说明

Propagation

这个属性用来设置事务的传播行为,默认值为 Propagation.REQUIRED,其他的属性信息如下:


  • Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果 A 方法和 B 方法都添加了注解,在默认传播模式下,A 方法内部调用 B 方法,会把两个方法的事务合并为一个事务

  • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。

  • Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。

  • Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。(当类 A 中的 a 方法用默认 Propagation.REQUIRED 模式,类 B 中的 b 方法加上采用 Propagation.REQUIRES_NEW 模式,然后在 a 方法中调用 b 方法操作数据库,然而 a 方法抛出异常后,b 方法并没有进行回滚,因为 Propagation.REQUIRES_NEW 会暂停 a 方法的事务)

  • Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。

  • Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。

  • Propagation.NESTED :和 Propagation.REQUIRED 效果一样。

timeout

此属性为设置此事务的超时时间(以秒为单位)。默认为底层事务系统的默认超时。专为与 Propagation.REQUIREDPropagation.REQUIRES_NEW 一起使用而设计,因为它仅适用于新启动的事务

readOnly

如果事务实际上是只读的,则可以将其设置为 true 布尔标志,从而允许在运行时进行相应的优化,默认为 false


如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;


如果你一次执行多条查询语句


例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持

isolation

设置事务隔离级别,默认为 Isolation.DEFAULT 专为与 Propagation.REQUIREDPropagation.REQUIRES_NEW 一起使用而设计,因为它仅适用于新启动的事务。 如果您希望隔离级别声明在参与具有不同隔离级别的现有事务时被拒绝,请考虑将事务管理器上的 “validateExistingTransactions” 标志切换为 “true”


其他属性信息如下(其实和 MySQL 的事务隔离级别一样的,不展开说明):


  • Isolation.DEFAULT :使用底层数据存储的默认隔离级别,MySQL 默认的隔离级别为可重复读


READ_UNCOMMITTED:读可不提交;


READ_COMMITTED:读提交;


REPEATABLE_READS:可重复读;


ERIALIZABLE:串行化

3 问:@Transactional 什么时候失效

  • 注解运用在非 public 的方法上

  • 事务拦截器 TransactionInterceptor 会在目前前后进行事务拦截,会对 DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy` 的 invoke 方法 进行调用

  • 本质其实就是调用 AbstractFallbackTransactionAttributeSourc`的 computeTransactionAttribute 方法,获取 Transactional 注解的事务配置信息

  • 我们可以看到 153-155 行


      if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {        return null;      }
复制代码


如果不是 public,则直接返回 null,事务即不生效


Tips1:protected、privat 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点


Tips2:即使你的方法是 public 的,但是如果被 private 的方法调用,@Transactional 注解同样也会失效


  • 抛出不符合 rollbackFor 的异常类型

  • 在不指定 rollbackFor 的回滚异常类型的时候,如果你抛出了 Exception 的异常类型,那么事务无法回滚

  • 在类中相互调用

  • 在 Service 中添加两个方法,在 Controller 中调用 a 方法,a 方法中调用 b 方法,b 方法抛出异常,结果事务也没进行回滚


  @Override  public void a (){      this.b();  }    @Transactional  private void b(){    // do something     throw new RuntimeException();  }
复制代码


为什么此种方法会导致事务失效呢?也是因为 AOP 的缘故


我们在该类的方法上标记了 @Service 注解,表示把这个类交由 Spring 管理,将该 Bean 注册到 Spring 的容器中,Spring 又是通过动态代理模式来实现 AOP ,简单理解就是容器中的 Bean 对象实际上都是代理对象


Spring 也正是通过该方式对 @Transactional 进行支持的,Spring 会对原对象中的方法进行封装(即检查到标有该注解的方法时,就会为它加上事务)


所以我们通过 Controller 对 a 方法进行调用的时候,不会进行事务控制


  • 多线程调用事务方法


    @Override    @Transactional    public void a (){        // 1-do something                // 2-多线程调用b        new Thread(() -> b()).start();        this.b();    }
@Transactional public void b(){ int a = 1 / 0 ; }
复制代码


从上面的 demo 中,我们可以看到事务方法 a 中,调用了事务方法 b,但是事务方法 b 是在另外一个线程中调用的


这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务,此时 b 方法中抛了异常,a 方法也无法进行回滚


  • 底层事务设置错误

  • 表不支持事务,或者未开启事务

  • 异常未被抛出异常,被 catch 住了

  • 事务的传播的特性设置错误

4 问:@Transactional 什么时候开启事务

这个答案藏在源码里


我们随意在一个带有 @Transactional 注解进行 debug


然后观察方法调用栈,发现这么一个地方



观察到 287 行 至 295 行


    if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {      // Standard transaction demarcation with getTransaction and commit/rollback calls.      TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);      Object retVal = null;      try {        // This is an around advice: Invoke the next interceptor in the chain.        // This will normally result in a target object being invoked.        // 注释写着:这是一个环绕执行器,会去调用链中的下一个拦截器。 这通常会导致目标对象被调用。        // 简单来说,就是这个方法会去调用你的业务代码        retVal = invocation.proceedWithInvocation();      }
复制代码


小眼一看 createTransactionIfNecessary 这个方法是用来干嘛的,debug 进去看看,走下去


重点关注到 org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin


发现此处只是针对事务做了一些准备动作,观察到 278-284 行


      if (con.getAutoCommit()) {        txObject.setMustRestoreAutoCommit(true);        if (logger.isDebugEnabled()) {          logger.debug("Switching JDBC Connection [" + con + "] to manual commit");        }        con.setAutoCommit(false);      }
复制代码


将自动提交关闭了,启动了事务管理


那么事务真正启动的时机是什么时候呢?


经过 debug 和测试,直接给出结论


事务是是在执行到准备动作后的第一个操作表的语句,事务才算是真正启动。

5 问:平时控制事务都是使用 @Transactional 吗?

@Transactional 虽然可以不侵入的为我们的代码加上事务控制,但是的确有许多需要注意事务失效的地方


并且在代码逻辑中, @Transactional 容易使事务控制粒度过大,而且 @Transactional 基于 AOP 实现的,但是在代码中,有时候我们会有很多切面,不同的切面可能会来处理不同的事情,多个切面之间可能会有相互影响


可以考虑自己手动控制事务的开启关闭


 @Resource protected TransactionTemplate transactionTemplate;  transactionTemplate.execute(input -> {})
复制代码


@Transactional 不是不可以用,而是需要因场景制宜


在补充一点规范中关于 @Transactional 的说明



用户头像

skow

关注

分享,前方就是一片花海 2019.05.30 加入

微信公众号:codeLiveHouse

评论

发布
暂无评论
阿里一面 五问 @Transactional