春哥大魔王的博客

春哥大魔王的博客

写在前面

琪姐问了个问题,原来的方法逻辑,类似如下伪代码:

updateTableA.setStatus(0);
rpcOperateAccount();
updateTableA.setStatus(1);

相信很好理解。

琪姐说将这段逻辑放到线程池中一起提交就OK(意思应该是DB落库成功)。

executorService.execute(()=>{
	try{
		// 上面那段逻辑
	} catch(Exception e){
		LOGGER.error("")
	}
})

但是把那段逻辑拿出来就落库失败:

func doSomething() throw BizException	{
	// 上面那段逻辑
}

最后琪姐说,修改了Spring的事务传播机制好了(应该是从默认的Required => Requires_New)。

虽然没有源码,但是可以基于这个问题说说Spring的事务传播机制。

而且事务会因为RuntimeException和Error回滚。

Spring的传播机制

传播记住有如下几种:

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
}
  • PROPAGATION_REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
  • PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
  • PROPAGATION_MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常。
  • PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。
  • PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  • PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

常用的主要有Required,RequiresNew,Nested三种。

  • Required:简单理解就是事务方法会判断是否存在事务,有事务就用已有的,没有就重新开启一个。
  • RequiresNew:简单理解就是开启新事务,若当前已有事务,挂起当前事务。新开启的事务和之前的事务无关,拥有自己的锁和隔离级别,可以独立提交和回滚,内层事务执行期间,外层事务挂起,内层事务执行完成后,外层事务恢复执行。
  • Nested:简单理解就是嵌套事务,如果外部事务回滚,则嵌套事务也会回滚!!!外部事务提交的时候,嵌套它才会被提交。嵌套事务回滚不会影响外部事务。子事务是上层事务的嵌套事务,在子事务执行之前会建立savepoint,嵌套事务的回滚会回到这个savepoint,不会造成父事务的回滚。

如果想事务一起执行可以用Required满足大部分场景,如果不想让执行的子事务的结果影响到父事务的提交可以将子事务设置为RequiresNew。

嵌套是子事务套在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫save point,然后执行子事务,这个子事务的执行也算是父事务的一部分,然后子事务执行结束,父事务继续执行。重点就在于那个save point。看几个问题就明了了:

如果子事务回滚,会发生什么?父事务会回滚到进入子事务前建立的save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。

如果父事务回滚,会发生什么?父事务回滚,子事务也会跟着回滚!为什么呢,因为父事务结束之前,子事务是不会提交的,我们说子事务是父事务的一部分,正是这个道理。那么:

事务的提交,是什么情况?是父事务先提交,然后子事务提交,还是子事务先提交,父事务再提交?答案是第二种情况,还是那句话,子事务是父事务的一部分,由父事务统一提交。

PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:

它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。

使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTA TrasactionManager实现可能有不同的支持方式。

PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。

另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。

由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back.

PROPAGATION_REQUIRED应该是我们首先的事务传播行为。它能够满足我们大多数的事务需求。

事务状态TransactionStatus

TransactionStatus接口的一个实现,这个接口的内容如下:

public interface TransactionStatus{
    boolean isNewTransaction(); // 是否是新的事物
    boolean hasSavepoint(); // 是否有恢复点
    void setRollbackOnly();  // 设置为只回滚
    boolean isRollbackOnly(); // 是否为只回滚
    boolean isCompleted; // 是否已完成
}

可以发现这个接口描述的是一些处理事务提供简单的控制事务执行和查询事务状态的方法,在回滚或提交的时候需要应用对应的事务状态。

事务注解

@Transactional(propagation=Propagation.REQUIRES_NEW,
            isolation=Isolation.READ_COMMITTED,
            noRollbackFor={UserAccountException.class},
            readOnly=true, timeout=3)

1.添加事务注解使用propagation 指定事务的传播行为,即当前的事务方法被另外一个事务方法调用时如何使用事务。默认取值为REQUIRED,即使用调用方法的事务REQUIRES_NEW:使用自己的事务,调用的事务方法的事务被挂起。

2.使用isolation 指定事务的隔离级别,最常用的取值为READ_COMMITTED。

3.默认情况下 Spring 的声明式事务对所有的运行时异常进行回滚,也可以通过对应的属性进行设置。通常情况下,默认值即可。

4.使用readOnly 指定事务是否为只读。 表示这个事务只读取数据但不更新数据,这样可以帮助数据库引擎优化事务。若真的是一个只读取数据库值得方法,应设置readOnly=true。

5.使用timeOut 指定强制回滚之前事务可以占用的时间。

最后

回到琪姐那个问题上,既然修改了传播机制可以正常提交了,那么说明需要开启两个事务才可以提交,而且捕获的异常是Excepiton(RollbackFor)。还存在几个问题待解决:

  • 现在两个update方法的隔离级别是RC,会不会和隔离级别有关呢?比如数据死锁?
  • 之前线程池执行和主线程执行在传播机制,隔离级别有区别吗?
07-07 16:07