第一部分:缓存的意义
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读写 |