我们知道mybatis的sqlsession接口默认的实现类是DefaultSqlsession,作者在该类上面注释了是线程非安全的:
那么到底为何说它是线程不安全的呢?
1.首先由于JDBC的Connection对象本身不是线程安全的,而session中又只有一个connection,所以不是线程安全的
一次SqlSession的执行最终只会产生一个connection,所以我们设想一下,在两个线程通过同一个sqlsession来执行crud,那么就有可能,我先跑完的线程,把唯一的这一个连接给关闭掉,从而造成另一条线程的逻辑不被成功执行,所以通过DefaultSqlSession来执行数据库操作是线程不安全的。
2.一级缓存 由于一级缓存是session级别的,所以如果多个线程同时使用session,当线程A进行了插入操作未完成,但是此时线程B进行查询并插入一级缓存,那么一级缓存与数据库中数据不一致
那么spring和mybatis整合的时候是如何解决这个不安全的问题呢,先说答案,我们再spring在整合mybatis的时候是通过sqlsessiontemplate来操作crud的,那么关键就是sqlsessiontemplate这个类了,它里面采用了threadlocal的方式保证每个线程独享一份自己的sqlsession,这样也就解决了我们说的线程安全问题,且跟我一起看源码,细细道来
首先我们看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方法:
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)) {
// 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);
}
}
}
}
这个方法其实就是一个事务的执行过程,先获取sqlsession然后执行sql,最后commit,如果异常就向上抛出异常,并且关闭session
关键就在第一行,怎么去获取sqlseission了,继续看(org.mybatis.spring.SqlSessionUtils#getSqlSession(org.apache.ibatis.session.SqlSessionFactory, org.apache.ibatis.session.ExecutorType, org.springframework.dao.support.PersistenceExceptionTranslator)):
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这个方法:
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放到一个TransactionSynchronizationManager的threadlocal里面去了:
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();
// set ThreadLocal Map if none found
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,到此就可以知道,sqlsessiontemplate解决sqlsession线程安全的问题就是通过threadlocal来实现每一个线程独享一份sqlsession
前面有讲到在执行完业务sql之后,如果异常了会调用一个closesession来关闭sqlsession,它里面最后是调用的mybatis的sqlsessionmanager的close方法,sqlsessionmanager也是通过threadlocal来实现线程安全的,至于为什么是调用sqlsessionmanager,这个是mybatis与spring整合的时候实例化sqlsession的策略决定的,这个看mybatis源码就知道实例化sqlsession的时候有两种不同的策略