DefaultSqlSession和SqlSessionTemplate的线程安全问题

发布时间 2023-10-07 17:00:17作者: 程序晓猿

这篇文章来分析下mybaits中SqlSession 接口的两个实现类 DefaultSqlSession SqlSessionTemplate 的线程安全问题。

一、DefaultSqlSession

先说结论,DefaultSqlSession是线程不安全的。

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

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

下面具体分析下

对于原因1,

单独使用mybatis时(不与spring整合)默认情况下SqlSession使用的就是此实现类,我们先来简单梳理下使用DefaultSqlSession时sql执行的流程

SqlSession 实现类中会持有Executor 对象,Executor中会持有Transaction,

Executor 中会先获取到StatementHandler 对象,然后用StatementHandler对象去执行sql,在这个过程中数据库连接是通过Transaction获取的。

单独使用mybatis时我们一般使用的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对象,

所以根据上边分析的sql执行流程,多个线程使用同一个DefaultSqlSession对象执行sql时它们使用的是同一个Connection对象。

再来看看原因2,一级缓存在BaseExecutor 中实现,

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

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

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

关于mybatis缓存的具体内容可以看下我的另一篇文章 mybatis中的缓存详解

二、SqlSessionTemplate

先说结论,SqlSessionTemplate是线程安全的。

mybatis与spring整合后执行sql时使用的就是这个实现,它为什么是线程安全的呢,因为它是通过内部持有的SqlSession代理对象来执行sql,

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    //真正执行sql的是这个代理对象
    private final SqlSession sqlSessionProxy;
    
    //在构造方法中给代理对象赋值
    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;
    //使用jdk的动态代理创建代理对象,
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }
}

创建代理对象是关键逻辑是在SqlSessionInterceptor中,这是SqlSessionTemplate中的一个内部类

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    //这个invoke方法就是执行sql的方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      //获取sqlsession对象,这个getSqlSession方法使用了spring的TransactionSynchronizationManager,
      //保证了每一个线程都会获取到单独的sqlsession(还是DefaultSqlSession),
      //这样多个线程使用同一个SqlSessionTemplate执行sql时,最终每个线程是使用各自的
      // DefaultSqlSession去执行sql,所以说SqlSessionTemplate是线程安全的。
      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)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          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);
        }
      }
    }
  }

上边这段代码中和线程安全相关的是哪个getSqlSession 方法,它使用了spring的TransactionSynchronizationManager,保证了每一个线程都会使用到单独的sqlsession(还是DefaultSqlSession)这样多个线程使用同一个SqlSessionTemplate执行sql时,最终每个线程是使用各自的DefaultSqlSession去执行sql,所以说SqlSessionTemplate是线程安全的。

总结下就是SqlSessionTemplate中通过动态代理保证了不同的线程使用不同的Sqlsession去执行sql

关于SqlSessionTemplate更细致的内容,这个涉及到mybaits中的事务控制,可以看下我的另一篇文章

mybatis中的事务管理