首页 > 数据库 >DefaultSqlSession 和 SqlSessionTemplate 的线程安全问题

DefaultSqlSession 和 SqlSessionTemplate 的线程安全问题

时间:2025-01-01 14:40:43浏览次数:1  
标签:SqlSessionTemplate DefaultSqlSession executorType sessionFactory SqlSession sess

总结自: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的线程安全问题。

标签:SqlSessionTemplate,DefaultSqlSession,executorType,sessionFactory,SqlSession,sess
From: https://www.cnblogs.com/Higurashi-kagome/p/18645930

相关文章

  • 浏览器渲染进程的线程有哪些?
    浏览器渲染进程的线程主要包括以下几种:GUI渲染线程:负责渲染浏览器页面,解析HTML、CSS,构建DOM树、CSSOM树、渲染树以及绘制页面。当界面需要重绘或由于某种操作引发回流时,该线程会执行。注意,GUI渲染线程和JS引擎线程是互斥的,以防止渲染出现不可预期的结果。JS引擎线程:......
  • DataTable 的线程安全问题
    DataTable的线程安全问题背景虽然说一般只读不太会涉及到线程安全问题,但是实际在编写代码的过程中发现datatable即使是读取也是线程不安全的.我的当时的代码是想要在多线中基于此datatable创建一个datarow,一开始以为是闭包的问题,后来解决闭包问题之后,发现还有有问题,于是......
  • Java子线程无法获取Attributes的解决方法
    在Java多线程编程中,开发者经常会遇到子线程无法获取主线程设置的Attributes的问题。Attributes通常用于存储与当前线程相关的数据,尤其在Web应用中,它们常用于请求上下文的管理。然而,由于Java线程是独立运行的,每个线程有自己的内存空间,一个线程无法直接访问另一个线程的局部变量或属......
  • 线程同步通信
    线程同步通信        多线程在运行过程中,各个线程都是随着OS的调度算法,占用CPU时间片来执行指令做事情,每个线程的运行完全没有顺序可言。但是在某些应用场景下,一个线程需要等待另外一个线程的运行结果,才能继续往下执行,这就需要涉及线程之间的同步通信机制。    ......
  • Linux 统计活跃线程和线程数
    摘要:使用Linux命令ps-eT动态查看进程中,以指定字符串打头的活跃线程和线程数。动态查看进程的线程数及活跃线程数实现方案  在Linux系统中,可以使用以下命令来动态查看进程中名字包含“keyword”的、活跃的线程或者线程数量:ps-eT|grep'keyword'|awk'{print$NF}'|s......
  • Python并发总结:多线程、多进程与异步编程
    随着多核的发展,Python中并发编程也变得越来越广泛且发展很快。一方面,Python提供了多种并发编程工具。比如,传统的多线程,通过threading模块方便地创建和管理线程,可用于I/O密集型任务;多进程,利用multiprocessing模块充分利用多核CPU优势,适合CPU密集型任务。另一方面,随着异步编程的......
  • 多线程开发常见问题汇总
    1.Thread.UncaughtExceptionHandlerUncaughtExceptionHandler‌是一个接口,用于处理线程因未捕获异常而突然终止的情况。虽然,通常都会在线程执行的代码中加try...catch来捕获异常,那么如果某些异常没有被catch住(比如,线程突然死掉了)那么我们将不知道发生了什么。因此,给每个现在设......
  • 在js中开启worker线程后,如何销毁?
    在JavaScript中,WebWorkers提供了在后台线程中运行脚本的能力,这样就不会阻塞主线程。当你创建了一个worker线程后,你可能需要在某个时刻销毁它,以释放资源。销毁worker线程非常简单,只需调用其terminate()方法即可。以下是一个简单的示例,展示了如何创建和销毁一个worker线......
  • 在js中,worker如何与主线程通信?
    在JavaScript中,WebWorkers提供了一种在浏览器后台线程中运行脚本的方法,这样就不会阻塞或影响页面的UI渲染。主线程和工作线程之间的通信主要通过postMessage()方法和onmessage事件处理器来实现。以下是一个简单的例子来说明如何实现这种通信:1.创建一个新的Worker首先,你需要在......
  • 【Java并发编程线程池】 ForkJoinPool 线程池是什么 怎么工作的 和传统的ThreadPoolEx
    Java中的ForkJoinPool线程池是什么怎么工作的Java中的ForkJoinPool线程池是什么怎么工作的相比较于传统的线程池,ForkJoinPool线程池更适合处理大量的计算密集型任务,它的核心思想是将一个大任务拆分成多个小任务,然后将这些小任务分配给多个线程去执行,最后将这些小任务的......