Mybatis缓存机制(一级缓存和二级缓存)

前言

为什么要学习Mybatis 缓存机制?

学习Mybatis 缓存机制,可以有效解决 数据库的压力,提高数据库的性能。

例如:你要 对tb_user 表 ,查询 所有用户的信息,并且多次查询所有用户信息。我们知道第一次查询表信息流程是 ,执行 sql 查询语句,找到存储在数据库的目标数据【在硬盘】,最后得到这些数据。那如果第二次查询,我们依旧如此,这般循环往复,如果是存储很多数据的数据库,这样一次查询,耗时是很长的,且会数据库的性能。

思考,当 第一次得到查询数据或者进行其他的操作后,能否把最终的数据临时存储在一个地方。当第二次执行相同的sql 语句时,传递相同的参数,可以直接从这地方获得,不再需要从 数据库进行二次查询。这样不仅节省了时间,还提高了性能。-------Mybatis 缓存机制 的功能


Mybatis一级缓存

一级缓存的功能

一级缓存会将第一次执行SQL语句后查询到的结果存储在缓存中。这些数据会被暂存,以便后续的查询操作可以直接从缓存中读取,而无需再次访问数据库。

  • 一级缓存 是SqlSession 级别的缓存。如果同一个SqlSession 对象 多次执行完全相同的sql 语句,在第一次执行完成后, Mybatis 会将查询结果写入一级缓存中,此后如果程序没有执行插入,更新,删除操作,当第二次执行相同的查询语句,Mybatis 会直接 读取一级缓存中的数据,而不用再去数据库查询,从而提高数据库的查询效率。

例子

从 数据表 tb_book 中多次 查询 di=1 的图书信息,使用 一级缓存 。

注意:学习 一级缓存的核心 不仅仅理解 一级缓存是临时存储场所,而且始终与数据库存储的数据保持同步------这里具体表现为数据的一致性


数据一致性的真正含义

数据一致性是指在缓存机制中,缓存中的数据与数据库中的数据始终保持一致。具体来说,这意味着:

  1. 读取操作:当从缓存中读取数据时,缓存中的数据必须是最新的,与数据库中的数据完全一致。

  2. 写入操作 :当数据库中的数据发生变化时(例如通过INSERTUPDATEDELETE操作),缓存中的数据必须及时更新或失效,以避免读取到过时的数据。


验证 数据的一致性

1 当执行相同的sql 语句时,获得相同的结果

2 存储在 一级缓存的数据,在数据库中也要存在,并且不会轻易被改变

demo(案例:验证 数据的一致性)

目的: 查询 id=1 时,图书信息

项目准备

tb_book 表 和与之对应的 实体类 Book 类

  • tb_book 表
  • 实体类 Book 类

添加 需要的依赖

  • mybatis 框架固定的依赖 + log4j 日志依赖 通过控制台直观看到整个过程
XML 复制代码
 <dependencies>
        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/log4j/log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    </dependencies>

log4j 日志依赖配套的log4j.properties 配置文件

XML 复制代码
# Set root logger level
log4j.rootLogger=DEBUG, stdout

# Set the log output format
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

BookMapper 接口方法

java 复制代码
    // 根据id查询图书
    public Book queryBookById(int id);
    // 更新图书
    public int updateBook(Book book);

正文

BookMapper.xml 映射文件

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="fs.mapper.BookMapper">
<!--    根据id 查询 图书信息-->
    <select id="queryBookById" parameterType="int" resultType="fs.entity.Book">
        select * from tb_book where id = #{id}
    </select>
<!--    根据id 修改-->
    <update id="updateBook" parameterType="fs.entity.Book">
        update tb_book set price = #{price},author = #{author},bookName = #{bookName} where id = #{id}
    </update>
</mapper>

BookTest 测试类

1 两次查询 id=1 时,图书信息
java 复制代码
        // 第一次执行
       Book book = sqlSession.selectOne("queryBookById", 1);
       System.out.println(book);
       // 第二次执行 sqlSession.selectOne("queryBookById", 1);
        Book book1 = sqlSession.selectOne("queryBookById", 1);
        System.out.println(book1);

运行截图

发现

通过运行截图和 代码发现,之后使用同样的sql 语句和传递相同的参数,会从一级缓存中直接获得,不再经过数据库。


2 查询id=1 和第二次 id=2 图书信息

BookTest 测试类

运行截图

发现

即使第二次执行相同的sql 语句,如果传递不同的参数,不能从一级缓存中获得目标结果,只能通过数据库查询


3 第一次执行 id=1 的查询 图书信息 ,第二次执行 修改 id=1 的图书名,第三次查询id=1图书信息

BookTest 测试类

java 复制代码
 // 第一次执行
       Book book = sqlSession.selectOne("queryBookById", 1);
       System.out.println(book);
     // 第二次更新
        int update = sqlSession.update("updateBook", new Book(1, "author", "Html基础","67"));
        sqlSession.commit();
        // 第二次执行 sqlSession.selectOne("queryBookById", 1);
        Book book1 = sqlSession.selectOne("queryBookById", 1);
        System.out.println(book1);
        sqlSession.close();

运行截图

最后我们发现:

一级缓存实质上是在内存开辟一个空间来存储数据的。

一级缓存不需要,我们手动创建或者设置的


Mybatis二级缓存

为什么要学习 Mybatis二级缓存?

学完一级缓存后,发现 在相同的Mapper 映射文件中使用相同的SQL语句,如果sqlSession不同,则两个sqlSession查询数据时,会查询数据两次,这样会降低数据库的查询效率。因此为了解决这个问题 使用 Mybatis二级缓存

Mybatis二级缓存 的优点

二级缓存 是一个跨多个 SqlSession 的共享缓存机制,它的作用范围比一级缓存更广泛 。二级缓存可以显著提高系统的性能,因为它允许在多个会话之间共享缓存数据,从而减少对数据库的重复查询。


二级缓存的工作原理

  1. 查询流程

    • 当执行查询操作时,MyBatis 首先检查一级缓存。

    • 如果一级缓存中没有找到数据,则检查二级缓存。

    • 如果二级缓存中也没有找到数据,则查询数据库,并将结果存入二级缓存。

  2. 更新流程

    • 当执行数据更新操作(如 INSERTUPDATEDELETE)时,MyBatis 会清空相关的一级缓存和二级缓存,以确保数据一致性。

手动打开二级缓存的步骤

1 在主配置文件 mybatis-config .xml 开启二级 缓存的全局配置

XML 复制代码
    <settings>
<!--        开启日志-->
    <setting name="logImpl" value="LOG4J"/>
<!--        开启二级缓存-->
    <setting name="cacheEnabled" value="true"/>
</settings>

2 在 Mapper XML 文件中,使用 <cache> 标签配置二级缓存。

二级缓存一般是默认状态,可以实现的功能如下

1 映射文件所有的select 语句将会被缓存

2 映射文件中的所有的insert,update,delete 都会被刷新缓存

3 缓存会使用LRU算法回收

4 没有刷新间隔

5 缓存是可读可写的,但不被共享

以上是默认状态的特性,如果需要调整,可通过cache 元素的属性来实现

配置参数说明:
  • eviction:缓存回收策略。可选值包括:

    • FIFO(先进先出)

    • LRU(最近最少使用)

    • SOFT(软引用)

    • WEAK(弱引用)

  • flushInterval:缓存刷新间隔(毫秒)。默认值为不刷新。

  • size:缓存的最大存储对象数量。

  • readOnly :是否只读。如果设置为 true,则缓存中的数据被认为是只读的,可以提高性能,但不允许修改。


3 . 实体类实现 Serializable

4 BookTest测试类中 两次查询 id=1 的图书信息

java 复制代码
package fs.Test;

import fs.entity.Book;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

public class BookTest {
    public static void main(String[] args) throws IOException {
        InputStream resource = Resources.getResourceAsStream("mybatis-confif.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resource);
        SqlSession sqlSession = build.openSession();
        SqlSession sqlSession1 = build.openSession();
        // 第一次执行
       Book book = sqlSession.selectOne("queryBookById", 3);
       System.out.println(book);
       sqlSession.close();
       // 第二次执行
       Book book1 = sqlSession1.selectOne("queryBookById", 3);
        System.out.println(book1);
        sqlSession1.close();

    }
}

运行截图

补充
  • 缓存命中率

缓存命中率的计算公式

缓存命中率通常通过以下公式计算: 缓存命中率=总查询次数缓存命中次数​

  • 缓存命中次数:从缓存中成功获取到数据的次数。

  • 总查询次数:所有查询操作的次数,包括从缓存中获取数据和从数据库中查询数据的次数。


示例

假设你进行了 10 次查询操作:

  • 3 次查询直接从缓存中获取到了数据(缓存命中)。

  • 7 次查询需要从数据库中查询数据(缓存未命中)。

那么缓存命中率为: 缓存命中率=103​=0.3 或 30%


MyBatis 中的缓存命中率

在 MyBatis 的日志中,你可以看到类似以下的输出:

DEBUG BookMapper:60 - Cache Hit Ratio [fs.mapper.BookMapper]: 0.0

这表示当前的缓存命中率为 0.0,即尚未有缓存命中。


为什么缓存命中率是 0.0?

  1. 缓存尚未生效

    • 如果是第一次查询,缓存尚未填充,因此命中率为 0。

    • 如果二级缓存未正确配置(如实体类未实现 Serializable 接口,或 <cache> 标签未正确设置),缓存可能不会生效。

  2. 查询条件不匹配

    • 缓存基于查询语句和参数进行匹配。如果查询语句或参数不同,即使使用了缓存,也可能无法命中。
  3. 缓存被清空

    • 如果在两次查询之间执行了数据更新操作(如 INSERTUPDATEDELETE),二级缓存可能会被清空,导致命中率为 0。
相关推荐
程序员小假20 分钟前
我们来说一说 ThreadLocal 内存泄漏
java·后端
xq952729 分钟前
获取Facebook 散列利器 来了 十六进制到 Base64 转换器
java
我不是混子1 小时前
聊聊Spring事件机制
java·后端
DKPT1 小时前
JVM栈溢出时如何dump栈信息?
java·jvm·笔记·学习·spring
DKPT1 小时前
JVM堆大小如何设置?
java·开发语言·jvm·笔记·学习
铅笔侠_小龙虾1 小时前
JVM 目录
java·jvm
yunxi_051 小时前
让大模型会“说话”:基于 Spring WebSocket 的毫秒级流式 RAG 对话
java·后端
用户6120414922131 小时前
jsp+servlet做的医院挂号看诊管理系统
java·javascript·mysql
€8111 小时前
Java入门级教程21——Java 缓存技术、RMI远程方法调用、多线程分割大文件
java·开发语言·java缓存代理模式的实现·java rmi远程方法调用·多线程分割大文件
渣哥1 小时前
Java线程池那些坑:我与线程池的恩怨情仇
java