Spring @Transaction的踩坑指南

目录

文章目录


前言

由于最近代码Review部分发现@Transaction注解使用的一些问题,由于陈年祖传的代码中一些不规范使用的情况,造成事务使用的一些问题。因此特此细化整理事务相关的知识来充实一下自己,以更好的面对实际的业务优化。

基本概念

事务(Transaction)是并发控制的单位,是用户定义的一个操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位。事务通常是以BEGIN TRANSACTION开始,以COMMIT或ROLLBACK结束。
COMMIT表示提交,即提交事务的所有操作。具体地说就是将事务中所有对数据库的更新写回到磁盘上的物理数据库中去,事务正常结束。
ROLLBACK表示回滚,即在事务运行的过程中发生了某种故障,事务不能继续进行,系统将事务中对数据库的所有以完成的操作全部撤消,滚回到事务开始的状态。

编程式事务

基于底层的API,如PlatformTransactionManager、TransactionDefinition和TransactionTemplate等核心接口,开发者完全可以通过编程的方式进行事务管理。编程式事务需要开发者在代码中手动管理事务的开启、提交和回滚等操作。

声明式事务

声明式事务管理建立在 AOP 之上的。其本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。优点是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过 @Transactional 注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

@Transaction注解的相关属性说明
Property(属性名称)Type(属性类型)Description(属性描述)
valueStringOptional qualifier that specifies the transaction manager to be used.
事务名称,默认情况下可以不管,当在同一个类中需要使用多个逻辑事务,且各个逻辑事务可能使用不同的事务管理器TransactionManager时需要通过该值来区分实际使用哪个事务管理器。
transactionManagerStringAlias for value.
事务管理器名称,默认情况下可以不管,同value.
propagationenum: org.springframework.transaction.annotation.PropagationOptional propagation setting.
指定事务的传播级别,默认值为REQUIRED
- REQUIRED 如果物理事务不存在则创建物理事务,如果物理事务存在则加入当前物理事务
- REQUIRES_NEW 总是创建一个新的物理事务,如果当前已经存在物理事务则先把已有物理事务挂起(新事务和当前事务独立,互不影响)
- NESTED 嵌套事务,嵌套事务可以独立进行回滚不会影响外层事务,但是外层事务发生异常时子事务也会同步回滚
- NOT_SUPPORTED 如果当前存在物理事务则挂起当前事务,当前任务执行完后恢复事务
- SUPPORTS 如果当前存在物理事务则事务生效,否则就不做事务控制
- MANDATORY 如果物理事务不存在则抛出一个异常
- NEVER 如果当前已经存在物理事务则抛出异常
isolationenum: org.springframework.transaction.annotation.IsolationOptional isolation level. Applies only to propagation values of REQUIRED or REQUIRES_NEW.
事务隔离级别,默认值为DEFAULT
- DEFAULT 默认隔离级别(使用数据库默认的事务隔离级别 mysql 默认的是REPEATABLE_READ)
- READ_UNCOMMITTED 允许读取一个事务未提交的数据,可能会出现脏读、幻读、不可重复读
- READ_COMMITTED 禁止读取一个事务未提交的数据,可以避免脏读,但会出现幻读和不可重复读的问题
- REPEATABLE_READ 可重复读,可以避免脏读、解决不可重复读的问题。
- SERIALIZABLE 基本理解为不可并发,顺序执行。可以避免脏读、幻读,解决不可重复读的问题
timeoutint (in seconds of granularity)
单位秒
Optional transaction timeout. Applies only to propagation values of REQUIRED or REQUIRES_NEW.
只有事务传播级别为REQUIRED or REQUIRES_NEW.两种值是才生效。
readOnlybooleanRead-write versus read-only transaction. Only applicable to values of REQUIRED or REQUIRES_NEW.
只有事务传播级别为REQUIRED or REQUIRES_NEW.两种值是才生效。
一个布尔标志,它的默认值是flase。如果事务实际上是只读的,可以通过将其值设置为true,并且它允许在运行时进行相应的优化。当其值为false时,这只是作为实际事务子系统的提示;它并不一定会导致写入访问失败。事务管理器无法理解只读提示,所以当要求只读事务时,不引发异常,而是默默地忽略暗示。
rollbackForArray of Class objects, which must be derived from Throwable.Optional array of exception types that must cause rollback.
Class对象数组类型的属性,用于配置在触发那些异常类,可以配置某个超类,此时其包含其所有子类异常也会触发回滚
rollbackForClassNameArray of exception name patterns.Optional array of exception name patterns that must cause rollback.
String对象数组类型的属性,用于配置在触发那些异常类(可以配置绝对路径,也可以仅配置类名)。"Exception"可以匹配所有名称为"XXXExceptionXXX"的异常,有点类似于模糊匹配
noRollbackForArray of Class objects, which must be derived from Throwable.Optional array of exception types that must not cause rollback.
类似于rollbackFor属性,只是这部分配置的异常类不会触发回滚
noRollbackForClassNameArray of exception name patterns.Optional array of exception name patterns that must not cause rollback.
类似于rollbackForClassName属性,只是这部分配置的异常类不会触发回滚

在Spring管理的事务中,要注意物理事务和逻辑事务之间的差异,以及传播设置如何应用于这种差异。Spring @Transaction注解修饰方法、类首先会创建一个逻辑事务,但是事务实际生效需要对应绑定物理事务。
默认情况下,参与事务加入外部作用域的特征,静默地忽略本地隔离级别、超时值或只读标志(如果有的话)。如果您希望在参与具有不同隔离级别的现有事务时拒绝隔离级别声明,请考虑在事务管理器上将validateExistingTransactions标志切换为true。这种非宽松模式还拒绝只读不匹配(即,试图参与只读外部作用域的内部读写事务)。

事务的传播级别

REQUIRED

当使用PROPAGATION_REQUIRED的传播级别时,如果当前尚未存在物理事务,则必须针对当前的作用域创建一个物理事务,或者参与为更大作用域的现有“外部”物理事务。这是同一线程中常见调用堆栈安排的良好默认设置。
当传播设置为propagation_REQUIRED时,将为应用该设置的每个方法创建逻辑事务作用域。每个这样的逻辑事务作用域都可以单独确定仅回滚状态,外部事务作用域在逻辑上独立于内部事务作用域。在标准PROPAGATION_REQUIRED行为的情况下,所有这些作用域都映射到相同的物理事务。因此,内部事务范围中的仅回滚(rollback-only)标记确实会影响外部事务的实际提交机会。
然而,在内部事务范围设置仅回滚标记的情况下,外部事务尚未决定回滚本身,因此回滚(由内部事务范围自动触发)是意外的。此时会引发相应的UnexpectedRollbackException。这是预期的行为,因此永远不能误导事务的调用方假设提交是在实际未执行时执行的。因此,如果内部事务(外部调用者不知道)以静默方式将事务标记为仅回滚,则外部调用器仍然调用commit。外部调用程序需要接收UnexpectedRollbackException,以清楚地指示改为执行了回滚。

image.png

REQUIRES_NEW

与PROPAGATION_REQUIRES_NEW相反,PROPAGATION_REQUIRED始终为每个受影响的事务范围使用独立的物理事务,从不参与外部范围的现有事务。在这种安排中,底层资源事务是不同的,因此可以独立地提交或回滚,外部事务(在针对内层业务整体try catch 的前提下)不受内部事务的回滚状态的影响,并且内部事务的锁在完成后立即释放。这种独立的内部事务还可以声明自己的隔离级别、超时和只读设置,而不继承外部事务的特征。

image.png

NESTED

PROPAGATION_NESTED是在单个物理事务是具有多个可回滚到的保存点。这种部分回滚允许内部事务作用域触发其作用域的回滚,而外部事务(需要try catch子事务,避免异常传递至父层事务,如果没有,则也会引发父事务整体回滚)能够继续物理事务,尽管某些操作已回滚。此设置通常映射到JDBC保存点,因此它仅适用于JDBC资源事务。请参阅Spring的DataSourceTransactionManager。
在这里插入图片描述
在这里插入图片描述

NOT_SUPPORTED

如果当前存在事务则挂起当前物理事务,当前任务(不做事务控制的)执行完后恢复事务。如果需要不受内层业务方法报错的影响,外层业务需要针对内层部分业务整体try catch

在这里插入图片描述

SUPPORTS

如果当前存在物理事务则做事务生效,否则就不做事务控制。

备注:使用了事务注解并不代表实际存在物理事务,这种事务隔离传播级别在使用中极有可能因为理解偏差而出现预期之外的结果

MANDATORY

如果物理事务不存在则抛出一个异常

NEVER

如果物理事务存在则抛出一个异常

事务的隔离级别

脏读、幻读、可重复读概念介绍

脏读:指一个事务读取了一个未提交事务的数据
说明:事务1更新了记录,但没有提交,事务2读取了更新后的行,然后事务T1回滚,现在T2读取无效。

不可重复读:在一个事务内读取表中的某一行数据,多次读取结果不同.一个事务读取到了另一个事务提交后(update)的数据.
说明:事务1读取记录时,事务2更新了记录并提交,事务1再次读取时可以看到事务2修改后的记录;

虚读(幻读):在一个事务内读取了别的事务插入的数据,导致前后读取不一致(insert)
说明:事务1读取记录时事务2增加了记录并提交,事务1再次读取时可以看到事务2新增的记录;

rollback*部分说明

异常回滚部分支持配置一个异常类的列表(可以不额外做配置,框架填充默认值),但是这些异常类必须是Throwable的子类。默认情况下,事务会针对 RuntimeException和Error的异常来触发回滚操作,自定义的业务异常(Exception的子类)将不会触发异常

@Transaction注解rollback*属性只能针对Throwable的子类中的RuntimeException和Error两个异常类的子类生效

	@Override
	public boolean rollbackOn(Throwable ex) {
		return (ex instanceof RuntimeException || ex instanceof Error);
	}
	public RollbackRuleAttribute(Class<?> clazz) {
		Assert.notNull(clazz, "'clazz' cannot be null");
		if (!Throwable.class.isAssignableFrom(clazz)) {
			throw new IllegalArgumentException(
					"Cannot construct rollback rule from [" + clazz.getName() + "]: it's not a Throwable");
		}
		this.exceptionName = clazz.getName();
	}

具体列举一些事务组合使用场景(含多线程)

测试代码节选

package com.spring.learning.transaction.service.propagation;

/**
 * @description:
 * @author: tianyang
 * @date: 2023/9/3 13:49
 */
public interface InnerTransactionService {

    /**
     * 对应事务传播级别 REQUIRED
     *@time 2023/9/3 13:52
     */
    void requiredWithException(String propagationDesc);

    /**
     * 对应事务传播级别 REQUIRED
     *@time 2023/9/3 13:52
     */
    void requiredWithOutException(String propagationDesc);

    /**
     * 对应事务传播级别 REQUIRED_NEW
     *@time 2023/9/3 13:52
     */
    void requiredNewWithException(String propagationDesc);

    /**
     * 对应事务传播级别 REQUIRED_NEW
     *@time 2023/9/3 13:52
     */
    void requiredNewWithOutException(String propagationDesc);

    /**
     * 对应事务传播级别 NESTED
     *@time 2023/9/3 13:52
     */
    void nestedWithException(String propagationDesc);


    /**
     * 对应事务传播级别 NESTED
     *@time 2023/9/3 13:52
     */
    void nestedWithOutException(String propagationDesc);

    /**
     * 对应事务传播级别 NOT_SUPPORTED
     *@time 2023/9/3 13:52
     */
    void notSupportedWithException(String propagationDesc);

    /**
     * 对应事务传播级别 SUPPORTS
     *@time 2023/9/3 13:52
     */
    void supportsWithException(String propagationDesc);

    /**
     * 对应事务传播级别 MANDATORY
     *@time 2023/9/3 13:52
     */
    void mandatoryWithException(String propagationDesc);

    /**
     * 对应事务传播级别 never
     *@time 2023/9/3 13:52
     */
    void neverWithException(String propagationDesc);



}


    private void doDbProcess(@NonNull String propagation) {
        doDbProcess(Boolean.TRUE, propagation);
    }

    private void doDbProcess(@NonNull Boolean doThrowException, @NonNull String propagation) {
        try {
            //为了在包含内外任务的时候能分清楚谁先执行谁后执行
            Thread.sleep(1500L);
        } catch (InterruptedException e) {
        }
        Integer innerTransactionField = (int) (Math.random() * 50);
        LOGGER.info("当前待入库innerTransactionField={}", innerTransactionField);
        transactionTestMapper.insertSelective(TransactionTest.initInnerTransactionField(innerTransactionField, propagation));
        if (Boolean.TRUE.equals(doThrowException)) {
            throw new NullPointerException();
        }
    }


AsyncInnerTransactionServiceImpl 和 InnerTransactionServiceImpl 都实现了InnerTransactionService接口,代码实现几乎一致,区别就是前者整体是异步的,后者是同步的

**备注:**异步情况下,主线程需要运行足够长的时间,确保在子线程执行完之后再终止,否则可能出现子线程执行一半后进程终止,实际造成子线程因为执行时间问题无法走完,实际的结果和预期有偏差

DROP TABLE IF EXISTS `transaction_test`;
CREATE TABLE `transaction_test` (
   `transaction_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
   `outer_transaction_field` int DEFAULT NULL COMMENT '外层事务操作入库对应的插入值字段',
   `inner_transaction_field` int DEFAULT NULL COMMENT '内层事务操作入库对应的插入值字段',
   `propagation` VARCHAR(128) DEFAULT NULL COMMENT '事务类型说明',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
   PRIMARY KEY (`transaction_id`)
) ENGINE=InnoDB AUTO_INCREMENT=88888 DEFAULT CHARSET=utf8mb4 COMMENT='事务测试数据表';

多层事务情况下,内层事务抛出异常

各种事务传播级别下的场景列举

outerAndInnerDefaultRollbackAllForInnerException

在内外层事务均为默认事务传播级别情况下,

  • 内层事务为异步时,异步情况下实际是重新绑定了新的物理事务,因此内层事务的失败实际上不会造成外层事务的回滚
  • 内层事务为同步时,因为内层事务发生异常导致整体事务回滚

代码样例
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public void outerAndInnerDefaultRollbackAllForInnerException() {
    String propagationDesc = "outerAndInnerDefaultRollbackAllForInnerException parent " + Propagation.REQUIRED.name();
    if (Boolean.TRUE.equals(asyncFlag)) {
        propagationDesc = propagationDesc + " asyncInner";
    }
    doDbProcess(Boolean.FALSE, propagationDesc);
    if (Boolean.TRUE.equals(asyncFlag)) {
        asyncInner.requiredWithException(propagationDesc);
    } else {
        inner.requiredWithException(propagationDesc);
    }
}

业务日志
==================================异步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6573d2f7]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@58fef7f7] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 6343(Integer), outerAndInnerDefaultRollbackAllForInnerException parent REQUIRED asyncInner(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6573d2f7]
====================[外层物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6573d2f7]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6573d2f7]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6573d2f7]
Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1de9eda7]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@58fef7f7] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 201(Integer), outerAndInnerDefaultRollbackAllForInnerException parent REQUIRED asyncInner asyncInner REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1de9eda7]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1de9eda7]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1de9eda7]


==================================同步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6cf0a747] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 511(Integer), outerAndInnerDefaultRollbackAllForInnerException parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19]
====================[内层物理事务-绑定]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19] from current transaction
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 41(Integer), outerAndInnerDefaultRollbackAllForInnerException parent REQUIRED inner REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19]

java.lang.NullPointerException
	at com.spring.learning.transaction.service.propagation.impl.InnerTransactionServiceImpl.doDbProcess(InnerTransactionServiceImpl.java:96)
	at com.spring.learning.transaction.service.propagation.impl.InnerTransactionServiceImpl.doDbProcess(InnerTransactionServiceImpl.java:84)
	

数据查询结果

image.png

outerDefaultInnerRequiresNewRollbackOuterForInnerException

在外层事务为默认事务传播级别,内层事务为REQUIRES_NEW事务传播级别(不管是同步还是异步,都会绑定不同的物理事务)情况下,

  • 内层事务为同步的情况下,因为内层事务的异常在未被trycatch的情况下会向上传递被带到外层事务,因此内层事务的异常将会影响外层事务,在内层事务发生异常的情况下外层事务也会回滚
  • 内层事务为异步的情况下,因为内层事务的异常将不会向上传递被带到父事务,因此内层事务的异常将不会影响外层事务,外层事务可在子事务发生异常的情况下正常提交

代码样例
 @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
 @Override
 public void outerDefaultInnerRequiresNewRollbackOuterForInnerException() {
     String propagationDesc = "outerDefaultInnerRequiresNewRollbackOuterForInnerException parent " + Propagation.REQUIRED.name();
     if (Boolean.TRUE.equals(asyncFlag)) {
         propagationDesc = propagationDesc + " asyncInner";
     }
     doDbProcess(Boolean.FALSE, propagationDesc);
     if (Boolean.TRUE.equals(asyncFlag)) {
         asyncInner.requiredNewWithException(propagationDesc);
     } else {
         inner.requiredNewWithException(propagationDesc);
     }
 }

业务日志
==================================异步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6caf7803]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@117d32e] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 750(Integer), outerDefaultInnerRequiresNewRollbackOuterForInnerException parent REQUIRED asyncInner(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6caf7803]
====================[外层物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6caf7803]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6caf7803]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6caf7803]
Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@32b2f088]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@384a9bf8] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 161(Integer), outerDefaultInnerRequiresNewRollbackOuterForInnerException parent REQUIRED asyncInner asyncInner REQUIRES_NEW(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@32b2f088]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@32b2f088]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@32b2f088]
[09:49:30.801][ERROR][pid = 21352][thread = async-transaction-Executor-1][o.s.a.i.SimpleAsyncUncaughtExceptionHandler:handleUncaughtException:39] Unexpected exception occurred invoking async method: public void com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.requiredNewWithException(java.lang.String)
java.lang.NullPointerException: null
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.doDbProcess(AsyncInnerTransactionServiceImpl.java:100) ~[classes/:?]
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.doDbProcess(AsyncInnerTransactionServiceImpl.java:87) ~[classes/:?]
[09:49:34.251][ERROR][pid = 21352][thread = main][c.s.l.t.s.p.i.CombineTransactionServiceImplTest:holdMainThreadUntilAsyncRun:140] 主线程执行完毕



==================================同步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3c3c4a71] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 4255(Integer), outerDefaultInnerRequiresNewRollbackOuterForInnerException parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
Transaction synchronization suspending SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@34780cd9]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7c8c70d6] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 45(Integer), outerDefaultInnerRequiresNewRollbackOuterForInnerException parent REQUIRED inner REQUIRES_NEW(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@34780cd9]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@34780cd9]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@34780cd9]
Transaction synchronization resuming SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]

java.lang.NullPointerException
	at com.spring.learning.transaction.service.propagation.impl.InnerTransactionServiceImpl.doDbProcess(InnerTransactionServiceImpl.java:97)
	at com.spring.learning.transaction.service.propagation.impl.InnerTransactionServiceImpl.doDbProcess(InnerTransactionServiceImpl.java:84)

数据查询结果

image.png

outerDefaultInnerNestedRollbackForInnerExceptionWhileTryCatch

在外层事务为默认事务传播级别,内层事务为NESTED事务传播级别情况下:

  • 内层为同步的情况下,虽然内外层事务绑定了同一个物理事务,但是由于内层事务(隔离级别为NESTED),其发生异常导致子事务事务独立回滚,外层事务针对内层事务整体做了try catch 处理,此时内层事务的异常将不影响外层事务提交
  • 内层事务为同步的情况下,内层事务发生异常导致子事务事务回滚,外层事务未针对内层事务整体做了try catch 处理,此时内层事务的异常将影响外层事务提交,导致整体回滚
  • 内层事务为异步的情况下,因为内层事务的都会绑定不同的物理事务,因此内层事务的异常将不会影响外层事务,外层事务可在内层事务发生异常的情况下正常提交

代码样例
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public void outerDefaultInnerNestedRollbackForInnerExceptionWhileTryCatch() {
    String propagationDesc = "outerDefaultInnerNestedRollbackForInnerExceptionWhileTryCatch parent " + Propagation.REQUIRED.name();
    if (Boolean.TRUE.equals(asyncFlag)) {
        propagationDesc = propagationDesc + " asyncInner";
    }
    doDbProcess(Boolean.FALSE,propagationDesc);
    try {
        if (Boolean.TRUE.equals(asyncFlag)) {
            asyncInner.nestedWithException(propagationDesc);
        } else {
            inner.nestedWithException(propagationDesc);
        }
    } catch (Exception e) {
       LOGGER.error("捕获内层事务抛出的异常,避免异常造成外部事务的回滚");
    }
}

业务日志
==================================异步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1716e8c5]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7e53339] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 3840(Integer), outerDefaultInnerNestedRollbackForInnerExceptionWhileTryCatch parent REQUIRED asyncInner(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1716e8c5]
====================[外层物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1716e8c5]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1716e8c5]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1716e8c5]
Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bc0ba7]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7e53339] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 165(Integer), outerDefaultInnerNestedRollbackForInnerExceptionWhileTryCatch parent REQUIRED asyncInner asyncInner NESTED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bc0ba7]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bc0ba7]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61bc0ba7]
[14:11:06.647][ERROR][pid = 19512][thread = async-transaction-Executor-1][o.s.a.i.SimpleAsyncUncaughtExceptionHandler:handleUncaughtException:39] Unexpected exception occurred invoking async method: public void com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.nestedWithException(java.lang.String)
java.lang.NullPointerException: null
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.doDbProcess(AsyncInnerTransactionServiceImpl.java:100) ~[classes/:?]
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.doDbProcess(AsyncInnerTransactionServiceImpl.java:87) ~[classes/:?]
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.nestedWithException(AsyncInnerTransactionServiceImpl.java:53) ~[classes/:?]
[14:11:10.134][ERROR][pid = 19512][thread = main][c.s.l.t.s.p.i.CombineTransactionServiceImplTest:holdMainThreadUntilAsyncRun:140] 主线程执行完毕


==================================同步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1352434e] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 1094(Integer), outerDefaultInnerNestedRollbackForInnerExceptionWhileTryCatch parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
====================[内层物理事务-绑定(和外层事务绑定同一个物理事务)]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd] from current transaction
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 31(Integer), outerDefaultInnerNestedRollbackForInnerExceptionWhileTryCatch parent REQUIRED inner NESTED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
[14:16:24.298][ERROR][pid = 23656][thread = main][c.s.l.t.s.p.i.CombineTransactionServiceImpl:outerDefaultInnerNestedRollbackForInnerExceptionWhileTryCatch:121] 捕获内层事务抛出的异常,避免异常造成外部事务的回滚
====================[物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]

数据查询结果

image.png

outerDefaultInnerNotSupportedNotRollbackOuterForInnerExceptionWhileTryCatch

在外层事务为默认事务传播级别,内层事务为NOT_SUPPORTED事务传播级别情况下:

  • 内层事务为同步的情况下,内层事务由于事务未启用,发生异常内层事务不回滚,外层事务针对内层事务整体做了try catch 处理,此时内层事务的异常将不影响外层事务提交
  • 内层事务为同步的情况下,内层事务由于事务未启用,发生异常内层事务不回滚,外层事务针对内层事务整体做了try catch 处理,此时内层事务的异常将影响外层事务提交,导致整体回滚
  • 内层事务为异步的情况下,内层事务由于事务未启用,发生异常内层事务不回滚,因为内层事务的都会绑定不同的物理事务,因此内层事务的异常将不会影响外层事务,父事务可在内层事务发生异常的情况下正常提交

代码样例
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public void outerDefaultInnerNotSupportedNotRollbackOuterForInnerExceptionWhileTryCatch() {
    String propagationDesc = "outerDefaultInnerNotSupportedNotRollbackOuterForInnerExceptionWhileTryCatch parent " + Propagation.REQUIRED.name();
    if (Boolean.TRUE.equals(asyncFlag)) {
        propagationDesc = propagationDesc + " asyncInner";
    }
    doDbProcess(Boolean.FALSE, propagationDesc);
    try {
        if (Boolean.TRUE.equals(asyncFlag)) {
            asyncInner.notSupportedWithException(propagationDesc);
        } else {
            inner.notSupportedWithException(propagationDesc);
        }
    } catch (Exception e) {
        LOGGER.error("捕获内层事务抛出的异常,避免异常造成外部事务的回滚");
    }
}

业务日志
==================================异步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb2d5e]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@21505815] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 8678(Integer), outerDefaultInnerNotSupportedNotRollbackOuterForInnerExceptionWhileTryCatch parent REQUIRED asyncInner(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb2d5e]
====================[外层物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb2d5e]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb2d5e]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb2d5e]
Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@596cb858]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@21505815] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 219(Integer), outerDefaultInnerNotSupportedNotRollbackOuterForInnerExceptionWhileTryCatch parent REQUIRED asyncInner asyncInner NOT_SUPPORTED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@596cb858]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@596cb858]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@596cb858]
[14:27:25.818][ERROR][pid = 484][thread = async-transaction-Executor-1][o.s.a.i.SimpleAsyncUncaughtExceptionHandler:handleUncaughtException:39] Unexpected exception occurred invoking async method: public void com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.notSupportedWithException(java.lang.String)
java.lang.NullPointerException: null
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.doDbProcess(AsyncInnerTransactionServiceImpl.java:100) ~[classes/:?]
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.doDbProcess(AsyncInnerTransactionServiceImpl.java:87) ~[classes/:?]
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.notSupportedWithException(AsyncInnerTransactionServiceImpl.java:65) ~[classes/:?]
[14:27:29.294][ERROR][pid = 484][thread = main][c.s.l.t.s.p.i.CombineTransactionServiceImplTest:holdMainThreadUntilAsyncRun:140] 主线程执行完毕


==================================同步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1352434e] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 3907(Integer), outerDefaultInnerNotSupportedNotRollbackOuterForInnerExceptionWhileTryCatch parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
====================[外层物理事务-挂起]
Transaction synchronization suspending SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@623ebac7]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@66b59b7d] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 42(Integer), outerDefaultInnerNotSupportedNotRollbackOuterForInnerExceptionWhileTryCatch parent REQUIRED inner NOT_SUPPORTED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@623ebac7]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@623ebac7]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@623ebac7]
====================[外层物理事务-重新开始]
Transaction synchronization resuming SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
[14:22:00.679][ERROR][pid = 8124][thread = main][c.s.l.t.s.p.i.CombineTransactionServiceImpl:outerDefaultInnerNotSupportedNotRollbackOuterForInnerExceptionWhileTryCatch:154] 捕获内层事务抛出的异常,避免异常造成外部事务的回滚
====================[外层物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]

数据查询结果

image.png

outerDefaultInnerSupportsNotRollbackOuterForInnerExceptionWhileTryCatch

在外层事务为默认事务传播级别,内层事务为SUPPORTS事务传播级别(仅有当当前已经存在物理事务的情况下才会使按照事务的机制走)情况下:

  • 内层事务为同步的情况下,内层事务和外层事务对应同一个物理事务,外层事务针对内层事务整体做了try catch 处理,但是由于内外层绑定的是同一个物理事务,导致整体回滚
  • 内层事务为同步的情况下,内层事务和外层事务对应同一个物理事务,外层事务未针对内层事务整体做了try catch 处理,此时内层事务的异常将影响外层事务提交,导致整体回滚
  • 内层事务为异步的情况下,内层事务执行时未存在已有的物理事务,实际不走事务,因此内层事务的异常将不会影响外层事务,父事务可在内层事务发生异常的情况下正常提交

代码样例
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public void outerDefaultInnerSupportsNotRollbackOuterForInnerExceptionWhileTryCatch() {
    String propagationDesc = "outerDefaultInnerSupportsNotRollbackOuterForInnerExceptionWhileTryCatch parent " + Propagation.REQUIRED.name();
    if (Boolean.TRUE.equals(asyncFlag)) {
        propagationDesc = propagationDesc + " asyncInner";
    }
    doDbProcess(Boolean.FALSE,propagationDesc);
    try {
        if (Boolean.TRUE.equals(asyncFlag)) {
            asyncInner.supportsWithException(propagationDesc);
        } else {
            inner.supportsWithException(propagationDesc);
        }
    } catch (Exception e) {
        LOGGER.error("捕获内层事务抛出的异常,避免异常造成外部事务的回滚");
    }
}

业务日志
==================================异步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1352434e] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 4122(Integer), outerDefaultInnerSupportsNotRollbackOuterForInnerExceptionWhileTryCatch parent REQUIRED asyncInner(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
====================[外层物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@437486cd]
Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e2bbd4f]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1352434e] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 262(Integer), outerDefaultInnerSupportsNotRollbackOuterForInnerExceptionWhileTryCatch parent REQUIRED asyncInner asyncInner SUPPORTS(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e2bbd4f]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e2bbd4f]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e2bbd4f]
[10:31:29.374][ERROR][pid = 25500][thread = async-transaction-Executor-1][o.s.a.i.SimpleAsyncUncaughtExceptionHandler:handleUncaughtException:39] Unexpected exception occurred invoking async method: public void com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.supportsWithException(java.lang.String)
java.lang.NullPointerException: null
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.doDbProcess(AsyncInnerTransactionServiceImpl.java:100) ~[classes/:?]
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.doDbProcess(AsyncInnerTransactionServiceImpl.java:87) ~[classes/:?]
	at com.spring.learning.transaction.service.propagation.impl.AsyncInnerTransactionServiceImpl.supportsWithException(AsyncInnerTransactionServiceImpl.java:71) ~[classes/:?]
[10:31:32.814][ERROR][pid = 25500][thread = main][c.s.l.t.s.p.i.CombineTransactionServiceImplTest:holdMainThreadUntilAsyncRun:140] 主线程执行完毕



==================================同步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3c3c4a71] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 9346(Integer), outerDefaultInnerSupportsNotRollbackOuterForInnerExceptionWhileTryCatch parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
====================[内层物理事务-绑定(对应同一个物理事务)]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d] from current transaction
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 43(Integer), outerDefaultInnerSupportsNotRollbackOuterForInnerExceptionWhileTryCatch parent REQUIRED inner SUPPORTS(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
[10:34:24.625][ERROR][pid = 20856][thread = main][c.s.l.t.s.p.i.CombineTransactionServiceImpl:outerDefaultInnerSupportsNotRollbackOuterForInnerExceptionWhileTryCatch:202] 捕获内层事务抛出的异常,避免异常造成外部事务的回滚
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
====================[物理事务(整体)-回滚]
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

	at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:870)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:707)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:633)


数据查询结果

image.png

常见问题(父子事务同步回滚)

这种使用场景实际基本不会出现,因为异步方案就是为了解决主线程不必为了等待复杂的子线程执行完毕才返回响应,如果需要控制父子线程中的事务同时做回滚,那实际就是破坏了异步使用的意义。

问题描述:

由于多线程情况下默认父子线程见不会公用同一个数据库连接,并且常规的线程启动方式一般线程执行的方法不会返回任何信息。这种场景下导致事务无法一致的原因有两个:

  • 由于子线程和主线程的事务对应绑定了不同的物理事务
  • 子线程中发生异常时无法将信息传递到父线程中

解决方法:

利用Callable机制

使用开启Callable机制的线程,不能简单的使用@Async注解或者使用Thread的方式来开启线程,需要能显式的拿到线程执行返回结果
线程的实现有多种类方式,只有实现Callable接口的线程才支持在主线程中获取其结果(包括抛出的异常)

  • 继承Thread类
  • 实现Runable接口
  • 实现Callable 接口
@Transactional(rollbackFor = Exception.class)
@Override
public void transactionWithCallableService() {
    String propagationDesc = "transactionWithCallableService parent sync " + Propagation.REQUIRED.name();
    doParentDbProcess(propagationDesc);
    try {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        //父子线程均开启事务的写法
        Future<String> future = executor.submit(() -> inner.requiredWithException(propagationDesc));
        do {
            future.get();
        } while (future.isDone());
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

多层事务情况下,外层事务抛出异常

各种事务传播级别下的场景列举

outerAndInnerDefaultRollbackAllForOuterException

在内外层事务均为默认事务传播级别情况下,

  • 内层事务为异步时,异步情况下实际是重新绑定了新的物理事务,因此内层事务的成功后外层事务的回滚对其不产生影响
  • 内层事务为同步时,因为外层事务发生异常导致整体事务回滚

代码样例
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public void outerAndInnerDefaultRollbackAllForOuterException() {
    String propagationDesc = "outerAndInnerDefaultRollbackAllForOuterException parent " + Propagation.REQUIRED.name();
    if (Boolean.TRUE.equals(asyncFlag)) {
        asyncInner.requiredWithOutException(propagationDesc);
    } else {
        inner.requiredWithOutException(propagationDesc);
    }
    doDbProcess(propagationDesc);
}

业务日志
==================================异步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1716e8c5]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7e53339] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 759(Integer), outerAndInnerDefaultRollbackAllForOuterException parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1716e8c5]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1716e8c5]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1716e8c5]

java.lang.NullPointerException
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:248)
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:240)

Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7df7886d]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@770fe31d] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 181(Integer), outerAndInnerDefaultRollbackAllForOuterException parent REQUIRED asyncInner REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7df7886d]
====================[内层物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7df7886d]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7df7886d]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7df7886d]


==================================同步情况下=============================================================
Creating a new SqlSession
===================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6cf0a747] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 40(Integer), outerAndInnerDefaultRollbackAllForOuterException parent REQUIRED inner REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19]
====================[内层物理事务-绑定]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19] from current transaction
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 1711(Integer), outerAndInnerDefaultRollbackAllForOuterException parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d44a19]

java.lang.NullPointerException
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:248)
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:240)

数据查询结果

image.png

outerDefaultInnerRequiresNewRollbackOuterForOuterException

在外层事务为默认事务传播级别,内层事务为REQUIRES_NEW事务传播级别情况下,

  • 内层事务传播隔离级别为REQUIRES_NEW,不管是同步还是异步,都会绑定不同的物理事务,因此外层事务的异常将不会影响子事务,内层事务可在外层事务发生异常的情况下正常提交

代码样例
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public void outerDefaultInnerRequiresNewRollbackOuterForOuterException() {
    String propagationDesc = "outerDefaultInnerRequiresNewRollbackOuterForOuterException parent " + Propagation.REQUIRED.name();
    if (Boolean.TRUE.equals(asyncFlag)) {
        asyncInner.requiredNewWithOutException(propagationDesc);
    } else {
        inner.requiredNewWithOutException(propagationDesc);
    }
    doDbProcess(propagationDesc);
}

业务日志
==================================异步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3c3c4a71] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 4548(Integer), outerDefaultInnerRequiresNewRollbackOuterForOuterException parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38eb0f4d]

java.lang.NullPointerException
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:248)
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:240)

Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48211e57]
[15:22:11.673][INFO ][pid = 4576][thread = SpringContextShutdownHook][c.a.d.p.DruidDataSource:close:2138] {dataSource-1} closing ...
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@42d97527] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 129(Integer), outerDefaultInnerRequiresNewRollbackOuterForOuterException parent REQUIRED asyncInner REQUIRES_NEW(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48211e57]
====================[内层物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48211e57]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48211e57]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@48211e57]


==================================同步情况下=============================================================
Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6caf7803]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@117d32e] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 34(Integer), outerDefaultInnerRequiresNewRollbackOuterForOuterException parent REQUIRED inner REQUIRES_NEW(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6caf7803]
====================[内层物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6caf7803]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6caf7803]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6caf7803]
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1a22e0ef]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3b920bdc] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 8490(Integer), outerDefaultInnerRequiresNewRollbackOuterForOuterException parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1a22e0ef]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1a22e0ef]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1a22e0ef]

java.lang.NullPointerException
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:248)
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:240)

数据查询结果

image.png

outerDefaultInnerNestedRollbackForOuterException

在外层事务为默认事务传播级别,内层事务为NESTED事务传播级别情况下:

  • 内层事务为同步的情况下,外层事务发生异常导致整体事务回滚
  • 内层事务为异步的情况下,因为内层事务的都会绑定不同的物理事务,因此外层事务的异常将不会影响内层事务,内层事务可在外层事务发生异常的情况下正常提交

代码样例
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    @Override
    public void outerDefaultInnerNestedRollbackForOuterException() {
        String propagationDesc = "outerDefaultInnerNestedRollbackForOuterException parent " + Propagation.REQUIRED.name();
        if (Boolean.TRUE.equals(asyncFlag)) {
            asyncInner.nestedWithOutException(propagationDesc);
        } else {
            inner.nestedWithOutException(propagationDesc);
        }
        doDbProcess(propagationDesc);
    }

业务日志
==================================异步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@56dfab87]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@74170687] will be managed by Spring
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 2401(Integer), outerDefaultInnerNestedRollbackForOuterException parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@56dfab87]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@56dfab87]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@56dfab87]

java.lang.NullPointerException
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:248)
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:240)
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.outerDefaultInnerNestedRollbackForOuterException(CombineTransactionServiceImpl.java:103)

Creating a new SqlSession
====================[内层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1b536c9]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@b4d5db9] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 258(Integer), outerDefaultInnerNestedRollbackForOuterException parent REQUIRED asyncInner NESTED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1b536c9]
====================[内层物理事务-提交]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1b536c9]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1b536c9]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1b536c9]



==================================同步情况下=============================================================
Creating a new SqlSession
====================[外层物理事务-绑定]
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@709ed6f3]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@51d387d3] will be managed by Spring
==>  Preparing: insert into transaction_test ( inner_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 30(Integer), outerDefaultInnerNestedRollbackForOuterException parent REQUIRED inner NESTED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@709ed6f3]
====================[内层物理事务-绑定(和外层事务绑定同一个物理事务)]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@709ed6f3] from current transaction
==>  Preparing: insert into transaction_test ( outer_transaction_field, propagation ) values ( ?, ? )
==> Parameters: 6659(Integer), outerDefaultInnerNestedRollbackForOuterException parent REQUIRED(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@709ed6f3]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@709ed6f3]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@709ed6f3]

java.lang.NullPointerException
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:248)
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.doDbProcess(CombineTransactionServiceImpl.java:240)
	at com.spring.learning.transaction.service.propagation.impl.CombineTransactionServiceImpl.outerDefaultInnerNestedRollbackForOuterException(CombineTransactionServiceImpl.java:103)

数据查询结果

image.png

其他特殊使用场景

子线程需要在主线程提交之后开始执行

问题描述:

主线程开启了事务, 子线程需要获取到基于主线程执行提交之后的数据。多线程情况下由于父子线程不会使用同一个数据库连接,因此实际上主线程未提交的信息子线程是无法查到的。但常规情况下子线程的执行时机是不可控的,无法保障它一定是在父线程的相关代码执行以后。因此一定程度上是存在冲突的,需要控制子线程在父线程提交之后执行。

解决方案

方法一:业务层面增加补偿处理(不建议)

子线程执行之前做一些必要的数据检查,确保所需的数据存在之后才能进行如实际执行的逻辑里,否则就需要一直循环判断

方法二:编程式事务来显示控制执行顺序
//
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
    @Override
    public void afterCommit() {
        //实际子线程需要执行的方法(此处可以是一个异步方法)
    }
});

事务失效的几种场景

在这里插入图片描述


protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
}

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值