总结自:DefaultSqlSession 和 SqlSessionTemplate 的线程安全问题、MyBatis 与 Spring 整合时是如何解决 SqlSession 线程不安全的问题的
DefaultSqlSession
原因 1:Connection 本身是线程不安全的。如果多个线程获取到同一个 Connection 进行数据库操作,一个线程正在更新数据,而另一个线程提交了事务,这种情况可能导致数据混乱和丢失。
原因 2:MyBatis 的一级缓存和二级缓存存储使用的都是 HashMap,而 HashMap 本身就不是线程安全的。
原因 1 分析
在单独使用 MyBatis(不与 Spring 整合)时,默认情况下 SqlSession 使用的是 DefaultSqlSession 实现类。以下是使用 DefaultSqlSession 时 SQL 执行的流程:
SqlSession
实现类中会持有Executor
对象。Executor
中会持有Transaction
。Executor
首先获取StatementHandler
对象,然后用该对象执行 SQL。在这个过程中,数据库连接是通过Transaction
获取的。
通常情况下,我们使用的 Transaction 实现类是JdbcTransaction
。以下是部分源码:
public class JdbcTransaction implements Transaction {// 成员变量protected Connection connection;protected DataSource dataSource;@Overridepublic 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 {@Overridepublic 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
放入TransactionSynchronizationManager
的ThreadLocal
中。
线程安全的实现
进入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
的线程安全问题。