MyBatis缓存与分页插件:从一级二级缓存到底层原理与PageHelper实战

第一部分:缓存的意义

1.1 为什么需要缓存

⭐ 老师强调 :数据库数据全部存在磁盘 里,因为内存断电就丢、无法永久保存;但CPU不直接读磁盘(太慢),而是从内存读写,内存再和磁盘交互。

缓存的本质: 弥补磁盘慢、内存易失的矛盾------把常用数据放内存加速访问。

存储位置 特点
磁盘 持久化保存,速度慢
内存 临时高速访问,断电丢失
缓存 内存中的临时存储,减少磁盘访问

⭐ 老师强调:查学生信息(如id=1)时,数据经磁盘读入内存再给CPU,无缓存机制就得每次从磁盘重读,很麻烦,所以引入缓存。

1.2 查找缓存流程

复制代码
查询请求
    │
    ▼
先查缓存 → 有?→ 直接返回
    │
    └── 无?→ 查数据库 → 写入缓存 → 返回

⭐ 老师强调:MyBatis缓存机制是:查找时先查缓存,有则直接返回,无才查磁盘,以此提高操作效率。虽然后续主流用Redis,但自带机制仍要了解。

第二部分:一级缓存(SqlSession级别)

2.1 概述

⭐ 老师强调 :一级缓存是SqlSession级别的,即操作数据库创建的SqlSession范围内的缓存。

特性 说明
默认状态 默认开启,无需配置
作用范围 单个SqlSession
存储结构 HashMap
隔离性 多个SqlSession相互隔离

⭐ 老师强调:SqlSession对应一次会话,其内部用HashMap结构作缓存空间,查到的结果存里面,且多个SqlSession相互隔离,只能访问自己的缓存。这就像你手边记事本,同一会话里查过的数据直接翻本子,不用再跑档案室。

2.2 Cache Key的构成

⭐ 老师强调 :哈希map的key不是普通字符串,而是Cache Key,由多部分组成,必须全部相同才视为同一条查询。

构成部分 说明
namespace 命名空间(如com.qcby.dao.StudentDao)
方法名 如findStudentById
SQL语句 必须一样
参数 传的参数必须一样
分页参数 若涉及分页也要一致

⭐ 老师强调:只有namespace、方法名、SQL、参数、分页全部相同,才判定查的同一条。

2.3 一级缓存失效条件

⭐ 老师强调:1级缓存像私人笔记本,只在自己会话、同条件、未改动且没清掉时才管用,跨会话或变动后就作废了。

失效条件 说明
SqlSession不同 每个1级缓存是会话独享,无法共享
查询条件不同 读不到
两次查找之间做了增删改 数据变动,缓存需清
手动清除(close) 会话关闭,缓存消失

2.4 事务提交对缓存的影响

⭐ 老师强调 :commit不仅表事务提交,还意味着清空一级缓存。原因在于增删改已变动数据库,若缓存不清会拿到旧数据。

缓存与提交流程:

复制代码
第一次查id=9
    │
    ▼
缓存无 → 查数据库 → 写入缓存
    │
    ▼
session.commit() → 清空一级缓存
    │
    ▼
第二次查id=9
    │
    ▼
缓存无(已清) → 再查数据库 → 写入缓存

2.5 测试代码

复制代码
@Test
public void run() {
    // 第一次查
    Student stu1 = mapper.findStudentById(9);
    System.out.println(stu1);
    
    session.commit();  // 清空一级缓存
    
    // 第二次查
    Student stu2 = mapper.findStudentById(9);
    System.out.println(stu2);
    
    // 第三次查
    Student stu3 = mapper.findStudentById(9);
    System.out.println(stu3);
    
    System.out.println(stu1 == stu2);  // false(commit后缓存已清)
    System.out.println(stu2 == stu3);  // true(同缓存对象)
}

⭐ 老师强调 :==对引用类型比较的是内存地址 ,结果为true说明两次取到的是同一个缓存对象。一级缓存用HashMap查到结果后返回其地址值,两次拿到同一地址,指向同一空间。

第三部分:二级缓存(namespace级别)

3.1 概述

⭐ 老师强调 :二级缓存是Mapper/namespace级别 的,基于namespace命名空间,是跨SqlSession共享的。

特性 一级缓存 二级缓存
默认状态 默认开启 默认不开启
作用范围 单个SqlSession 同namespace跨会话
存储结构 HashMap(地址引用) HashMap(序列化字节)
失效条件 commit/close等 库真正改动

3.2 开启二级缓存

步骤1:全局配置开启缓存

复制代码
<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
    <setting name="cacheEnabled" value="true"/>
</settings>

步骤2:映射文件声明使用缓存

复制代码
<mapper namespace="com.qcby.dao.StudentDao">
    <!-- 使用二级缓存 -->
    <!-- eviction: 回收策略 -->
    <!-- flushInterval: 刷新间隔(毫秒) -->
    <!-- size: 缓存对象数量 -->
    <!-- readOnly: true只读缓存 false读写缓存 -->
    <cache eviction="FIFO" flushInterval="6000" size="512" readOnly="true"/>
    
    <select id="findStudentById" resultType="com.qcby.entity.Student" 
            parameterType="java.lang.Integer">
        select * from student where id = #{id}
    </select>
    
    <select id="findAllStudent" resultType="com.qcby.entity.Student">
        select * from student
    </select>
</mapper>

步骤3:实体类实现序列化接口

复制代码
package com.qcby.entity;

import java.io.Serializable;
// 序列化:Java对象 → 二进制字节数组
// 反序列化:二进制字节数组 → Java对象
public class Student implements Serializable {
    private Integer id;
    private String name;
    private Integer age;
    private String sex;
    private Integer tid;
    
    // getter/setter、toString省略
}

⭐ 老师强调 :想用2级缓存,得在student类上额外实现序列化接口 。这接口是空的,无需重写任何方法,作用只是个标识,告诉JVM该类对象可被序列化。类似商品贴了"可运输"标签,机器才允许搬运。

3.3 缓存配置参数

参数 说明
eviction 回收策略(FIFO先进先出、LRU最近最少使用)
flushInterval 刷新间隔(毫秒),默认不设置只在语句调用时刷新
size 缓存对象数量,默认1024
readOnly true=只读缓存,false=读写缓存

⭐ 老师强调:

  • 回收策略:因空间受限需按规则淘汰对象。LRU按"使用时间"清,FIFO按"存入顺序"清
  • readOnly=false:写入取出需经序列化与反序列化,返回的不是同一内存对象,更安全但稍慢
  • readOnly=true:无论谁查找都返回同一个对象,比较结果为true

3.4 二级缓存读写顺序

⭐ 老师强调:

  • 写入:一级缓存被禁用后,数据写入二级缓存(写是"一缓拒则转二缓")
  • 读取:先查二级缓存,没有再查一级,最后才查数据库(读是"先二缓后一缓")

完整流程:

复制代码
写:数据库 → 一级缓存(默认) → 一级拒 → 二级缓存
读:二级缓存 → 一级缓存 → 数据库

3.5 二级缓存清空条件

⭐ 老师强调 :2级缓存只有在数据库真正发生插入、删除等改动时才会被清空;若仅执行commit而库内无改动,缓存不清。这是因为库变后缓存若留旧数据,读出来就是脏数据。

3.6 测试代码(二级缓存)

复制代码
@Test
public void test() throws IOException {
    // 加载主配置文件
    InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
    // 创建SqlSessionFactory对象
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // 创建两个SqlSession
    SqlSession session1 = sqlSessionFactory.openSession();
    SqlSession session2 = sqlSessionFactory.openSession();
    
    // 通过sqlsession创建代理对象
    StudentDao mapper1 = session1.getMapper(StudentDao.class);
    StudentDao mapper2 = session2.getMapper(StudentDao.class);
    
    // 第一次查询(写入一级缓存)
    Student student1 = mapper1.findStudentById(9);
    System.out.println(student1);
    
    // 关闭session1(一级缓存数据写入二级缓存)
    session1.close();
    System.out.println("-----------------------------------------");
    
    // 第二次查询(从二级缓存读取)
    Student student2 = mapper2.findStudentById(9);
    System.out.println(student2);
    session2.close();
    
    // 对比地址(二级缓存序列化后反序列化,地址不同)
    System.out.println(student1 == student2);  // false
    
    inputStream.close();
}

⭐ 老师强调:两级缓存因存储结构差异(地址引用 vs 序列化字节),导致同一数据对象身份不同。

  • 一级缓存 :返回地址引用,同一对象==为true
  • 二级缓存 :序列化存byte数组,反序列化生成新对象 ,==为false

第四部分:PageHelper分页插件

4.1 引入依赖

复制代码
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.3.2</version>
    <scope>compile</scope>
</dependency>

4.2 配置插件

复制代码
<configuration>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    
    <!-- 配置插件 -->
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 开启合理化分页 -->
            <property name="reasonable" value="true"/>
        </plugin>
    </plugins>
    
    <environments default="mysql">
        <!-- ... -->
    </environments>
    
    <mappers>
        <mapper resource="mapper/StudentDao.xml"/>
    </mappers>
</configuration>

⭐ 老师强调 :插件类似之前学的过滤器 。过滤器是在请求到达目标资源前拦截,检查登录状态,不放行就打回登录页;而该插件是在查找SQL后拼接limit实现分页,相当于拦截后加工。

4.3 合理化分页(reasonable)

⭐ 老师强调:当页码越界或不合理时,比如查-100或第20页(总10页),框架不会报错,而是自动展示合理数据------返回第一页或最后一页。

reasonable 效果
true 越界自动展示合理数据(第一页/最后一页)
false 越界直接返回空数据

⭐ 老师强调:框架像"老好人",你翻错页也给你凑合看;但咱自己写还得规矩,入参和返回数据该写全还要写全,别依赖框架容错。

4.4 使用分页

复制代码
@Test
public void run1() {
    // 开启分页查询(页码从1开始)
    PageHelper.startPage(2, 5);  // 第2页,每页5条
    
    List<Student> students = mapper.findAllStudent();
    for (Student student : students) {
        System.out.println(student);
    }
    
    // 封装分页信息
    PageInfo<Student> pageInfo = new PageInfo<>(students);
    System.out.println("当前页数:" + pageInfo.getPageNum());
    System.out.println("每页条数:" + pageInfo.getPageSize());
    System.out.println("总记录数:" + pageInfo.getTotal());
    System.out.println("总页数:" + pageInfo.getPages());
    System.out.println("上一页:" + pageInfo.getPrePage());
    System.out.println("下一页:" + pageInfo.getNextPage());
    System.out.println("是否有上一页:" + pageInfo.isHasPreviousPage());
    System.out.println("是否有下一页:" + pageInfo.isHasNextPage());
    System.out.println("是否为首页:" + pageInfo.isIsFirstPage());
    System.out.println("是否为末页:" + pageInfo.isIsLastPage());
    System.out.println("存放页码的数据:" + Arrays.toString(pageInfo.getNavigatepageNums()));
    System.out.println("获取当前页数据:" + pageInfo.getList());
}

4.5 PageInfo属性速查

方法 说明
getPageNum() 当前页数
getPageSize() 每页条数
getTotal() 总记录数
getPages() 总页数
getPrePage() 上一页页码
getNextPage() 下一页页码
isHasPreviousPage() 是否有上一页
isHasNextPage() 是否有下一页
isIsFirstPage() 是否为首页
isIsLastPage() 是否为末页
getNavigatepageNums() 页码数组
getList() 当前页数据

⭐ 老师强调:

  • 程序员调用分页方法直接写目标页码即可,无需从零拼接参数
  • 页码从1开始,而非0
  • 方法命名贴合英文语义,读单词就能猜出用途

附录一:核心对比速查表

一级缓存 vs 二级缓存

对比 一级缓存 二级缓存
级别 SqlSession namespace/Mapper
默认状态 开启 关闭
作用范围 单会话 跨会话共享
存储结构 HashMap(地址引用) HashMap(序列化字节)
对象比较 ==为true ==为false
失效条件 commit/close/条件变化 库真正改动

缓存读写顺序

操作 顺序
写入 一级缓存 → 拒则二级缓存
读取 二级缓存 → 一级缓存 → 数据库

缓存配置参数

参数 说明
eviction 回收策略(FIFO/LRU)
flushInterval 刷新间隔(毫秒)
size 缓存对象数量
readOnly true只读/false读写
相关推荐
宸津-代码粉碎机7 小时前
OpenAI 连夜迎战 Grok Bot 和 Muse:AI 智能体从 “会聊天” 到 “能办事”,现在入场还来得及吗
java·大数据·人工智能·分布式·python
chenbingjie_c8 小时前
C++ 进阶核心知识点总结:模板、内存分区、new/delete、内存池
开发语言·c++
噢,我明白了9 小时前
java中唯一键和幂等键的应用
java·后端
程序猿乐锅9 小时前
【黑马点评 | 第八篇】Redisson分布式锁
java·数据库·spring boot·redis·分布式·spring·缓存
考虑考虑9 小时前
synchronized字符串常量
java·后端·java ee
长谷深风1119 小时前
Tool与Skill:AI能力设计的分水岭
java·人工智能·ai·大模型·aiagent
m0_587383009 小时前
广州24小时自助健身房解决方案实战指南与系统部署要点
java·spring·小程序·架构·需求分析
Escalating_xu9 小时前
【C 语言】深入理解指针(1·下):指针运算、野指针、assert 与传址实战
java·c语言·开发语言
实心儿儿9 小时前
Qt — Qt 事件
开发语言·qt