“IT魔幻屋”致力于让你遇见更好的自己!

说起 mybatis,作为 Java 程序员应该是无人不知,它是常用的数据库访问框架。与 Spring 和 Struts 组成了 Java Web 开发的三剑客— SSM。当然随着 Spring Boot 的发展,现在越来越多的企业采用的是 SpringBoot + mybatis 的模式开发,我们公司也不例外。而 mybatis 对于我也仅仅停留在会用而已,没想过怎么去了解它,更不知道它的缓存机制了,直到那个生死难忘的 BUG。故事的背景比较长,但并不是啰嗦,只是让读者知道这个 BUG 触发的场景,加深记忆。在遇到类似问题时,可以迅速定位。

先说下故事的前提,为了防止用户在动态中输入特殊字符,用户的动态都是编码后发到后台,而后台在存入到 DB 表之前会解码以方便在 DB 中查看以及上报到搜索引擎。在查询用户动态的时候先从 DB 表中读取并在后台做一次编码再传到前端,前端再解码就可以正常展示了。流程如下图:

有一天后端预发环境发布完毕后,用户的动态页面有的动态显示正常,而有的却是被编码过的。看到现象后的第一个反应就是有问题的动态被编码了两次,但是编码操作只会在 service 层的 findById 中有。理论不会在上层犯这种低级错误。话不多说便开始排查新增加的代码,发现只要进入了新增加代码中的某个 if 分支则被编码了两次。分支中除了再次调用 findById(必要性不讨论),也无其他特殊代码了。百思不得其解后请教了旁边的老司机,老司机说可能是 mybatis 缓存。于是看了下我代码,将编码的操作从 findById 中移出来后再次发布到预发,正常了,心想老司机不愧是老司机。本次 BUG 触发的有两个条件需要注意:

  • 整个操作过程都在一个函数中,而函数上面加了 @Transactional 的注解(对 mybatis 来说是在同一个 SESSION 中)
  • 一般只会调用 findByIdy 一次,如果进入分支则会调用两次 (第一次调用后做了编码后被缓存,第二次从缓存读后继续被编码)

便开始谷歌 mybatis 的缓存机制,搜到了一篇非常不错的文章《 聊聊 mybatis 的缓存机制 》,推荐大家看一下。但是这篇文章讲到了源码,涉及的比较深。而且并没讲 SpringBoot 下 mybatis 下的缓存知识点,遂作此篇,以作补充。

缓存的配置

SpringBoot + mybatis 环境搭建很简单而且网上一堆教程,这里不班门弄斧了,记得在项目中将 mytatis 的源码下载下来即可。mybaits 一共有两级缓存:一级缓存的配置 key 是 localCacheScope,而二级缓存的配置 key 是 cacheEnabled,从名字上可以得出以下信息:

  • 一级缓存是本地或者说局部缓存,它不能被关闭,只能配置缓存范围。SESSION 或者 STATEMENT。
  • 二级缓存才是 mybatis 的正统,功能会更强大些。

先来看下在 SpringBoot中 如何配置 mybatis 缓存的相关信息。默认情况下 SpringBoot 下的 mybatis 一级缓存为 SESSION 级别,二级缓存也是打开的,可以在 mybatis 源码中的 org.apache.ibatis.session.Configuration.class 文件中看到(idea中打开),如下图:

也可以通过以下测试程序查看缓存开启情况:

@RunWith(SpringRunner.class)@SpringBootTestpublic class LearnApplicationTests { private SqlSessionFactory factory; @Before public void setUp() throws Exception { InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml"); factory = new SqlSessionFactoryBuilder().build(inputStream); } @Test public void showDefaultCacheConfiguration() { System.out.println("一级缓存范围: " + factory.getConfiguration().getLocalCacheScope()); System.out.println("二级缓存是否被启用: " + factory.getConfiguration().isCacheEnabled()); }}

如果要设置一级缓存的缓存级别和开关二级缓存,在 mybatis-config.xml (当然也可以在 application.xml/yml 中配置)加入如下配置即可:

但需要注意的是二级缓存 cacheEnabled 只是个总开关,如果要让二级缓存真正生效还需要在 mapper xml 文件中加入 。一级缓存只在同一 SESSION 或者 STATEMENT 之间共享,二级缓存可以跨 SESSION,开启后它们默认具有如下特性:

  • 映射文件中所有的 select 语句将被缓存
  • 映射文件中所有的 insert/update/delete 语句将刷新缓存

一二级缓存同时开启的情况下,数据的查询顺序是 二级缓存 -> 一级缓存 -> 数据库。一级缓存比较简单,而二级缓存可以设置更多的属性,只需要在 mapper 的 xml 文件中的 中配置即可,具体如下:

触发缓存

  1. 配置一级缓存为 SESSION 级别

Controller 中调用两次 getOne,代码如下:

@RequestMapping("/getUser")public UserEntity getUser(Long id) { //第一次调用 UserEntity user1=userMapper.getOne(id); //第二次调用 UserEntity user2=userMapper.getOne(id); return user1;}

调用: http://localhost:8080/getUser?id=1,打印结果如下:

从图中的 1/2/3/4 可以看出每次 mapper 层的一次接口调用如 getOne 就会创建一个 session,并且在执行完毕后关闭 session。所以两次调用并不在一个 session 中,一级缓存并没有发生作用。开启事务,Controller 层代码如下:

@RequestMapping("/getUser")@Transactional(rollbackFor = Throwable.class)public UserEntity getUser(Long id) { //第一次调用 UserEntity user1=userMapper.getOne(id); //第二次调用 UserEntity user2=userMapper.getOne(id); return user1;}

由于在同一个事务中,虽然调用了 select 操作两次但是只执行了一次 sql ,缓存发挥了作用。这就跟一开始我遇到的那个 BUG 场景一样:同一 session 且 select 调用 > 1 次。如果在两次调用中间插入 update 操作,缓存会立即失效。只要 session 中有 insert、update 和 delete 语句,该 session 中的缓存会立即被刷新。但是注意这只是在同一 session 之间。不同 session 之间如 session1 和 session2,session1 里的 insert/update/delete 并不会影响 session 2 下的缓存,这在高并发或者分布式的情况下会产生脏数据。所以建议将一级缓存级别调成 statement。

  1. 配置一级缓存为 STATEMENT 级别

再次将(1)中的无事务和有事务的代码分别执行一遍,打印结果始终如下:

配置成 SATEMENT 后,一级缓存相当于被关闭了。STATEMENT 级别暂时不好模拟,但是我猜测 STATEMENT 级别即在同一执行 sql 的接口中(如上面的 getOne 中)缓存,出了 getOne 缓存即失效。

  1. 配置二级缓存,同时为了避免一级缓存的干扰,将一级缓存设置为 STATEMENT

Controller 中去掉 @Transactional 注解代码如下:

@RequestMapping("/getUser")public UserEntity getUser(Long id) { UserEntity user1=userMapper.getOne(id); UserEntity user2=userMapper.getOne(id); return user1;}

当然二级缓存开关保证打开,在 mapper xml 文件中加入 ,整个文件代码如下:

<?xml version="1.0" encoding="UTF-8" ?> id, name, sex SELECT  FROM users WHERE id = #{id};

执行 http://localhost:8080/getUser?id=1,打印结果如下:

从图中红框可以看出第二次查询命中缓存,0.5 是命中率。再次执行

http://localhost:8080/getUser?id=1

打印结果如下:

这次一次 sql 也没执行了,缓存命中率上升到 0.75了,所以说二级缓存全局缓存。但它的缓存范围也是有限的,一级缓存在同一个 session 中。二级缓存虽然可以跨 session 但也只能在同一 namespace 中,所谓 namespace 即 mapper xml 文件。具体实验请看《聊聊 mybatis 的缓存机制》中的关于二级缓存的实验 4 和 5。再看下二级缓存配置对二级缓存的影响,为了明显的看出效果,只改如下配置:

/****/@RequestMapping("/getUser")public UserEntity getUser(Long id, Long id2) { //第一个对象 1 System.out.println("================缓存对象 1================="); UserEntity user1 = userMapper.getOne(id); //另一个对象 2 System.out.println("========缓存对象 2,剔除缓存中的对象 1======="); UserEntity user2=userMapper.getOne(id2); user2 = userMapper.getOne(id2); //再次读取第一个对象 System.out.println("==========缓存被剔除,执行查询 sql==========="); user1 = userMapper.getOne(id); //暂停 5s try { sleep(5000); }catch (Exception e){ e.printStackTrace(); } System.out.println("============5s 后再次查询对象 2============="); user2 = userMapper.getOne(id2); return user1;}

执行 http://localhost:8080/getUser?id=1&id2=2 最后打印的结果如下:

可以看出二级缓存只能缓存一个对象且 5s 后就失效了,配置生效。缓存配置中还有一个重要的配置 type,该配置可以配置第三方的 cache,特别在高并发和分布式情况下。当然,使用更专业的分布式缓存才是王道,例如 redis 等。

关注“IT魔幻屋”,学习更多前沿技术,来这里你将遇见更好的自己!

springboot mybatis 事务_SpringBoot 下 Mybatis 的缓存相关推荐

  1. springboot update数据_SpringBoot整合Mybatis+Druid+数据库(注解版)

    运行展示 正题 Spring boot :2.1.5RELEASE :数据库(Mysql.Oracle):Mybatis:阿里云的连接池 : Druid : 步骤 1.POM依赖 <!-- My ...

  2. springboot mysql 事务_springBoot(14):使用SQL关系型数据库-事务处理

    一.事务的四个特性(ACID) 原子性(Atomicity): 事务是一个原子操作,由一系列动作组成.事务的原子性确保动作要么全部完成,要么完全不起作用. 一致性(Consistency): 一旦事务 ...

  3. MyBatis事务管理解析:颠覆你心中对事务的理解

    MyBatis事务管理解析:颠覆你心中对事务的理解! 1 .说到数据库事务,人们脑海里自然不自然的就会浮现出事务的四大特性.四大隔离级别.七大传播特性. 四大还好说,问题是七大传播特性是哪儿来的?是S ...

  4. 手把手带你阅读Mybatis源码(三)缓存篇

    点击上方"Java知音",选择"置顶公众号" 技术文章第一时间送达! 前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读M ...

  5. springboot mysql事物_springboot如何开启数据库事务

    springboot开启事务很简单,只需要一个注解@Transactional 就可以了.因为在springboot中已经默认对jpa.jdbc.mybatis开启了事事务,引入它们依赖的时候,事物就 ...

  6. java | (三十一)MyBatis(1)配置、映射、缓存

    目录 配置MyBatis 基本配置 采用注解的方式映射sql 主要API生命周期 别名typeAliases environments配置 mappers SQL映射 select(带参数) sele ...

  7. SpringBoot整合:Druid、MyBatis、MyBatis-Plus、多数据源、knife4j、日志、Redis,Redis的Java操作工具类、封装发送电子邮件等等

    SpringBoot笔记 一.SpringBoot 介绍 1.1.SpringBoot简介 SpringBoot 是一个快速开发的框架, 封装了Maven常用依赖.能够快速的整合第三方框架:简化XML ...

  8. 【mybatis源码分析(四)】mybatis事务实现原理

    Mybatis管理事务分为三种方式: mybatis的Transaction接口 JdbcTransaction:使用JDBC的事务管理机制,利用java.sql.Connection对象完成事务的提 ...

  9. MyBatis实战【下】

    七. 动态 SQL 在 MyBatis 中提供了动态 SQL 功能.将使用 Java 代码拼接 SQL 语句,改变为在 XML 映射文件中使用标签拼接 SQL 语句. MyBatis 中动态 SQL ...

最新文章

  1. redux 入门到实践
  2. C++ 11 新特性 nullptr 学习
  3. Java技术回顾之JNDI--JNDI API
  4. 如何衡量研发效能?阿里资深技术专家提出了5组指标
  5. 信息烟尘中的学习方法
  6. pptx打不开,未安装该文件类型的文本转换程序~[解决方案]
  7. JavaEE_Spring Framework
  8. 《CCNP TSHOOT 300-135学习指南》——1.2节结构化故障检测与排除方法
  9. matlab中asix off_MATLAB试卷+答案
  10. matlab 流星雨,dijkstra算法及其matlab实现
  11. 手机浏览器都是按照什么分辨率解析移动端网页的
  12. 机器学习- 吴恩达Andrew Ng 编程作业技巧 for Week3
  13. 在Simulink中对S 函数进行参数传递的三种方法
  14. linux 关闭后台程序
  15. H3CNE GB0-191最新题库 加解释四份材料
  16. 产品经理:一个商业 AIoT 智能硬件产品的完整拆解
  17. 外贸企业邮箱那个好用,哪个企业邮箱最好用?
  18. 会计平台常见问题QA
  19. DL4J中文文档/开始/从源码构建
  20. TestFlight应用

热门文章

  1. 小波变换学习~语音端点检测
  2. Adobe Auditon使用功能(一):将音频文件切分成多段,并将每段音频分别保存到不同的文件中
  3. 文巾解题 876. 链表的中间结点
  4. 产品运营必须知道的几个概念,什么是用户群体?
  5. js title 单选框 获取_js简单获取表单中单选按钮值的方法
  6. 使用 labelImg 制作YOLO系列目标检测数据集(.xml文件)
  7. Matplotlib实例教程(十六)3D直方图
  8. Numpy Axis
  9. 凸优化中如何改进GD方法以防止陷入局部最优解
  10. Youtube推荐系统是如何挖掘用户内心另一面的