SqlSession 和 SqlSessionTemplate 简单使用及注意事项
1、SqlSession 简单使用
先简单说下 SqlSession 是什么?SqlSession 是对 Connection 的包装,简化对数据库操作。所以你获取到一个 SqlSession 就相当于获取到一个数据库连接,就可以对数据库进行操作。
SqlSession API 如下图示:
配置好数据,直接通过 SqlSessionFactory 工厂获取 SqlSession 示例,代码如下:
public class MyBatisCacheTest {private static SqlSessionFactory sqlSessionFactory;private static Configuration configuration;private static JdbcTransaction jdbcTransaction;private static Connection connection;private static MappedStatement mappedStatement;private static SqlSession sqlSession;static {try {InputStream inputStream = MyBatisCacheTest.class.getResourceAsStream("/mybatis-config.xml");sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);configuration = sqlSessionFactory.getConfiguration();configuration.setCacheEnabled(true);connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/gwmdb?useSSL=false&allowPublicKeyRetrieval=true", "root", "itsme999");jdbcTransaction = new JdbcTransaction(connection);String statement = "org.apache.ibatis.gwmtest.dao.PersonMapper.getPerson";mappedStatement = configuration.getMappedStatement( statement);// 注意这里设置了自动提交sqlSession = sqlSessionFactory.openSession(true);} catch (Exception e) {e.printStackTrace();}}}
2、SqlSession 缓存使用
SqlSession 获取到后开始演示下它的缓存使用。代码如下:
public static void main(String[] args) throws Exception {PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);Person person = mapper.getPerson(1);Person person1 = mapper.getPerson(1);System.out.println("person==person1 = " + (person == person1));}
最终结果输出为 true,因为在 SqlSession 里面是有缓存的,默认一级缓存开启,二级缓存不开启,这里暂时不讲二级缓存,想了解请看二级缓存使用篇。
但是在使用这个一级缓存时,需要注意,在多线程环境下面,会出现数据安全问题,多线程并发操作代码如下:
public static void main(String[] args) throws Exception {for (int i = 0; i < COUNT; i++) {new Thread(() -> {// 准备好 10 个线程try {cdl.await();} catch (Exception e) {e.printStackTrace();}// 随便调用其中一个查询方法PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);Person person = mapper.getPerson(1);System.out.println("person = " + person);}).start();cdl.countDown();}}
抛出异常如下:
### Cause: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.Listat org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:155)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76)at org.apache.ibatis.gwmtest.MyBatisCacheTest.lambda$main$0(MyBatisCacheTest.java:77)at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.Listat org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:163)at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:137)at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:90)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:153)... 5 more
具体原因是为什么呢?因为在多线程环境下面,共用同一个 SqlSession 导致的,具体原因看源码,SqlSession 底层调用 Executor,在 MyBatis 中它们是一对一关系。
在 MyBatis 中有分三个基本执行器:
- SimpleExecutor:每次数据库操作都需要重新编译 SQL 语句,然后开始操作数据库
- ResuExecutor (推荐):只有第一次访问数据库会编译 SQL 语句,后面不会重新编译,提高效率,然后操作数据库
- BatchExecutor:当需要批量操作数据库时,进行打包分批访问数据库
除了上面三个基本 Executor 之外,因为还有一些公共的操作,所以向上衍生出一个 BaseExecutor,比如最基本的一级缓存就是在这个执行器做的,因为一级缓存是本地缓存不能跨线程使用,所以又继续向上衍生出 CachingExecutor,二级缓存就是在这里做的,这里可以定义一些缓存比如:Redis、MongoDB 等等。
看到 SqlSession 操作一级缓存的地方(BaseExecutor 类中),源码如下:
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// ...Object object = localCache.getObject(key);List<E> list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}return list;}private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// ...localCache.putObject(key, EXECUTION_PLACEHOLDER);List<E> list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);localCache.putObject(key, list);return list;}
假设现在两个线程并发调用 mapper.getPerson(1) ,最终都要拿到 SqlSession 实例去操作数据库。而 SqlSession 和 Executor 是一对一关系,SqlSession 最终会给到 BaseExecutor 处理,最终调用上面的源码 query() 方法。
而上面的源码你只需要关注两个地方:存和取缓存。存缓存的地方注意细节,MyBatis 会先往一级缓存中保存一个占位符 EXECUTION_PLACEHOLDER,具体作用是为了能够解决子查询中循环依赖问题,不展开叙述。注意这里保存的是占位符。假设现在线程1过来恰好往一级缓存中保存完这个占位符,但是线程1此时没来得及往下执行,CPU 执行权被线程2抢走,那么现在线程2过来执行 query() 方法,因为是同一个 SqlSession,所以 cacheKey 是一模一样的,线程2会去一级缓存中取值,此时线程2取出来的肯定是线程1之前在里面保存的占位符。线程1拿到这个占位符之后,开始执行类型转换,也就是对应这句代码:(List) localCache.getObject(key),你觉得此时泛型转换能成功么?肯定不能,所以直接抛出异常。
解决办法是什么?源码不太好改,只能从使用层面进行改进,主要是因为缓存 key 是一样的,线程1从缓存中可以取出一个占位符,那么让缓存 key 不一样不就行了么?最快最简单的让缓存 key 不一样就是换一个 SqlSession。用不同的会话去操作数据库是不会出现这样的问题。所以最终改进的代码如下:
public static void main(String[] args) throws Exception {for (int i = 0; i < COUNT; i++) {new Thread(() -> {// 准备好 10 个线程try {cdl.await();} catch (Exception e) {e.printStackTrace();}// 调用查询方法sqlSession = sqlSessionFactory.openSession(true);PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);Person person = mapper.getPerson(1);System.out.println("person = " + person);}).start();cdl.countDown();}}
就是每次都重新生成一个 SqlSession 实例。其实底层也换了一个 Connection 实例。这个就是我们常说的线程安全问题是 SqlSession 的一个实现 DefaultSqlSession,MyBatis 作者也对此类加以Note that this class is not Thread-Safe
的注释。
或者换个理解 SqlSesion 线程不安全,SqlSesion 是 Mybatis 中的会话单元,对于 Mybatis 中而言,一个会话对应一个 SqlSession,也对应一个JDBC中的 Connection。多个线程同时操作 Connection,A线程执行完 SQL,还想再执行点其他的,但是B线程对这个 Connection 进行commit 操作,导致A线程一脸懵逼。
2、SqlSessionTemplate 简单使用
上面 SqlSession 存在这样的安全问题,Spring 在继承它的时候,做了改进,在 SqlSession 上继续封装一层,具体是通过动态代理做的。SqlSessionTemplate 在每次调用 API 时都会重新给你创建 SqlSession 实例。这样就能保证每次都在不同的 SqlSession 会话中操作数据库,比较安全。
下面开始演示个问题,代码如下:
public static void main(String[] args) {PaymentMapper paymentMapper = context.getBean(PaymentMapper.class);Payment payment = paymentMapper.queryAccount(1);Payment payment1 = paymentMapper.queryAccount(1);System.out.println("payment1 == payment = " + (payment1 == payment));}
最终输出结果为:false,和之前测试的结果不一样。SqlSession 不是有一级缓存嘛,为什么这里结果是 false。为什么?是因为 Spring 对 SqlSession 对象做了一层优化。之前说过同一个 SqlSession 在多线程环境下会出现安全问题,所以 Spring 在你每次操作 API 时都会重新创建新的 SqlSession 实例。所以 SqlSession 都是不一样的,就不用再去谈什么缓存。除非你是同一个 SqlSession 才有缓存之说。
那么怎么让一级缓存生效呢?可以开启事务,保证这些操作都在同一个事务下。改进代码如下:
public static void main(String[] args) {DataSourceTransactionManager tx = (DataSourceTransactionManager)context.getBean(TransactionManager.class);TransactionStatus transaction = tx.getTransaction(TransactionDefinition.withDefaults());PaymentMapper paymentMapper = context.getBean(PaymentMapper.class);Payment payment = paymentMapper.queryAccount(1);Payment payment1 = paymentMapper.queryAccount(1);System.out.println("payment1 == payment = " + (payment1 == payment));tx.commit(transaction);}
最终结果为:true,进入 SqlSessionTemplate 核心源码如下:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator) {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;}
可以看到是从 TransactionSynchronizationManager 事务管理器中获取到一个 SqlSession 实例。如果没有开启事务,这个 TransactionSynchronizationManager 中获取不到,就会走下面的 openSession() 创建新的实例。
在看到 getResource() 方法,核心源码如下:
@Nullablepublic static Object getResource(Object key) {Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);Object value = doGetResource(actualKey);if (value != null && logger.isTraceEnabled()) {logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +Thread.currentThread().getName() + "]");}return value;}@Nullableprivate static Object doGetResource(Object actualKey) {Map<Object, Object> map = resources.get();if (map == null) {return null;}Object value = map.get(actualKey);// Transparently remove ResourceHolder that was marked as void...if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {map.remove(actualKey);// Remove entire ThreadLocal if empty...if (map.isEmpty()) {resources.remove();}value = null;}return value;}
最终看到变量 resources 源码如下:
public abstract class TransactionSynchronizationManager {private static final ThreadLocal<Map<Object, Object>> resources =new NamedThreadLocal<>("Transactional resources");
}
发现竟然是一个 ThreadLocal 变量,这是每个线程私有的东西,人手一份,互不影响,当你开启事务之后,这个变量就已经保存好一个 SqlSession 连接,所以每次调用 API 时获取到的都是同一个 SqlSession 对象,是同一个会话,那么一级缓存就会开始生效。如果你没有开启事务,就会通过 SqlSessionFactory 工厂调用 openSession() 方法打开 SqlSession 会话,但是此时 SqlSessionTemplate 每次都会通过 SqlSessionFactory 打开一个新的 SqlSession,这样就不存在说啥一级缓存了都,完全两个 SqlSession。
相关文章:

SqlSession 和 SqlSessionTemplate 简单使用及注意事项
1、SqlSession 简单使用 先简单说下 SqlSession 是什么?SqlSession 是对 Connection 的包装,简化对数据库操作。所以你获取到一个 SqlSession 就相当于获取到一个数据库连接,就可以对数据库进行操作。 SqlSession API 如下图示:…...

1. QSaveFile和QFile的简单使用
1. 说明 QSaveFile和QFile两个类都是用来操作文件的,区别在于QSaveFile在对文件进行写入时有一种保护机制,再写入出错时,不会对源文件中的内容进行操作。该类在执行写操作时,会先将内容写入到一个临时文件中,如果没有…...

工业4.0是如何优化垃圾处理行业的
如今,工业4.0正在影响着制造业和物流等行业,其发展潜力在未来还有望进一步扩大。一些全球领先的垃圾处理公司已经开始在水处理和废物回收等领域应用工业4.0。工业4.0的创新给这个领域带来了一些必要的改进。随着环境危机的加剧,垃圾处理行业面…...
vue 动画(transition)
一、 实现原理 在插入、更新、移除 DOM 元素时,在合适的时候给元素添加样式类名,配合 CSS 样式使用,实现动画效果。 通俗来讲,就是将要进行动画操作的 DOM 元素用 transition 标签包裹起来。在此html元素运动前,运动…...

Python 爬虫工程师面试经验分享,金三银四
🙃 作为一个 Python 爬虫工程师,我可以分享一些我在面试中的经验和建议。 首先一点是在面试中要表现自信、友好、乐于合作,同时对公司的业务和文化也要有一定的了解和兴趣,这些也是公司在招聘中看重的因素。 文章目录🕛…...
MySQL实战篇-MySQL 降配导致的实例宕机
问题描述 由于近期对服务器进行了降配,该mysql数据库会进行批量写入操作,直接导致实例宕机 查看错误日志: 2021-02-02T09:09:23.557505Z 0 [Note] InnoDB: page_cleaner: 1000ms intended loop took 16791ms. The settings might not be optimal. (fl…...

时隔多年,这次我终于把动态代理的源码翻了个地儿朝天
本文内容整理自 博学谷狂野架构师 动态代理简介 Proxy模式是常用的设计模式,其特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。 用户可以更加结构图࿰…...

数据分析-深度学习 Tensorflow Day6
我们需要解决的问题:1: 什么是bp 神经网络?2:理解bp神经网络需要哪些数学知识?3:梯度下降的原理4: 激活函数5:bp的推导。1.什么是bp网络?引用百度知道回复:“我们最常用的…...

leaflet 设置多个marker,导出为一个geojson文件(066)
第066个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中使用L.marker设置多个markers, 通过数据重组,导出为geojson文件。 这里面 ayer instanceof L.Marker 是一个很重要的判断条件,可以灵活地去运用。 直接复制下面的 vue+openlayers源代码,操作2分钟即可…...

企业与第三方供应商合作时,会存在哪些安全风险?
随着现代社会的发展,企业供应链、产业供应链已日渐成熟。其中,供应商与企业的关系也由最初的纯粹买卖关系发展成了合作伙伴关系。在整个供应链体系中,供应商与其受众承担着供应链中环环相扣的责任,可以说,企业安全的薄…...

技术源自洛克希德·马丁,光场XR眼镜FYR解析
专注于医疗场景的一家XR眼镜厂商FYR(全称:FYR Medical)近期亮相,并宣布完成了260万美元A轮融资,本轮融资由NuVasive领投,资金将用于开发世界上第一个XR光场“放大镜”类产品。据青亭网了解,NuVa…...
剑指 Offer 10- II. 青蛙跳台阶问题(LeetCode 70. 爬楼梯)(动态规划打表)
题目: 链接:剑指 Offer 10- II. 青蛙跳台阶问题;LeetCode 70. 爬楼梯 难度:简单 相关博文:剑指 Offer 10- I. 斐波那契数列(动态规划打表) 一只青蛙一次可以跳上1级台阶,也可以跳上…...
webpack(高级)--文件的压缩Terser(js/css/html) Tree Shaking
webpack Terser Terser是一个javascript的解释(Parser),Mangler(绞肉机) /Compressor(压缩机)的工具集 早期我们会使用uglify-js来压缩,丑化我们的javascript代码 但是目前已经不在维护 并且不支持ES6语法 Terser是从uglify-es fork 过来的 也就是说 Terser可以帮…...

做软文发布需要注意哪些细节?
软文发布是一种有效的网络营销和推广活动,它以媒体等形式把产品信息植入到软文报道或新闻中,进行心理暗示和引导销售,进行正面宣传以及促进销售的新型网络营销方式,它不但能够有效地推行产品宣传、也能有效地提高网络曝光率&#…...
【Python】一篇文章读懂yield基本用法
这一次,田辛老师想通俗易懂地解释一下Python中的yield功能。 本文要说明以下四个问题: yield是什么什么是迭代器和生成器yield的基本用法如何使用yield from 用真正简单的方法讲解yield并不容易。 我想,就算你不懂yield语句,也…...

Docker getting started
系列文章目录 Docker 概述 Docker getting started 文章目录系列文章目录前言一、容器及镜像的概念二、容器化一个应用三、更新应用四、分享应用五、持久化数据存储volume mount 和 bind mount比较Container volumesbind mounts六、跨多容器的应用七、Docker 其它八、Docker 图…...
【Uniapp使用遇到问题合集】
Uniapp使用遇到问题合集问题一跳转页面后无法进行滑动/滚动的操作描述解决方法问题一 跳转页面后无法进行滑动/滚动的操作 描述 如题,实际操作是我在uniapp自带的组件uni-popup弹出层中加入了一个点击事件,点击后可跳转到指定的页面 但实际运行中出现了跳转过后页面过长时无…...
宝塔面板破解最新教程
宝塔,让运维简单高效。面板支持Linux与Windows系统。一键配置:LAMP/LNMP、网站、数据库、FTP、SSL,通过Web端轻松管理服务器。今天考高分网就简单说一下BT宝塔面板专业版最新破解教程。 网地址:https://www.bt.cn/ 网上的破解版一般分为两种,一种是直接…...

基于zookeeper的Hadoop集群搭建详细步骤
目录 一、一些基本概念 二、集群配置图 三、Hadoop高可用集群配置步骤 1.在第一台虚拟机解压hadoop-3.1.3.tar.gz到/opt/soft/目录 2.修改文件名、属主和属组 3.配置windows四台虚拟机的ip映射 4.修改hadoop配置文件 (1)hadoop-env.sh (2)workers (3)crore-site.xml …...
职称有哪些意义?如何提升职称?
每年我们会看到很多人都会努力地提升自己的职称,那么为什么大家都想要晋升职称?在这里余老师说说他的作用,您可以参考一下。 一、个人金钱方面的提升 工资。职称直接关联的就是涨工资了。正常情况下,职称和工资是一一对应的了,…...

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...
unix/linux,sudo,其发展历程详细时间线、由来、历史背景
sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...

GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...
Web中间件--tomcat学习
Web中间件–tomcat Java虚拟机详解 什么是JAVA虚拟机 Java虚拟机是一个抽象的计算机,它可以执行Java字节码。Java虚拟机是Java平台的一部分,Java平台由Java语言、Java API和Java虚拟机组成。Java虚拟机的主要作用是将Java字节码转换为机器代码&#x…...

Unity中的transform.up
2025年6月8日,周日下午 在Unity中,transform.up是Transform组件的一个属性,表示游戏对象在世界空间中的“上”方向(Y轴正方向),且会随对象旋转动态变化。以下是关键点解析: 基本定义 transfor…...