DefaultSqlSession 和 SqlSessionTemplate 的线程安全问题

总结自:DefaultSqlSession 和 SqlSessionTemplate 的线程安全问题MyBatis 与 Spring 整合时是如何解决 SqlSession 线程不安全的问题的

DefaultSqlSession

原因 1:Connection 本身是线程不安全的。如果多个线程获取到同一个 Connection 进行数据库操作,一个线程正在更新数据,而另一个线程提交了事务,这种情况可能导致数据混乱和丢失。

原因 2:MyBatis 的一级缓存和二级缓存存储使用的都是 HashMap,而 HashMap 本身就不是线程安全的。

原因 1 分析

在单独使用 MyBatis(不与 Spring 整合)时,默认情况下 SqlSession 使用的是 DefaultSqlSession 实现类。以下是使用 DefaultSqlSession 时 SQL 执行的流程:

  1. SqlSession实现类中会持有Executor对象。
  2. Executor中会持有Transaction
  3. Executor首先获取StatementHandler对象,然后用该对象执行 SQL。在这个过程中,数据库连接是通过Transaction获取的。

通常情况下,我们使用的 Transaction 实现类是JdbcTransaction。以下是部分源码:

public class JdbcTransaction implements Transaction {
// 成员变量
protected Connection connection;
protected DataSource dataSource;
@Override
public Connection getConnection() throws SQLException {
if (connection == null) {
openConnection();
}
return connection;
}
}

从源码可以看出,同一个JdbcTransaction对象如果多次获取连接,返回的是同一个 Connection 对象。因此,多个线程使用同一个 DefaultSqlSession 对象执行 SQL 时,它们使用的是同一个 Connection 对象。

原因 2 分析

一级缓存在BaseExecutor中实现:

public abstract class BaseExecutor implements Executor {
// 这个属性实现了一级缓存
protected PerpetualCache localCache;
}

PerpetualCache内部会持有一个 HashMap 来存储一级缓存,而 HashMap 本身是线程不安全的。因此,多个线程使用同一个 SqlSession 执行 SQL 时,一级缓存这个 map 是线程不安全的。

二级缓存在CachingExecutor中实现,最终数据还是被放到了一个 HashMap 中,所以道理是一样的。

SqlSessionTemplate

在 Spring 整合 MyBatis 时,我们通过SqlSessionTemplate来操作 CRUD(SqlSessionTemplate本身实现了SqlSession接口)。SqlSessionTemplate通过ThreadLocal,确保每个线程独享一份自己的SqlSession,从而解决了线程安全问题。

SqlSessionTemplate 构造方法

首先,查看SqlSessionTemplate的构造方法:

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}

其中,sqlSessionProxy实际上是一个SqlSession,通过 JDK 动态代理生成。动态代理最终会调用对应的 interceptor 的invoke方法。

SqlSessionInterceptor 的 invoke 方法

private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}

这个方法主流程即控制事务:获取SqlSession、执行 SQL、最后提交事务。如果发生异常,则抛出异常并关闭 session。

获取 SqlSession 的方法

关键在于如何获取SqlSession,接下来查看getSqlSession方法:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}

其中调用了registerSessionHolder方法来注册SqlSessionHolder

注册 SqlSessionHolder

接下来,查看registerSessionHolder方法:

private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
SqlSessionHolder holder;
if (TransactionSynchronizationManager.isSynchronizationActive()) {
Environment environment = sessionFactory.getConfiguration().getEnvironment();
if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
LOGGER.debug(() -> "Registering transaction synchronization for SqlSession [" + session + "]");
holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
holder.setSynchronizedWithTransaction(true);
holder.requested();
} else {
if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
LOGGER.debug(() -> "SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
} else {
throw new TransientDataAccessResourceException("SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
}
}
} else {
LOGGER.debug(() -> "SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
}
}

在这里,SqlSessionHolder持有了创建出来的SqlSession对象。重要的一行是:

TransactionSynchronizationManager.bindResource(sessionFactory, holder);

这行代码将SqlSessionHolder放入TransactionSynchronizationManagerThreadLocal中。

线程安全的实现

进入bindResource方法:

public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Assert.notNull(value, "Value must not be null");
Map map = (Map) resources.get();
if (map == null) {
map = new HashMap();
resources.set(map);
}
if (map.put(actualKey, value) != null) {
throw new IllegalStateException("Already value [" + map.get(actualKey) + "] for key [" +
actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
}
if (logger.isTraceEnabled()) {
logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" +
Thread.currentThread().getName() + "]");
}
}

resources是一个ThreadLocal,方法先从resources中取出 map,然后将SqlSessionHolder放入 map 中,因此可以知道SqlSessionTemplate通过ThreadLocal实现每个线程独享一份SqlSession,从而解决了SqlSession的线程安全问题。

posted @   Higurashi-kagome  阅读(36)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
历史上的今天:
2023-01-01 2022年年终总结
2022-01-01 Eclipse:.. cannot be resolved to a type
点击右上角即可分享
微信分享提示