什么是缓存

缓存就是内存中的一个对象,用于对数据库查询结果的保存,用于减少与数据库的交互次数从而降低数据库的压力,进而提高响应速度。

MyBatis 缓存机制原理

Mybatis 缓存机制原理是将第一次从数据库 SQL 查询的结果数据保存到缓存(内存中),当下一次 SQL 查询和第一次相同,如果缓存中有数据则直接获取,而不用再从数据库获取,从而减少数据库访问频率,大大提升数据库性能。

MyBatis 一级缓存

一级缓存是 Sqlssion 级别的缓存。

在操作数据库时需要构造 SqlSession 对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。

不同的 SqlSession 对象之间的缓存数据是互相不影响的。

一级缓存的作用范围是同一个 SqlSession 对象,在同一个 SqlSession 对象中再次执行相同的 SQL 语句,第一次执行完毕会将数据库中查询的数据写到缓存,第二次会从缓存中获取数据,不必再从数据库中查询,从而提升查询效率。当 SqlSession 对象释放后,该 SqlSession 对象中的一级缓存也就不存在了。

MyBatis 默认开启一级缓存,并且无法关闭

一级缓存工作原理如下:

一级缓存满足条件如下:

  • 同一个 SqlSession 对象
  • 相同的 SQL 语句和参数

注:使用 SqlSession.clearCache( ) 方法可以强制清除一级缓存

测试 MyBatis 一级缓存

既然每个 SqlSession 都会有自己的一个缓存,那么我们用同一个 SqlSession 是不是就能感受到一级缓存的存在呢?调用多次 getMapper 方法,生成对应的SQL语句,判断每次SQL语句是从缓存中取还是对数据库进行操作,下面的例子来证明一下

		@Test
public void cacheTest() {
List<UserEntity> userEntities = userMapper.selectUserByAge(20);
System.out.println(userEntities); List<UserEntity> userEntities2 = userMapper.selectUserByAge(20);
System.out.println(userEntities2); List<UserEntity> userEntities3 = userMapper.selectUserByAge(20);
System.out.println(userEntities3);
}

执行测试,输出结果如下:

2020-08-10 16:14:44,790 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==>  Preparing: select * from tb_user where age > ?
2020-08-10 16:14:44,837 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==> Parameters: 20(Integer)
2020-08-10 16:14:44,884 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] <== Total: 7
[UserEntity{id=1, userName='张三', password='123456', name='张三', age=22, sex=1, birthday=Sun Sep 02 00:00:00 IRKST 1990, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null}
[UserEntity{id=1, userName='张三', password='123456', name='张三', age=22, sex=1, birthday=Sun Sep 02 00:00:00 IRKST 1990, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null}

可以看到,连续执行三次查询 SQL 语句,但只打印了一条 SQL 语句,其他两条 SQL 语句都是从缓存中查询的,所以它们生成了相同的 UsereEntity 对象。

接着我在第一条和第二条 SQL语句 之间插入更新的 SQL 语句,代码如下:

    @Test
public void cacheTest() {
List<UserEntity> userEntities = userMapper.selectUserByAge(20);
System.out.println(userEntities); int result = userMapper.updateUser(1,"张三");
sqlSession.commit(); List<UserEntity> userEntities2 = userMapper.selectUserByAge(20);
System.out.println(userEntities2);
}

执行测试,结果如下:

020-08-10 16:20:47,384 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==>  Preparing: select * from tb_user where age > ?
2020-08-10 16:20:47,431 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==> Parameters: 20(Integer)
2020-08-10 16:20:47,478 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] <== Total: 7
[UserEntity{id=1, userName='张三', password='123456', name='张三', age=22, sex=1, birthday=Sun Sep 02 00:00:00 IRKST 1990, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null}
2020-08-10 16:20:47,478 [main] [mapper.UserMapper.updateUser]-[DEBUG] ==> Preparing: update tb_user set name=? where id=?
2020-08-10 16:20:47,478 [main] [mapper.UserMapper.updateUser]-[DEBUG] ==> Parameters: 张三(String), 1(Integer)
2020-08-10 16:20:47,478 [main] [mapper.UserMapper.updateUser]-[DEBUG] <== Updates: 1
2020-08-10 16:20:47,493 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==> Preparing: select * from tb_user where age > ?
2020-08-10 16:20:47,493 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==> Parameters: 20(Integer)
2020-08-10 16:20:47,509 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] <== Total: 7
[UserEntity{id=1, userName='张三', password='123456', name='张三', age=22, sex=1, birthday=Sun Sep 02 00:00:00 IRKST 1990, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null},
[UserEntity{id=1, userName='张三', password='123456', name='张三', age=22, sex=1, birthday=Sun Sep 02 00:00:00 IRKST 1990, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null},

可以看到,在两次查询 SQL 语句中使用插入 SQL 语句,会对一级缓存进行刷新,会导致一级缓存失效。

我们知道一级缓存就是 SqlSession 级别的缓存,而同一个 SqlSession 会有相同的一级缓存,那么使用不同的 SqlSession 是不是会对一级缓存产生影响呢?

		@Test
public void cacheTest() {
List<UserEntity> userEntities = userMapper.selectUserByAge(20);
System.out.println(userEntities); UserMapper userMapper2
= sqlSessionFactory.openSession().getMapper(UserMapper.class);
List<UserEntity> userEntities2 = userMapper2.selectUserByAge(20);
System.out.println(userEntities2);

执行测试,结果如下:

2020-08-10 16:26:36,243 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==>  Preparing: select * from tb_user where age > ?
2020-08-10 16:26:36,290 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==> Parameters: 20(Integer)
2020-08-10 16:26:36,322 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] <== Total: 7
[UserEntity{id=1, userName='张三', password='123456', name='张三', age=22, sex=1, birthday=Sun Sep 02 00:00:00 IRKST 1990, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null}
2020-08-10 16:26:36,337 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==> Preparing: select * from tb_user where age > ?
2020-08-10 16:26:36,337 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==> Parameters: 20(Integer)
2020-08-10 16:26:36,353 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] <== Total: 7
[UserEntity{id=1, userName='张三', password='123456', name='张三', age=22, sex=1, birthday=Sun Sep 02 00:00:00 IRKST 1990, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null}

上面代码使用了不同的 SqlSession 对同一个 SQL 语句执行了相同的查询操作,却对数据库执行了两次相同的查询操作,生成了不同的 UserEnity 对象,由此可见,不同的 SqlSession 是肯定会对一级缓存产生影响的。

//手动清除一级缓存方法
sqlSession.clearCache();

在开启一级缓存时,当有两个 SqlSession 对象存在,一个用于查询数据,一个用于更新数据,如果查询和更新是同一张表的相同数据,这时可能会出现数据脏读,而解决办法是查询时手动清空缓存。

MyBatis 二级缓存

二级缓存是 Mapper 级别的缓存。

多个 SqlSession 对象 SQL 语句查询数据库结果会存放二级缓存区域,而多个SqlSession对象可以共用二级缓存。

二级缓存是多个 SqlSesion 对象共用的。

其作用范围是 Mapper 的同一个 namespace ,不同的 SqlSession 对象再次执行相同 namepace 下的 SQL 语句,第一次执行会将数据库中查询结果数据存储到二级缓存中,第二次会从二级缓存中获取数据,而不再从数据库中获取,从而提高查询效率。

MyBatis 二级缓存默认关闭,需要手动开启二级缓存。

MyBatis 的二级缓存是 Mapper 范围级别,除了在 MyBatis 环境配置 mybatis-config.xml 设置二级缓存总开关,还要在具体的 mapper.xml 中加入 标签。

步骤如下:

  1. mybatis-config.xml 设置二级缓存总开关

    <settings>
    <!-- 开启二级缓存 -->
    <setting name="cacheEnabled" value="true" />
    </settings>
  2. 在具体的 mapper.xml 中加扩标签

    <mapper>
    <!--开启二级缓存(表示对哪个mapper 开启缓存)-->
    <cache />
    </mapper>

    设置 cache 标签的属性

    cache 标签有多个属性

    • eviction: 缓存回收策略

      • LRU - 最近最少回收,移除最长时间不被使用的对象(默认)
      • FIFO - 先进先出,按照缓存进入的顺序来移除它们
      • SOFT - 软引用,移除基于垃圾回收器状态和软引用规则的对象
      • WEAK - 弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象
    • flushinterval:缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值

    • readOnly: 是否只读;true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改

    • size : 缓存存放多少个元素

    • type: 指定自定义缓存的全类名(实现Cache 接口即可)

    • blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

    注:开启二级缓存后,MyBatis要求返回的实体类对象必须是可序列化的

    开启二级缓存后,在不同 SqlSesion 下执行相同查询 SQL 语句,代码如下:

    		@Test
    public void cacheTest() {
    List<UserEntity> userEntities = userMapper.selectUserByAge(20);
    System.out.println(userEntities);
    sqlSession.commit(); //提交SQL语句到数据库返回查询结果 UserMapper userMapper2
    = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    List<UserEntity> userEntities2 = userMapper2.selectUserByAge(20);
    System.out.println(userEntities2);
    }

    执行测试,结果如下 :

2020-08-10 16:41:43,119 [main] [mapper.UserMapper]-[DEBUG] Cache Hit Ratio [mapper.UserMapper]: 0.0
2020-08-10 16:41:43,509 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==> Preparing: select * from tb_user where age > ?
2020-08-10 16:41:43,572 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] ==> Parameters: 20(Integer)
2020-08-10 16:41:43,603 [main] [mapper.UserMapper.selectUserByAge]-[DEBUG] <== Total: 7
[UserEntity{id=1, userName='张三', password='123456', name='张三', age=22, sex=1, birthday=Sun Sep 02 00:00:00 IRKST 1990, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null},
2020-08-10 16:41:43,634 [main] [mapper.UserMapper]-[DEBUG] Cache Hit Ratio [mapper.UserMapper]: 0.5
[UserEntity{id=1, userName='张三', password='123456', name='张三', age=22, sex=1, birthday=Sun Sep 02 00:00:00 IRKST 1990, created='2020-06-17 09:30:58.0', updated='2020-06-17 09:30:58.0', interests=null},

通过结果可以得知,首次执行的SQL语句是从数据库中查询得到的结果,然后第一个 SqlSession 执行提交,第二个 SqlSession 执行相同的查询后是从二级缓存中查取的。

值得注意的是,SqlSession 在未提交的时候,SQL 语句产生的查询结果还没有放入二级缓存中,这个时候 SqlSession2 在查询的时候是感受不到二级缓存的存在的。

与一级缓存一样,更新操作很可能对二级缓存造成影响。

多表操作对二级缓存也会产生影响,如下:

GameMapper.java 文件添加一个接口方法,代码如下:

@Update("update tb_game set name = #{name} where id = #{id}")
public int updateGameNameById(@Param("name") String name, @Param("id") int id);

在测试 MyBatisTest.java中添加一个测试方法,代码 如下:

		@Test
public void cacheJoinTableTest() {
GameEntity gameEntity = gameMapper.selectGameByName("英雄联盟");
System.out.println(gameEntity); SqlSession sqlSession2 = sqlSessionFactory.openSession();
GameMapper gameMapper2 = sqlSession2.getMapper(GameMapper.class);
gameMapper2.updateGameNameById("王者荣耀",1);
sqlSession2.commit(); gameEntity = gameMapper.selectGameByName("英雄联盟");
System.out.println(gameEntity);
}

执行测试,结果如下:

2020-08-10 22:58:51,867 [main] [mapper.GameMapper]-[DEBUG] Cache Hit Ratio [mapper.GameMapper]: 0.0
2020-08-10 22:58:52,289 [main] [mapper.GameMapper.selectGameByName]-[DEBUG] ==> Preparing: select * from tb_game where name =?
2020-08-10 22:58:52,320 [main] [mapper.GameMapper.selectGameByName]-[DEBUG] ==> Parameters: 英雄联盟(String)
2020-08-10 22:58:52,367 [main] [mapper.GameMapper]-[DEBUG] Cache Hit Ratio [mapper.GameMapper]: 0.0
2020-08-10 22:58:52,367 [main] [mapper.GameMapper.selectAccountById]-[DEBUG] ====> Preparing: select * from tb_account where id=?
2020-08-10 22:58:52,367 [main] [mapper.GameMapper.selectAccountById]-[DEBUG] ====> Parameters: 1(Integer)
2020-08-10 22:58:52,367 [main] [mapper.GameMapper.selectAccountById]-[DEBUG] <==== Total: 1
2020-08-10 22:58:52,367 [main] [mapper.GameMapper.selectGameByName]-[DEBUG] <== Total: 1
GameEntity{id=1, name='英雄联盟', type='MOBA', operator='腾讯游戏', accounts=[AccountEntity{id=1, userName='潇洒哥', password='12345'}]}
2020-08-10 22:58:52,382 [main] [mapper.GameMapper.updateGameNameById]-[DEBUG] ==> Preparing: update tb_game set name = ? where id = ?
2020-08-10 22:58:52,382 [main] [mapper.GameMapper.updateGameNameById]-[DEBUG] ==> Parameters: 王者荣耀(String), 1(Integer)
2020-08-10 22:58:52,398 [main] [mapper.GameMapper.updateGameNameById]-[DEBUG] <== Updates: 1
2020-08-10 22:58:52,398 [main] [mapper.GameMapper]-[DEBUG] Cache Hit Ratio [mapper.GameMapper]: 0.0
GameEntity{id=1, name='英雄联盟', type='MOBA', operator='腾讯游戏', accounts=[AccountEntity{id=1, userName='潇洒哥', password='12345'}]}

在对 tb_game 表执行了一次更新后,再次进行联查,发现数据库中查询出的还是游戏名仍是王者荣耀,也就是说,最后一次联查实际上查询的是第一次查询结果的缓存,而不是从数据库中查询得到的值,这样就读到了脏数据

如果是两个 mapper 命名空间的话,解决办法是可以使用 cache-ref 来把一个命名空间指向另外一个命名空间,从而消除上述的影响,再次执行,就可以查询到正确的数据。

一级缓存和二级缓存的区别

二级缓存是 Mapper 级别,一级缓存是 SqlSession 级别,多个 SqlSession 级别的一级缓存可以共享一个 Mapper 级别的二级缓存。

当开启二级缓存后,数据的查询执行的流程是二级缓存 -> 一级缓存 -> 数据库

MyBatis 缓存使用注意事项

  1. 缓存是以namespace为单位的,不同namespace下的操作互不影响。

  2. insert,update,delete操作会清空所在namespace下的全部缓存。

  3. 通常使用MyBatis Generator 生成的代码中,都是各个表独立的,每个表都有自己的namespace

  4. 多表操作一定不要使用二级缓存,因为多表操作进行更新操作,一定会产生脏数据。

如果你遵守二级缓存的注意事项,那么你就可以使用二级缓存。但是,如果不能使用多表操作,二级缓存不就可以用一级缓存来替换掉吗?而且二级缓存是表级缓存,开销大,没有一级缓存直接使用 HashMap 来存储的效率更高,所以二级缓存并不推荐使用

MyBatis 缓存机制(十三)的更多相关文章

  1. mybatis缓存机制

    目录 mybatis缓存机制 Executor和缓存 一级缓存 小结 二级缓存 小结 mybatis缓存机制 mybatis支持一.二级缓存来提高查询效率,能够正确的使用缓存的前提是熟悉mybatis ...

  2. 聊聊MyBatis缓存机制【美团-推荐】

    聊聊MyBatis缓存机制 2018年01月19日 作者: 凯伦 文章链接 18778字 38分钟阅读 前言 MyBatis是常见的Java数据库访问层框架.在日常工作中,开发人员多数情况下是使用My ...

  3. 《深入理解mybatis原理4》 MyBatis缓存机制的设计与实现

    <深入理解mybatis原理> MyBatis缓存机制的设计与实现 本文主要讲解MyBatis非常棒的缓存机制的设计原理,给读者们介绍一下MyBatis的缓存机制的轮廓,然后会分别针对缓存 ...

  4. Mybatis缓存机制及mybatis的各个组成部分

    Mybatis 一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 ...

  5. 聊聊MyBatis缓存机制

    https://tech.meituan.com/mybatis_cache.html 前言 MyBatis是常见的Java数据库访问层框架.在日常工作中,开发人员多数情况下是使用MyBatis的默认 ...

  6. 【转】MyBatis缓存机制

    转载:https://blog.csdn.net/bjweimengshu/article/details/79988252. 本文转载自公众号 美团技术点评 前言 MyBatis是常见的Java数据 ...

  7. 开源框架是如何使用设计模式的-MyBatis缓存机制之装饰者模式

    写在前面 聊一聊MyBatis是如何使用装饰者模式的,顺便回顾下缓存的相关知识,可以看看右侧目录一览内容概述. 装饰者模式 这里就不了它的概念了,总结下就是套娃.利用组合的方式将装饰器组合进来,增强共 ...

  8. MyBatis缓存机制-二级缓存

    MyBatis二级缓存是基于namespace级别的缓存. 1.MyBatis的缓存机制整体设计以及二级缓存的工作模式 如上图所示,当开一个会话时,一个SqlSession对象会使用一个Executo ...

  9. Mybatis——缓存机制

    MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制.缓存可以极大的提升查询效率. MyBatis系统中默认定义了两级缓存. 一级缓存和二级缓存. 1.默认情况下,只有一级缓存( ...

随机推荐

  1. gateway调用Fegin失败问题解决

    起因 新项目用的是springcloud2.0,网关用gateway替换了zuul. gateway动态路由跟zuul有本质上的区别.这就涉及到webflux这一套大东东了.简单来说,gateway是 ...

  2. 002-Java的标识符和关键字

    目录 一.标识符 1.什么是标识符 2.标识符的命名规则 3.标识符的命名规范 二.关键字 1.什么是关键字 2.Java中的关键字 一.标识符 1.什么是标识符   标识符就是程序员自己规定的代表一 ...

  3. Kubernetes 普及系列:容器基础入门

    随着云原生时代的来临,云以及分布式计算已经是时下最受欢迎的技术之一了.其中 Docker 作为最知名的容器平台,到底有着怎样的魅力来让其无人不知无人不晓?废话不多说,让我们开始逐层掀开容器技术的神秘面 ...

  4. .Net之配置文件

    1. 说明 默认情况下读取配置Configuration的默认优先级:ConfigureAppConfiguration(自定义读取)>CommandLine(命令行参数)>Environ ...

  5. 100多个很有用的JavaScript函数以及基础写法大集合

    100多个很有用的JavaScript函数以及基础写法大集合 1.document.write("");为 输出语句2.JS中的注释为//3.传统的HTML文档顺序是:docume ...

  6. php图片合成【png图片】

    php 图片合成[png图片] 示例代码 <?php header("Content-type:text/html;charset=utf-8"); error_report ...

  7. 【JDK8】Java8 Stream流API常用操作

    Java版本现在已经发布到JDK13了,目前公司还是用的JDK8,还是有必要了解一些JDK8的新特性的,例如优雅判空的Optional类,操作集合的Stream流,函数式编程等等;这里就按操作例举一些 ...

  8. hdu1316 水大数

    题意:      给你一个区间,问这个区间有多少个斐波那契数. 思路:      水的大数,可以直接模拟,要是懒可以用JAVA,我模拟的,打表打到1000个就足够用了... #include<s ...

  9. 数据库的读写分离(Amoeba)

    目录 Amoeba Amoeba读写分离的配置 Amoeba Amoeba(变形虫) 项目,该开源框架于2008年开始发布一款 Amoeba for Mysql软件. 这个软件基于Java致力于MyS ...

  10. 【python】Leetcode每日一题-最长公共子序列

    [python]Leetcode每日一题-最长公共子序列 [题目描述] 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度.如果不存在 公共子序列 ,返回 0 . ...