spring中父子线程共享事务

一、原生jdbc的父子线程共用事务

初始化数据库中的数据
在这里插入图片描述

1、单线程事务提交

private static void t1() throws Exception {
    Connection conn = getConn();
    PreparedStatement stmt1 = null;
    PreparedStatement stmt2 = null;
    try {
        // 1. 禁用自动提交,开启事务
        conn.setAutoCommit(false);

        // 2. 执行多个数据库操作
        String sql1 = "UPDATE student SET age = age - 1 WHERE id = ?";
        stmt1 = conn.prepareStatement(sql1);
        stmt1.setInt(1, 1);
        stmt1.executeUpdate();

        String sql2 = "UPDATE student SET age = age + 1 WHERE id = ?";
        stmt2 = conn.prepareStatement(sql2);
        stmt2.setInt(1, 2);
        stmt2.executeUpdate();

        // 3. 事务回滚, 将会回滚两个执行语句的内容
        conn.rollback();
        
        // 4. 提交事务
        conn.commit();
        System.out.println("Transaction committed successfully.");
    } catch (SQLException e) {
        // 发生异常时回滚事务
        if (conn != null) {
            try {
                conn.rollback();
                System.out.println("Transaction rolled back.");
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
        e.printStackTrace();
    } finally {
        close(stmt1, stmt2);
    }
}

获取数据库连接和释放连接

private static Connection getConn() throws Exception {
        String url = "jdbc:mysql://localhost:3306/test";
        String username = "root";
        String password = "123456";
        // 1. 获取数据库连接
        return DriverManager.getConnection(url, username, password);
    }

    private static void close(AutoCloseable... closeables) {
        for (AutoCloseable closeable : closeables) {
            if (closeable != null) {
                try {
                    closeable.close();
                } catch (Exception e) {
                    // ... do something
                    e.printStackTrace();
                }
            }
        }
    }

执行完后, 数据库中数据如下, 可以看到两条语句都执行成功
在这里插入图片描述

2、父子线程提交事务

还原数据库中的数据如下

在这里插入图片描述

private static void t2() throws Exception {
        Connection conn = getConn();
        PreparedStatement stmt1 = null;
        try {
            // 1. 禁用自动提交,开启事务
            conn.setAutoCommit(false);

            // 2. 父线程执行
            String sql1 = "UPDATE student SET age = age - 1 WHERE id = ?";
            stmt1 = conn.prepareStatement(sql1);
            stmt1.setInt(1, 1);
            stmt1.executeUpdate();

            // 3. 子线程执行
            Thread t1 = new Thread(() -> {
                String sql2 = "UPDATE student SET age = age + 1 WHERE id = ?";
                try {
                    PreparedStatement stmt2 = conn.prepareStatement(sql2);
                    stmt2.setInt(1, 2);
                    stmt2.executeUpdate();
                    // 子线程回滚
                    conn.rollback();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            });
            t1.start();
            // 等t1执行完
            t1.join();

            // 4. 提交事务
            conn.commit();
            System.out.println("Transaction committed successfully.");
        } catch (SQLException e) {
            // 发生异常时回滚事务
            if (conn != null) {
                try {
                    conn.rollback();
                    System.out.println("Transaction rolled back.");
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            e.printStackTrace();
        } finally {
            close(stmt1);
        }
    }

执行之后看结果

在这里插入图片描述

age没有发生变化, 也就是说子线程回滚了, 主线程中的提交不会生效, 达到了异步任务中事务共享的效果。

二、jdbcTemplate实现父子共用事务

jdbcTemplateupdate方法为例, 看看内部实现

// ... 只看核心update方法
protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss)
			throws DataAccessException {
    return updateCount(execute(psc, ps -> {
        try {
            // 赋值
            if (pss != null) {
                pss.setValues(ps);
            }
            // 使用PreparedStatement执行
            int rows = ps.executeUpdate();
            if (logger.isTraceEnabled()) {
                logger.trace("SQL update affected " + rows + " rows");
            }
            return rows;
        }
        finally {
            // ...
        }
    }, true));
}

继续看execute

private <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action, boolean closeResources)
			throws DataAccessException {
		// 从数据源中获取一个链接
		Connection con = DataSourceUtils.getConnection(obtainDataSource());
		PreparedStatement ps = null;
		try {
            // ...省略部分代码
			// 执行
			T result = action.doInPreparedStatement(ps);
			return result;
		}
		catch (SQLException ex) {
			//...
		}
		finally {
            // 关闭资源
			if (closeResources) {
				if (psc instanceof ParameterDisposer) {
					((ParameterDisposer) psc).cleanupParameters();
				}
                // 关闭Statement
				JdbcUtils.closeStatement(ps);
                // 回收链接(连接池的场景下是对连接的引用数-1, 并不会直接释放连接)
				DataSourceUtils.releaseConnection(con, getDataSource());
			}
		}
	}

这里是从连接池中获取一个链接, 然后执行对应的db操作, 这里重点看一下 Connection con = DataSourceUtils.getConnection(obtainDataSource());

DataSourceUtils.getConnection的核心逻辑如下

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
		Assert.notNull(dataSource, "No DataSource specified");

    	// 上下文中获取当前数据源中的连接句柄
		ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    	// 1.连接句柄存在 并且 2(句柄中有链接 或者 是事务同步)
		if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
            // 引用数+1
			conHolder.requested();
			// 直接获取连接
			return conHolder.getConnection();
		}
		// 从数据源中获取连接
		Connection con = fetchConnection(dataSource);

    	// ... 省略事务同步处理逻辑

		return con;
	}

从这个方法可以知道两点

  1. 如果当前上下文中存在连接就用该连接
  2. 不存在上下文中的连接就从数据源中获取

清楚了这个逻辑后, 我们来构建多线性下使用事务的场景

@Autowired
private DataSource dataSource;

@Autowired
private JdbcTemplate jdbcTemplate;

private void jdbcTemplateTest() throws Exception {
    Connection con = DataSourceUtils.getConnection(dataSource);
    con.setAutoCommit(false);
    ConnectionHolder holderToUse = new ConnectionHolder(con);
    // 在jdbcTemplate执行update之前将数据库连接放到上下文中
    TransactionSynchronizationManager.bindResource(dataSource, holderToUse);

    String sql1 = "UPDATE student SET age = age - 1 WHERE id = ?";
    jdbcTemplate.update(sql1, "1");
    Thread t1 = new Thread(() -> {
        TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
        String sql2 = "UPDATE student SET age = age + 1 WHERE id = ?";
        jdbcTemplate.update(sql2, "2");
        try {
            con.rollback();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    });
    t1.start();
    t1.join();
    con.commit();
    con.setAutoCommit(true);
}

注意这里需要在jdbcTemplate执行update之前调用bindResource操作, 因为上面介绍的update里面会先从上下文中获取数据库连接, 在子线程中也需要执行bindResource将连接绑定到子线程中, 这样父子线程就用的同一个连接了。

注意这里是使用Connection直接调用的setAutoCommit控制手动提交, 所以用完后记得设置为自动提交

三、PlatformTransactionManager 管理事务下的父子线程共享事务

简单demo

先看下一般的单线程下的手动事务

@Autowired
private PlatformTransactionManager transactionManager;

@Autowired
private TransactionDefinition definition;

private void manualTx() {
    String sql1 = "UPDATE student SET age = age - 1 WHERE id = ?";
    // 开启手动事务
    TransactionStatus status = this.transactionManager.getTransaction(definition);
    try {
        jdbcTemplate.update(sql1, "1");
        throw new RuntimeException("错误了");
    } catch (Exception e) {
        this.transactionManager.rollback(status);
    }
    if (!status.isCompleted()) {
        this.transactionManager.commit(status);
    }
}

这里主要是使用getTransaction方法开启手动提交, 然后rollback方法回滚事务, commit方法提交事务

原理剖析

这里我们看看手动开启方法getTransaction

public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException {

		// Use defaults if no transaction definition given.
		TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

    	// 上下文中获取事务
		Object transaction = doGetTransaction();
		// 判断是否获取到了连接和并且事务由jdbc管理
		if (isExistingTransaction(transaction)) {
			// Existing transaction found -> check propagation behavior to find out how to behave.
			return handleExistingTransaction(def, transaction, debugEnabled);
		}

		// 以下是事务不存在的情况
    	// 判断传播机制; 这里是必须有事务的传播机制
		if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
			throw new IllegalTransactionStateException(
					"No existing transaction found for transaction marked with propagation 'mandatory'");
		}
    	// 需要事务的传播机制
		else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
				def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
				def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
			SuspendedResourcesHolder suspendedResources = suspend(null);
			// 创建新的事物
			try {
				return startTransaction(def, transaction, debugEnabled, suspendedResources);
			}
			catch (RuntimeException | Error ex) {
				resume(null, suspendedResources);
				throw ex;
			}
		}
		else {
			// 创建空事物
            // ....
		}
	}

核心逻辑

  1. 如果存在事务, 则使用当前事务
  2. 如果不存在事务, 并且隔离界别需要开启事务, 那么创建新事务

再跟一下doGetTransaction方法

protected Object doGetTransaction() {
    DataSourceTransactionObject txObject = new DataSourceTransactionObject();
    txObject.setSavepointAllowed(isNestedTransactionAllowed());
    ConnectionHolder conHolder =
            (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
    txObject.setConnectionHolder(conHolder, false);
    return txObject;
}

我们看到了上面也介绍过的从上下文中获取连接句柄的方法TransactionSynchronizationManager.getResource

继续看startTransaction方法, 创建新的事物

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
			boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    DefaultTransactionStatus status = newTransactionStatus(
            definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
    // 开启事务
    doBegin(transaction, definition);
    // 将事物相关信息放入到上下文中
    prepareSynchronization(status, definition);
    return status;
}

这里做了两件事: 1. 开启事务 2. 将事物相关信息放入到上下文中

先看一下开启事务doBegin方法

protected void doBegin(Object transaction, TransactionDefinition definition) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
		Connection con = null;
    try {
        // 1.上下文中没有连接 2.或者需要将事务资源绑定到上下文中
        if (!txObject.hasConnectionHolder() ||
                txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            // 创建新的数据库连接
            Connection newCon = obtainDataSource().getConnection();
            // 标记为新创建的数据库连接
            txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
        }
        // 设置为绑定资源到上下文
        txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
        con = txObject.getConnectionHolder().getConnection();

        // 隔离级别
        Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
        txObject.setPreviousIsolationLevel(previousIsolationLevel);
        txObject.setReadOnly(definition.isReadOnly());

        // 设置为手动提交
        if (con.getAutoCommit()) {
            txObject.setMustRestoreAutoCommit(true);
            con.setAutoCommit(false);
        }
        // 设置事务只读
        prepareTransactionalConnection(con, definition);
        // 标记事务激活状态
        txObject.getConnectionHolder().setTransactionActive(true);

        int timeout = determineTimeout(definition);
        if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
            txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
        }

        // 新的数据库连接, 就将连接绑定的上下文中
        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
        }
    }
}

方法小结

  1. 没事务就开启事务
  2. 设置事务为手动提交
  3. 将数据库连接放到上下文中TransactionSynchronizationManager.bindResource

咋们再来看看prepareSynchronization方法, 将事务信息放到上下文

protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {
    // 新事务打开了事务同步
    if (status.isNewSynchronization()) {
        // 事务激活状态
        TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction());
        // 隔离级别
        TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
                definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ?
                        definition.getIsolationLevel() : null);
        TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
        TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
        TransactionSynchronizationManager.initSynchronization();
    }
}

在传播机制是PROPAGATION_REQUIRED(有事务就用当前事务, 没有就新建)的场景下我们看一下handleExistingTransaction方法, 用来获取当前事务

// ... 省略传播机制判断的代码

if (isValidateExistingTransaction()) {
    // 非默认隔离级别, 校验隔离级别
    if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
        Integer currentIsolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
        if (currentIsolationLevel == null || currentIsolationLevel != definition.getIsolationLevel()) {
            // ...抛异常
        }
    }
    // 校验readOnly
    if (!definition.isReadOnly()) {
        if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
            // ...抛异常
        }
    }
}
// 将事务信息绑定到线程上下文
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);

总结一下transactionManager.getTransaction方法

  1. 从当前上下文获取数据库连接句柄TransactionSynchronizationManager.getResource
  2. 如果存在连接和事务
  • 直接使用当前事务
  • 校验事务的传播机制, 是否只读
  • 将事务信息放到上下文
  1. 如果当前不存在事务(PROPAGATION_REQUIRED场景下)
  • 创建新的数据库连接
  • 设置为手动提交
  • 绑定数据库连接句柄到上下文TransactionSynchronizationManager.bindResource
  • 将事务信息放到上下文

关于TransactionStatuscommit方法和rollback方法我们就不看了, 它里面都校验了事务是否已经激活事务同步, 可以使用TransactionSynchronizationManager.initSynchronization方法来激活

有了以上信息支撑后, 我们再来看PlatformTransactionManager下的父子线程共享事务

我们只需要将父线程中的数据库句柄放到子线程, 并且设置传播机制, 是否只读, 激活事务同步就可以了;

PlatformTransactionManager下的父子线程使用同一个事务的demo

private void manualMultiTx() throws Exception {
        String sql1 = "UPDATE student SET age = age - 1 WHERE id = ?";

        // 开启手动事务
        TransactionStatus status = this.transactionManager.getTransaction(definition);
    	// 获取父线程中绑定的数据库连接句柄
        ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);

        // 因为getTransaction已经将ConnectionHolder绑定到当前线程(TransactionSynchronizationManager.bindResource),所以这里会直接拿这个连接
        jdbcTemplate.update(sql1, "1");
        Thread t1 = new Thread(() -> {
            /******************** 核心步骤start ***************/
            // 将ConnectionHolder绑定到当前线程
            TransactionSynchronizationManager.bindResource(dataSource, connectionHolder);
            // 将事务相关信息绑定到现成上下文
            // 设置隔离级别
 TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(definition.getIsolationLevel());
            // 设置是否只读
            TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
            // 激活事务同步
            TransactionSynchronizationManager.initSynchronization();
			/******************** 核心步骤end ***************/
            
            System.out.println("线程1执行");
            jdbcTemplate.update(sql1, "2");
            if (!status.isCompleted()) {
            	this.transactionManager.rollback(status);
            }
        });
        t1.start();
        t1.join();
        System.out.println("run线程执行");
        if (!status.isCompleted()) {
            this.transactionManager.commit(status);
        }
    }

其实这里的将事务相关信息绑定到现成上下文直接copy上面的prepareSynchronization方法的内容即可

改成这样更完整

/******************** 核心步骤start ***************/
TransactionSynchronizationManager.bindResource(dataSource, connectionHolder);
DefaultTransactionStatus s = (DefaultTransactionStatus) status;
// 事务激活状态
TransactionSynchronizationManager.setActualTransactionActive(s.hasTransaction());
// 隔离级别, 这里稍微不一样
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(definition.getIsolationLevel());
TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
TransactionSynchronizationManager.initSynchronization();
/******************** 核心步骤end ***************/

四、@Transactional注解下的父子线程事务共享

我们得看到@Transactional注解的增强类TransactionInterceptor中去;

在invoke中调用了invokeWithinTransaction方法, 我们关注一下

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

    // 事务属性
    TransactionAttributeSource tas = getTransactionAttributeSource();
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    // 事务管理器
    final TransactionManager tm = determineTransactionManager(txAttr);
    // ... 省略一些其它代码

    PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
    // 切点方法
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    // 这里是JdbcTransactionManager, 也是PlatformTransactionManager实现类
    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
        // 创建事务
        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

        Object retVal;
        try {
            // 调用目标方法
            retVal = invocation.proceedWithInvocation();
        }
        catch (Throwable ex) {
            // 这里会rollback
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        }

        // 这里会commit
        commitTransactionAfterReturning(txInfo);
        return retVal;
    }
    // ... 省略部分代码
}

这里我们只需要关注用来创建事务的createTransactionIfNecessary方法即可

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
			@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {

    // ... 省略部分代码
    TransactionStatus status = null;
    if (txAttr != null) {
        if (tm != null) {
            status = tm.getTransaction(txAttr);
        }
    }
    // 包装一下返回
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

回到了上面原理剖析中介绍的PlatformTransactionManager#getTransaction方法了, 也就是说它也会先从线程上下文中获取数据库连接句柄, 有的话直接用了, 没有就去创建然后放到当前上下文, 和上面介绍的第3节中的一样, 这里就不赘述了。

demo和上面的也基本没有区别

private void transactionalTest() throws Exception {

    // 开启手动事务
    TransactionStatus status = this.transactionManager.getTransaction(definition);
    ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);

    // 因为getTransaction已经将ConnectionHolder绑定到当前线程(TransactionSynchronizationManager.bindResource),所以这里会直接拿这个连接
	// 主线程中service调用
    myService.tranTest();
    Thread t1 = new Thread(() -> {
        // 将ConnectionHolder绑定到当前线程
        TransactionSynchronizationManager.bindResource(dataSource, connectionHolder);
        DefaultTransactionStatus s = (DefaultTransactionStatus) status;
        // 标记当前事务已存在
        TransactionSynchronizationManager.setActualTransactionActive(s.hasTransaction());
        // 设置隔离级别
   TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(definition.getIsolationLevel());
        // 设置是否只读
        TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
        TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
        TransactionSynchronizationManager.initSynchronization();

        System.out.println("线程1执行");
        // 子线程中service调用
        myService.tranTest();
        if (!status.isCompleted()) {
            this.transactionManager.rollback(status);
        }
    });
    t1.start();
    t1.join();
    System.out.println("run线程执行");
    if (!status.isCompleted()) {
        this.transactionManager.commit(status);
    }

}

service

@Component
public class MyService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void tranTest() {
        String sql1 = "UPDATE student SET age = age - 1 WHERE id = ?";
        jdbcTemplate.update(sql1, "2");
    }

}

个人公众号: 行云代码

五、总结

  1. 父子线程中事务共享的原理就是要让一个数据库连接在父子线程中同时使用
  2. 原生jdbc中, 将数据库连接直接从父线程传递到子线程即可
  3. jdbcTemplate的使用中, 只需将同一个数据库连接同时绑定到父子线程中接口, 使用TransactionSynchronizationManager.bindResource
  4. 在spring中使用PlatformTransactionManager开启手动事务, 除了第三点的需要将数据库连接同时在父线程中绑定transactionManager.getTransaction(definition)和在子线程中绑定TransactionSynchronizationManager.bindResource之外, 还需要将事务的相关信息绑定到子线程(详细见文章内部)
  5. 在spring中使用PlatformTransactionManager不管是配合jdbcTemplate还是调用@Transactional使用声明式原理都是一样的, 都是得从在子线程中绑定父线程中的相同的事务信息和连接信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

uncleqiao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值