事务与数据库连接
事务 & 数据库连接的线程绑定机制
一、核心实现原理
Spring通过TransactionSynchronizationManager类(内部使用ThreadLocal)实现数据库连接的线程绑定,具体流程发生在事务管理器DataSourceTransactionManager的doBegin()方法中
二、源码解析
1. 事务拦截器入口(TransactionInterceptor)
java
public class TransactionInterceptor extends TransactionAspectSupport {
public Object invoke(MethodInvocation invocation) {
return invokeWithinTransaction(...); // 触发事务处理链
}
}
在父类TransactionAspectSupport中,通过invokeWithinTransaction方法创建事务上下文
2. 事务绑定线程的关键代码
在DataSourceTransactionManager的doBegin()方法中:
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);
}
}
这里通过TransactionSynchronizationManager将ConnectionHolder(包含物理连接)绑定到当前线程的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 许可协议,转载请注明出处!
评论