为什么 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 构造方法

首先,查看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());
}

可以看到,其核心是通过 JDK 动态代理生成一个SqlSession,赋值给sqlSessionProxy。动态代理的拦截器是SqlSessionInterceptor

SqlSessionTemplateSqlSession方法的实现,最终会委托给sqlSessionProxy,继而最终调用SqlSessionInterceptorinvoke方法。

SqlSessionInterceptor 的 invoke 方法

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 获取 SqlSession
    SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
    try {
      // 调用 SqlSession 方法执行 SQL
      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) {
        // 如果有异常,则关闭 SqlSession
        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);
      }
    }
  }
}

这个方法主要即执行 SQL,其流程为:

  • 获取SqlSession实例。
  • 调用 SqlSession 方法执行 SQL,必要时提交事务。
  • 如果发生异常,则抛出异常并关闭 SqlSession。

那么这个方法是如何保证线程安全的呢?关键在于每个线程都独有SqlSession实例,不存在多线程共享。关键在于getSqlSession方法如何创建及管理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
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

  // 尝试从 SqlSessionHolder 中获取 SqlSession
  SqlSession session = sessionHolder(executorType, holder);
  if (session != null) {
    return session;
  }

  LOGGER.debug(() -> "Creating a new SqlSession");
  // 创建一个新的 SqlSession
  session = sessionFactory.openSession(executorType);

  // 注册持有 SqlSession 的 SqlSessionHolder
  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

  return session;
}

可以看到,方法首先尝试从SqlSessionHolder中获取SqlSession,如果获取不到,则创建一个新的SqlSession,然后调用registerSessionHolder方法来注册一个持有该SqlSessionSqlSessionHolder

注册 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 + "]");

      // 创建一个 SqlSessionHolder,持有该 SqlSession
      holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
      // 将 SqlSessionHolder 注册到 TransactionSynchronizationManager 中
      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 方法

进入bindResource方法:

public static void bindResource(Object key, Object value) throws IllegalStateException {
   Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
   Assert.notNull(value, "Value must not be null");
   // 从 ThreadLocal 中获取 map
   Map map = (Map) resources.get();
   if (map == null) {
      map = new HashMap();
      resources.set(map);
   }
   // 保存 SqlSessionHolder 到 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 中。这样SqlSessionHolder就是线程独享的,而SqlSessionHolder中持有SqlSession,因此SqlSession也是线程独有的。

总结

从源码可知,SqlSessionTemplate通过ThreadLocal实现每个线程独享一份SqlSession,从而解决了SqlSession的线程安全问题。

posted @ 2025-01-01 14:42  Higurashi-kagome  阅读(164)  评论(0)    收藏  举报