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
的构造方法:
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 { @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)) { 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
的线程安全问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
2023-01-01 2022年年终总结
2022-01-01 Eclipse:.. cannot be resolved to a type