SqlSession 和 SqlSessionTemplate 简单使用及注意事项

news2024/10/2 12:04:23

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.List
	at 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.List
	at 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 中有分三个基本执行器:

  1. SimpleExecutor:每次数据库操作都需要重新编译 SQL 语句,然后开始操作数据库
  2. ResuExecutor (推荐):只有第一次访问数据库会编译 SQL 语句,后面不会重新编译,提高效率,然后操作数据库
  3. BatchExecutor:当需要批量操作数据库时,进行打包分批访问数据库
     

除了上面三个基本 Executor 之外,因为还有一些公共的操作,所以向上衍生出一个 BaseExecutor,比如最基本的一级缓存就是在这个执行器做的,因为一级缓存是本地缓存不能跨线程使用,所以又继续向上衍生出 CachingExecutor,二级缓存就是在这里做的,这里可以定义一些缓存比如:Redis、MongoDB 等等。

看到 SqlSession 操作一级缓存的地方(BaseExecutor 类中),源码如下:

  @Override
  public <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() 方法,核心源码如下:

	@Nullable
	public 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;
	}
	
	@Nullable
	private 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。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于 CentOS7 的 KVM 部署 + 虚拟机创建

目录一、实验环境二、部署 KVM三、创建虚拟机四、远程管理 KVM 虚拟机FAQ一、实验环境 实验环境&#xff1a;VMware Workstation 16 Pro 打开虚拟机之前&#xff0c;首先开启 VMware Workstation Pro 16 上的硬件辅助虚拟化功能&#xff0c;如下图所示&#xff1a; 二、部署 …

Spring Cloud Gateway集成Nacos实现负载均衡

&#x1f4a1;Nacas可以用于实现Spring Cloud Gateway中网关动态路由功能&#xff0c;也可以基于Nacos来实现对后端服务的负载均衡&#xff0c;前者利用Nacos配置中心功能&#xff0c;后者利用Nacos服务注册功能。接下来我们来看下Gateway集成Nacos实现负载均衡的架构图一. 环境…

为什么现代企业发展离不开CRM系统的助力

如今的CRM系统对于任何企业来说都重要&#xff0c;因为它能帮助企业收获新客户&#xff0c;保留现有客户&#xff0c;并且将不同部门的信息全部汇集&#xff0c;实时提供关于每位客户整体全面的看法。因此&#xff0c;销售、市场营销和客户支持等领域的客户直接服务员工能够做出…

VHDL-延迟模型-惯性延迟与传输延迟

目录 1&#xff0c;惯性延时 2&#xff0c;传输延时 信号通过元件都会有延迟&#xff0c;延迟时间的计算是逻辑仿真的重要功能。考虑延迟信息得到的仿真输出波形可以更精确地反映实际电路的情况。针对元件的延时&#xff0c;人们根据需要建立了一些用的延时模型&#xff0c;这…

集成电路相关书籍

注&#xff1a;从此开始&#xff0c;文中提到的书籍都会在公众号对应文章末尾给出链接&#xff0c;不需要在微信后台获取&#xff0c;当然还是可以通过在微信后台回复相关书名获取对应的电子书。 在后台看到很多人回复集成电路相关的一些书籍&#xff0c;所以本文就提供一些书籍…

GD库图片裁剪指定形状解决办法(PHP GD库 海报)

需求描述&#xff1a;需要把图片裁剪成一个指定的平行四边形&#xff0c;目的是使用GD库&#xff0c;把裁剪后的图片放在底图上面&#xff0c;使最终合成的图片看起来是一个底图平行四边形的样子提示&#xff1a;可以结合本作者的其他文章&#xff0c;来生成一个定制化的海报&a…

【项目精选】基于Javaee的影视创作论坛的设计与实现(视频+论文+源码)

点击下载源码 基于Javaee的影视创作论坛的设计与实现主要用功能包括&#xff1a; 首页推荐、用户管理、影片管理、评论管理、 预告片管理、海报管理、公告管理、数据检索、用户注册与登录等等功能、统结构如下 &#xff08;1&#xff09;后台管理: 管理模块&#xff1a;管理员…

vscode编程小插件之Doxygen和Better Align

一、插件Doxygen:配置相应文件、函数说明项。 1、扩展商店&#xff0c;搜索Doxygen&#xff0c;如下图1&#xff0c;安装。 图1 2、设置项中&#xff0c;选择扩展设置&#xff0c;如图2 图2 3、配置版本、作者邮箱、作者名称、日期格式等等&#xff0c;如图3 4、定义函数后&…

DM8:DMDSC共享存储集群搭建-实例初始化(待完成)

DM8:DMDSC共享存储集群搭建-实例初始化1 环境介绍2 使用 DMASMCMD 工具初始化磁盘3 各个节点先后分别启动 dmcss3.1 EP733.2 EP744 各个节点先后分别启动 dmasmsvr 程序4.1 EP734.2 EP745 使用 dmasmtool 工具创建 DMASM 磁盘组(在一个节点执行)6 使用 dminit 初始化 DB 实例环…

STM32定时器实现红外接收与解码

1.NEC协议 红外遥控是一种比较常用的通讯方式&#xff0c;目前红外遥控的编码方式中&#xff0c;应用比较广泛的是NEC协议。NEC协议的特点如下&#xff1a; 载波频率为 38KHz8位地址和 8位指令长度地址和命令2次传输&#xff08;确保可靠性&#xff09;PWM 脉冲位置调制&#…

Shiro简介

介绍 ApacheShiro 是一个功能强大且易于使用的 Java 安全(权限)框架。Shiro 可以完成:认证、授权、加密、会话管理、与 Web集成、缓存等。借助Shiro 您可以快速轻松地保护任何应用程序一一从最小的移动应用程序到最大的 Web 和企业应用程序。 1.2:为什么要用 shiro 自2003年以…

GIS状态检测新技术——振动分析法

提示&#xff1a;唐老师好&#xff0c;我之前因为“阳”了&#xff0c;所以就没有参与汇报&#xff0c;给老师带来不便&#xff0c;请老师见谅。以此篇文章代替课堂汇报。 文章目录前言一、不同故障对应的振动频谱和故障特征量二、GIS设备振动特征估计1.GIS设备状态空间2.粒子滤…

npm install报错,node-sass: Command failed

报错如下&#xff1a; gyp verb check python checking for Python executable "python2" in the PATH gyp verb which failed Error: not found: python2 gyp verb which failed at getNotFoundError (C:\Program Files (x86)\Jenkins\jobs\NdbSite-hot-fix-Ma…

《游戏学习》| 微信对话模拟生成器源码分析

简介微信对话生成器&#xff0c;是一款在线微信聊天对话制作的工具&#xff0c;它可以设置苹果或安卓状态栏&#xff0c;包括手机电量、手机时间等&#xff0c;还可以设置不同用户的角色&#xff0c;然后发送文字、语音、红包、转账等多种好玩的功能&#xff0c;可谓是一款娱乐…

es的使用与原理8 -- 初识es及es简单CRUD语法

为什么不能使用数据库做搜索&#xff1f; 1、比方说&#xff0c;每条记录的指定字段的文本&#xff0c;可能会很长&#xff0c;比如说“商品描述”字段的长度&#xff0c;有长达数千个&#xff0c;甚至数万个字符&#xff0c;这个时候&#xff0c;每次都要对每条记录的所有文本…

vue处理一千张图片进行分页加载

vue处理一千张图片进行分页加载 开发过程中&#xff0c;如果后端一次性返回你1000多条图片或数据&#xff0c;那我们前端应该怎么用什么思路去更好的渲染呢&#xff1f; 第一种&#xff1a;我们可以使用分页加载 第二种&#xff1a;我们可以进行懒加载那我们用第一种方法使用…

Mysql 与 磁盘交互的过程

从之前的Mysql架构可以了解到&#xff0c;Mysql 客户端不是直接和磁盘打交道&#xff0c;我们在客户端输入的sql语句会被发送给服务端&#xff0c;服务端对sql语句进行解析、缓存等操作&#xff0c;然后再交由存储引擎去读写磁盘。这其实是从 C/S 的角度去了解Mysql。 站在OS的…

jvm如何判断对象已死?

在堆里面存放着各种各类的Java对象&#xff0c;垃圾收集器在对堆进行垃圾回收时&#xff0c;首要就是判断哪些对象还活着&#xff0c;哪些对象已经死去(即不被任何途径引用的对象)。引用计数器算法&#xff1a;引用计数器算法简单概括为&#xff1a;给对象添加一个引用计数器&a…

Java基础面试题——IO和多线程专题

文章目录1.介绍下进程和线程的关系2.说说Java中实现多线程的几种方法3.如何停止一个正在运行的线程4.介绍下线程中的常用方法1.start方法2.run方法3.getName方法4.优先级5.sleep方法6.isAlive7.join8.yield9.wait和notify/notifyAll5.介绍下线程的生命周期6.为什么wait, notify…

什么?30秒生成一个logo

logo 是徽标或者商标的英文说法。logo 承载着企业的无形资产&#xff0c;是企业综合信息传递的媒介。标志作为企业CIS战略的最主要部分&#xff0c;在企业形象传递过程中&#xff0c;是应用最广泛、出现频率最高&#xff0c;同时也是最关键的元素。通过形象的 logo 可以让消费者…