这篇文章来分析下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中的事务控制,可以看下我的另一篇文章