DefaultSqlSession 和 SqlSessionTemplate 的线程安全问题

总结自:DefaultSqlSession 和 SqlSessionTemplate 的线程安全问题、MyBatis 与 Spring 整合时是如何解决 SqlSession 线程不安全的问题的

DefaultSqlSession

原因 1:Connection 本身是线程不安全的。如果多个线程获取到同一个 Connection 进行数据库操作,一个线程正在更新数据,而另一个线程提交了事务,这种情况可能导致数据混乱和丢失。

原因 2:MyBatis 的一级缓存和二级缓存存储使用的都是 HashMap,而 HashMap 本身就不是线程安全的。

原因 1 分析

在单独使用 MyBatis(不与 Spring 整合)时,默认情况下 SqlSession 使用的是 DefaultSqlSession 实现类。以下是使用 DefaultSqlSession 时 SQL 执行的流程:

  1. SqlSession实现类中会持有Executor对象。
  2. Executor中会持有Transaction
  3. 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放入TransactionSynchronizationManagerThreadLocal中。

线程安全的实现

进入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的线程安全问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/862271.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Java/Spring] 深入理解:Spring Web DispatcherServlet

1 概述:Spring Web DispatcherServlet DispatcherServlet 简介org.springframework.web.servlet.DispatcherServlet 是一个 Servlet,它接收所有的 HTTP 请求,并根据请求的信息将其分发给相应的处理器(Handler)进行处理。 它是 Spring MVC 架构模式中的关键部分,将请求处理…

DVWA靶场File Inclusion (文件包含) 漏洞所有级别通关教程及源码解析

文件包含 文件包含漏洞(File Inclusion Vulnerability)是一种常见的网络安全漏洞,主要出现在应用程序中不安全地处理文件路径时。攻击者可以利用此漏洞执行恶意文件,或者访问不该被访问的文件 1. low 有3个页面随便点击一个,可以在url处发现传参点访问:http://127.0.0.1/…

[Java]代理模式

本篇文章主要是对静态代理和动态代理实现思路的简述,以示例为主,少涉及理论。 如果文中阐述不全或不对的,多多交流。【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) https://www.cnblogs.com/cnb-yuchen/p/18002823 出自【进步*于辰的博客】参考笔记一…

lvs+dr+httpd+keepalived高可用部署

lvs+dr+httpd+keepalived高可用部署参考文档:http://blog.csdn.net/m582445672/article/details/7670015 a、 keepalived 是lvs 的扩展项目,因此它们之间具备良好的兼容性。这点应该是keepalived 部署比其他类似工具能更简洁的原因吧! b、 通过对服务器池对象的健康检查,实…

《数字经济:行业产业链》全景梳理及区域热力图

行业主要上市公司:中兴通讯(000063)、大华股份(002236)、中科创达(300496)、软通动力(301236)、奇安信(688561)、同方股份(600100)、……等 本文核心数据:《数字经济:产业链 》、《数字经济:产业链 - 生态图谱》、《“产业数字化” 和 “数字产业化” 上市公司…

《数字经济行业产业链》全景梳理及区域热力图

行业主要上市公司:中兴通讯(000063)、大华股份(002236)、中科创达(300496)、软通动力(301236)、奇安信(688561)、同方股份(600100)、……等 本文核心数据:《数字经济产业链 》、《数字经济产业链生态图谱》、《产业数字化 和 数字产业化上市公司区域分布 TOP5》、…

【长路经】C#读取文件抛出FileNotFoundException异常

前言 在winform中读取文件信息时,突然抛出了FileNotFoundException的异常,但是本地是有这个文件的。 随后找到了这个文件,查看属性,[位置]属性,多了"\\?\"的前缀,百度得知这是windows对长路经的处理。需要注意: 目前在NetFx框架下,才有这个问题。 在NetCore…

【unity】学习制作类银河恶魔城游戏-5-

在-1---4-中,了解了unity的基础操作,从创建一个人物开始,通过状态机和代码完善人物的各种功能达到预期效果,随着功能的越来越多,状态机的处理也就越发麻烦,因此,从零开始设计一个合理的状态机很有必要 梦开始的地方 创建一个新的项目修改舒服的布局创建状态机,通过脚本…

CH340C 请求 USB 设备描述符失败

简短不看版如果出现上述状况,说明线有问题:线的确是数据线(不是电源线),但是不是能与CH340正常通讯的数据线(数据线的数据传输功能有问题。 需要更换其它数据线.前文该文分享CH340驱动使用过程可能出现的问题,以下出现的问题解决方案都建立在CH340硬件正常的情况下。CH3…

CH32V203F6P6-TSSOP20测试之02

一、问题思考 直接用官方提供的例程,为何下载程序后没有什么响应,难道自己设计的电路有什么不妥? 于是,对于电路进行具体分析,结果发现: 第一、官方的BOOT0采用杜邦线连接,在芯片手册好像找不到关于BOOT0的叙述,仅仅在《CH32V20x评估板说明书》中看到:也就是说,这芯片…

PVC

短线反弹

大语言模型提示技巧(一)-指令要清晰、具体

在与大语言模型对话时,应该提供尽可能清晰和具体的指令来表达希望模型执行的操作,这将有助于模型给出接近你所期待的输出,并降低得到无关或不正确回复的可能性。但需要注意的是,编写清晰的指令不意味着编写简短的指令,事实上,更长的提示实际上更清晰且提供了更多的上下文…