为什么 DefaultSqlSession 线程不安全以及 SqlSessionTemplate 是如何解决线程安全问题的
总结自: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;
@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。
SqlSessionTemplate对SqlSession方法的实现,最终会委托给sqlSessionProxy,继而最终调用SqlSessionInterceptor的invoke方法。
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方法来注册一个持有该SqlSession的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 + "]");
// 创建一个 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放入TransactionSynchronizationManager的ThreadLocal中。
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的线程安全问题。
浙公网安备 33010602011771号