事务与数据库连接

March 02, 2025 / Administrator / 15阅读 / 0评论/ 分类: Transaction

事务 & 数据库连接的线程绑定机制

一、核心实现原理

Spring通过TransactionSynchronizationManager类(内部使用ThreadLocal)实现数据库连接的线程绑定,具体流程发生在事务管理器DataSourceTransactionManagerdoBegin()方法中


二、源码解析

1. 事务拦截器入口(TransactionInterceptor)

java

public class TransactionInterceptor extends TransactionAspectSupport {
    public Object invoke(MethodInvocation invocation) {
        return invokeWithinTransaction(...); // 触发事务处理链
    }
}

在父类TransactionAspectSupport中,通过invokeWithinTransaction方法创建事务上下文

2. 事务绑定线程的关键代码

DataSourceTransactionManagerdoBegin()方法中:

protected void doBegin(Object transaction, TransactionDefinition definition) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        Connection con = null;

        try {
            if (!txObject.hasConnectionHolder() ||
                    txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                Connection newCon = obtainDataSource().getConnection();
                if (logger.isDebugEnabled()) {
                    logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
                }
                // 创建一个ConnectionHolder,然后将Connection放到ConnectionHolder中,最后将ConnectionHolder保存到txObject中
                txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
            }

            txObject.getConnectionHolder().setSynchronizedWithTransaction(true);

            // Bind the connection holder to the thread.
            if (txObject.isNewConnectionHolder()) {
                // 将每个datasource对应的ConnectionHolder,绑定到threadLocal中
                TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
            }
        }

        catch (Throwable ex) {
            if (txObject.isNewConnectionHolder()) {
                DataSourceUtils.releaseConnection(con, obtainDataSource());
                txObject.setConnectionHolder(null, false);
            }
            throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
        }
    }

这里通过TransactionSynchronizationManagerConnectionHolder(包含物理连接)绑定到当前线程的ThreadLocal变量中

// TransactionSynchronizationManager#bindResource
// Key为DataSource对象,Value为ConnectionHolder对象
public static void bindResource(Object key, Object value) throws IllegalStateException {
        Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
        Assert.notNull(value, "Value must not be null");
        Map<Object, Object> map = resources.get();
        // set ThreadLocal Map if none found
        if (map == null) {
            map = new HashMap<>();
            resources.set(map);
        }
        Object oldValue = map.put(actualKey, value);
        // Transparently suppress a ResourceHolder that was marked as void...
        if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
            oldValue = null;
        }
        if (oldValue != null) {
            throw new IllegalStateException(
                    "Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread");
        }
    }

上面的resources就是一个ThreadLocal变量。

通过Map结构存储多个数据源的连接,Key为DataSource对象,Value为ConnectionHolder对象

private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");

三、连接获取流程

当执行SQL时,MyBatis/Spring JDBC会通过以下路径获取连接:

java

// 通过DataSourceUtils获取连接
Connection con = DataSourceUtils.getConnection(dataSource);

// 内部实现逻辑
public static Connection doGetConnection(DataSource dataSource) {
    ConnectionHolder conHolder = (ConnectionHolder) 
        TransactionSynchronizationManager.getResource(dataSource);
    return conHolder.getConnection();
}

这里直接从当前线程的ThreadLocal中获取已绑定的连接

上面,我们说了,每个数据源(datasource),都会有对应的独立的数据库连接(connection),那在多数据源场景中,我们在事务中,是如何判断,具体用哪一个数据库连接的呢?

四、多数据源场景的特殊处理

在多数据源配置中,Spring通过AbstractRoutingDataSource动态路由数据源。其核心方法:

java

public abstract class AbstractRoutingDataSource extends DataSource {
    protected DataSource determineTargetDataSource() {
        Object lookupKey = determineCurrentLookupKey(); // 从ThreadLocal获取数据源标识
        return getResolvedDataSources().get(lookupKey);
    }
}

通过上面的方法,我们就知道了具体使用哪个数据源(datasource),进而,我们通过TransactionSynchronizationManager,就能找到该datasource对应的connection,从而确定,在事务中,具体使用哪一个数据库连接了。

五 、事务和数据库连接之间的关系,为什么要引入第三者threadlocal呢

上面,我们发现,数据库连接(connection),是存储到threadlocal中的。为什么,不直接将connection,存储到事物对象中呢(DataSourceTransactionObject)?

其实,如果是单纯的一个事务,直接将connection,存储到事物对象中(DataSourceTransactionObject),是没有任何问题的。

但是,我们需要考虑一个嵌套事务的场景:

事务具有ACID特性,要求一起提交,或者一起回滚。

如果开启事务A,使用数据库连接1,紧接着,嵌套开始事务B(是required,不是required_new),使用数据库连接2,事务B需要加入到事务A

如果此时事务B中要执行commit,因为事务B使用的是数据库连接2,所以,直接在数据库层面完成了提交,但是接下来在事务A中,突然中断了,导致事务A要求回滚,数据库连接1就执行了回滚

这就导致了事务的不一致性,因为数据库连接2完成了提交,而数据库连接1完成了回滚。但是,我们从逻辑上看,事务B加入了事务A,所以应该是一个事务,不应该既回滚又提交。

为了解决这个问题,我们就需要在开始事务B的时候,持有和事务A相同的数据库连接才行,

那要想实现这个目的(两个事物持有相同的数据库连接),最好的方式,就是将数据库连接存储到threadlocal中,这样就不用在代码中,显示的传递数据库连接了。

因此,事务和数据库连接,是直接相关的,而threadlocal属于第三者。但是这个第三者又是必须的,特别是在嵌套事务的场景中。

多角度看问题

站在事务的角度:

  • 在一个事务中,事务的有效管控范围,是当前这个线程干的活,如果使用了其他线程,那么这个事务,是无法管控其他线程干的活

  • 在一个事务,不管执行了多少个增删改查,使用的都是同一个数据库连接。
    因为在事务刚开启时,这个数据库连接,就被设置到了ThreadLocal中,也即这个数据库连接,存储到了当前线程的上下文中了
    后续,在获取数据库连接时,都是直接从当前线程的上下文中,获取存储的数据库连接

疑问:如果存在了嵌套事务,事务A 嵌套事物B,如果事物B发起commit了,此时是真的在数据库层提交了吗?还是说,要等到事务A也发起commit,才会真正在数据库层,进行提交?

如果事务B发起了commit,但是后面事务A,又发起了rollback,那么会出现什么问题?springboot是怎么处理这种情况的?

文章作者:Administrator

文章链接:http://localhost:8090//archives/1740899098089

版权声明:本博客所有文章除特别声明外,均采用CC BY-NC-SA 4.0 许可协议,转载请注明出处!


评论