【MyBatis学习笔记 八】MyBatis两级缓存机制

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 【MyBatis学习笔记 八】MyBatis两级缓存机制

早在去年12月份的一篇Blog中【MySQL数据库原理 一】MySQL架构及查询语句执行流程探索MySQL的执行步骤中就提到过查询缓存这一个概念:

并且提到过其实不建议使用查询缓存,正因为如此,我们才不把缓存做到数据库,这样作为服务端的数据库缓存了各个客户端大量查询结果能用的比例却比较低,性价比不高;反之大多数应用都把缓存做到了应用逻辑层,简单的如一个map的MyBatis,由客户端自己定义策略。

缓存的基本概念

什么是缓存,缓存就是存储在内存中的临时数据。将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题。使用缓存的目的就是减少和数据库的交互次数,减少系统开销,提高系统效率,但是一定要注意,缓存存储的只能是:经常查询并且不经常改变的数据

MyBatis两级缓存

MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率,MyBatis默认定义了两级缓存:一级缓存和二级缓存

  • 一级缓存,SqlSession级别的缓存,也称为本地缓存,默认情况下,只有一级缓存开启。
  • 二级缓存,基于namespace级别的缓存,也称为全局缓存,需要手动开启和配置。

其中,为了提高扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存,整体的结构图如下:

我们使用之前的Person进行相关的测试,看看一次会话期间的一级缓存是如何实现的。PersonDao查询语句如下

Person getPersonByIdResult(@Param("id")int id);

xml中的SQL配置如下:

<select id="getPersonByIdResult" resultMap="PersonBankAccountByResultMap">
        select p.id id,p.username,username,p.password password,p.age age,p.phone phone,p.email email,p.hobby hobby,ba.id bid,ba.bank_name bank_name,ba.account_name account_name,ba.person_id person_id
        from person p,bank_Account ba
        where p.id=ba.person_id and p.id=#{id}
    </select>
    <resultMap id="PersonBankAccountByResultMap" type="com.example.MyBatis.dao.model.Person">
        <!-- id为主键 -->
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="age" property="age"/>
        <result column="phone" property="phone"/>
        <!-- column写错会因为查到数据匹配不上,给默认值, type写错会报错,找不到属性-->
        <result column="email" property="email"/>
        <!-- column是数据库表的列名 , property是对应实体类的属性名 -->
        <result column="hobby" property="interests"/>
        <collection  property="bankAccounts" javaType="ArrayList" ofType="com.example.MyBatis.dao.model.BankAccount">
            <result column="bid" property="id"  />
            <result column="bank_name" property="bankName"/>
            <result column="account_name" property="accountName"/>
            <result column="person_id" property="personId"/>
        </collection>
    </resultMap>

MyBatis一级缓存

一级缓存表示会话缓存,也就是SqlSession缓存,在一次会话中的查询数据会被缓存下来。

一级缓存测试

我们编写测试类来查看下一级缓存是否生效

@Test
    public void testGetPersonByIdResultFirstCache() {
        //1.获取SqlSession对象
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        //2.执行SQL
        PersonDao personDao = sqlSession.getMapper(PersonDao.class);
        Person person = personDao.getPersonByIdResult(1);
        System.out.println(person);
        Person person2 = personDao.getPersonByIdResult(1);
        System.out.println(person2);
        //关闭sqlSession
        sqlSession.close();
    }

日志打印结果如下:

查询同样的数据,sql语句只被执行了一次,第二次是从缓存中获取的。

一级缓存失效情况

一级缓存是SqlSession级别的缓存,是一直开启的,我们关闭不了它。但有以下几种情况一级缓存会自动失效:

1 SqlSession不同导致缓存失效

两次请求的sqlSession不同导致缓存失效,这个很容易理解,因为一级缓存的作用域就在一次会话内嘛

@Test
    public void testGetPersonByIdResultFirstCache() {
        //1.获取SqlSession对象
        SqlSession sqlSession1 = MybatisUtils.getSqlSession();
        SqlSession sqlSession2 = MybatisUtils.getSqlSession();
        //2.执行SQL
        PersonDao personDao1 = sqlSession1.getMapper(PersonDao.class);
        PersonDao personDao2 = sqlSession2.getMapper(PersonDao.class);
        Person person = personDao1.getPersonByIdResult(1);
        System.out.println(person);
        Person person2 = personDao2.getPersonByIdResult(1);
        System.out.println(person2);
        //关闭sqlSession
        sqlSession1.close();
        sqlSession2.close();
    }

从日志可以看到进行了两次查询

2 查询条件不同导致缓存失效

查询条件不同导致缓存失效,这个也很容易理解,因为两次查询的数据不同,第二次查询获取到的数据没有出现在第一次查询后缓存里

@Test
    public void testGetPersonByIdResultFirstCache() {
        //1.获取SqlSession对象
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        //2.执行SQL
        PersonDao personDao = sqlSession.getMapper(PersonDao.class);
        Person person = personDao.getPersonByIdResult(1);
        System.out.println(person);
        Person person2 = personDao.getPersonByIdResult(2);
        System.out.println(person2);
        //关闭sqlSession
        sqlSession.close();
    }

从日志可以看到进行了两次查询

3 两次查询间执行增删改操作导致缓存失效

同MySQL之前的查询缓存一样,只要表在两次查询之间发生了变更,那么这个变更语句会清空查询缓存,这也是MySQL弃用查询缓存的原因:

@Test
    public void testGetPersonByIdResultFirstCache() {
        //1.获取SqlSession对象
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        //2.执行SQL
        PersonDao personDao = sqlSession.getMapper(PersonDao.class);
        Person person = personDao.getPersonByIdResult(1);
        System.out.println(person);
        int result= personDao.deletePerson(0);
        System.out.println(result);
        Person person2 = personDao.getPersonByIdResult(1);
        System.out.println(person2);
        //关闭sqlSession
        sqlSession.close();
    }

从日志可以看到进行了两次查询

4 手动清除SqlSession缓存导致缓存失效

这个也很容易理解,当我们手动clear掉SqlSession缓存时,缓存自然而然也被处理掉了:

@Test
    public void testGetPersonByIdResultFirstCache() {
        //1.获取SqlSession对象
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        //2.执行SQL
        PersonDao personDao = sqlSession.getMapper(PersonDao.class);
        Person person = personDao.getPersonByIdResult(1);
        System.out.println(person);
        sqlSession.clearCache();
        Person person2 = personDao.getPersonByIdResult(1);
        System.out.println(person2);
        //关闭sqlSession
        sqlSession.close();
    }

从日志可以看到进行了两次查询

MyBatis二级缓存

二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存,基于namespace级别的缓存,一个命名空间,对应一个二级缓存,也就是一个Mapper对应一个二级缓存。工作机制:

  1. 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中
  2. 如果当前会话关闭了,这个会话对应的一级缓存就没了,一级缓存中的数据被保存到二级缓存中
  3. 新的会话查询信息,可以从二级缓存中获取内容;
  4. 不同的mapper查出的数据会放在自己对应的缓存(map)

我们来看下二级缓存的实现方式:

1 开启全局缓存

首先需要在全局配置文件mybatis-config.xml中打开全局缓存,也就是二级缓存:

<settings>
        <setting name="logImpl" value="log4j"/>
        <setting name="cacheEnabled" value="true"/>  //打开全局缓存
    </settings>

2 PersonMapper中开启全局缓存

personMapper.xml的配置文件中加入:

<!--创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,
     而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突-->
    <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

3 测试二级缓存

只有在一级缓存会话关闭或者提交后,缓存数据才会被提交到二级缓存,我们模拟下这个实现:

@Test
    public void testGetPersonByIdResultSecondCache() {
        //1.获取SqlSession对象
        SqlSession sqlSession1 = MybatisUtils.getSqlSession();
        SqlSession sqlSession2 = MybatisUtils.getSqlSession();
        //2.执行SQL
        PersonDao personDao1 = sqlSession1.getMapper(PersonDao.class);
        PersonDao personDao2 = sqlSession2.getMapper(PersonDao.class);
        Person person = personDao1.getPersonByIdResult(1);
        System.out.println(person);
        //会话1关闭,会话2执行查询
        sqlSession1.close();
        Person person2 = personDao2.getPersonByIdResult(1);
        System.out.println(person2);
        //关闭sqlSession
        sqlSession1.close();
        sqlSession2.close();
    }

查看日志发现只进行了一次查询,全局缓存生效

EhCache实现二级缓存

Ehcache是一种广泛使用的java分布式缓存,用于二级全局缓存。要在应用程序中使用Ehcache,需要引入依赖的jar包,输入如下Maven坐标引入:

<dependency>
            <groupId>org.mybatis.caches</groupId>
            <artifactId>mybatis-ehcache</artifactId>
            <version>1.2.1</version>
        </dependency>

然后在对应的Mapper中,我们这里是personMapper.xml文件中加入该缓存支持,同时注释原有二级缓存配置:

<!--创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突-->
    <!--<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>-->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

代码测试结果同原有二级缓存测试。

总结一下

MySQL其实不建议使用查询缓存,同时8.X以后版本也移除了该模块。正因为如此,我们才不把缓存做到数据库,作为服务端的数据库缓存了各个客户端大量查询结果只要进行一次全表更新就失效,能用的比例非常低,性价比不高;反之大多数应用都把缓存做到了应用逻辑层,简单的如一个map的MyBatis,由客户端自己定义策略,缓存由框架自己实现,MySQL没有负担,客户端也能灵活定义自己的配置。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
缓存 Java 数据库连接
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
文章介绍了MyBatis的缓存机制,包括一级缓存和二级缓存的配置和使用,以及如何整合第三方缓存EHCache。详细解释了一级缓存的生命周期、二级缓存的开启条件和配置属性,以及如何通过ehcache.xml配置文件和logback.xml日志配置文件来实现EHCache的整合。
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
|
3月前
|
缓存 应用服务中间件 nginx
Web服务器的缓存机制与内容分发网络(CDN)
【8月更文第28天】随着互联网应用的发展,用户对网站响应速度的要求越来越高。为了提升用户体验,Web服务器通常会采用多种技术手段来优化页面加载速度,其中最重要的两种技术就是缓存机制和内容分发网络(CDN)。本文将深入探讨这两种技术的工作原理及其实现方法,并通过具体的代码示例加以说明。
348 1
|
8天前
|
SQL Java 数据库连接
Mybatis架构原理和机制,图文详解版,超详细!
MyBatis 是 Java 生态中非常著名的一款 ORM 框架,在一线互联网大厂中应用广泛,Mybatis已经成为了一个必会框架。本文详细解析了MyBatis的架构原理与机制,帮助读者全面提升对MyBatis的理解和应用能力。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Mybatis架构原理和机制,图文详解版,超详细!
|
17天前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
18天前
|
SQL 缓存 Java
MyBatis如何关闭一级缓存(分注解和xml两种方式)
MyBatis如何关闭一级缓存(分注解和xml两种方式)
52 5
|
1月前
|
缓存 Java 数据库连接
使用MyBatis缓存的简单案例
MyBatis 是一种流行的持久层框架,支持自定义 SQL 执行、映射及复杂查询。本文介绍了如何在 Spring Boot 项目中集成 MyBatis 并实现一级和二级缓存,以提高查询性能,减少数据库访问。通过具体的电商系统案例,详细讲解了项目搭建、缓存配置、实体类创建、Mapper 编写、Service 层实现及缓存测试等步骤。
|
1月前
|
存储 缓存 负载均衡
Nginx代理缓存机制
【10月更文挑战第2天】
71 4
|
1月前
|
存储 缓存 NoSQL
深入理解后端缓存机制的重要性与实践
本文将探讨在后端开发中缓存机制的应用及其重要性。缓存,作为提高系统性能和用户体验的关键技术,对于后端开发来说至关重要。通过减少数据库访问次数和缩短响应时间,缓存可以显著提升应用程序的性能。本文将从缓存的基本概念入手,介绍常见的缓存策略和实现方式,并通过实例展示如何在后端开发中有效应用缓存技术。最后,我们将讨论缓存带来的一些挑战及其解决方案,帮助您在实际项目中更好地利用缓存机制。
|
2月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
70 8
|
2月前
|
缓存 Java Python
python垃圾回收&缓存机制
python垃圾回收&缓存机制