乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

二级缓存

导学

本节学习目标:

  • 理解二级缓存的作用域与开启方式
  • 掌握二级缓存与一级缓存的协作查询顺序
  • 学会通过日志验证二级缓存的命中行为
  • 明确二级缓存的数据一致性边界与刷新机制

定义

二级缓存(Second Level Cache)是MyBatis在Mapper命名空间级别维护的全局缓存,需要显式配置才能开启。它解决的核心性能痛点是:多个SqlSession执行相同查询时,避免每个会话都访问数据库,将热点数据提升到应用级内存中共享。

例如,乐途公司人事系统的员工信息查询接口,被多个并发请求调用,二级缓存可以让首次查询的结果被后续所有SqlSession复用。

核心原理

作用域与生命周期

特性一级缓存二级缓存
作用域SqlSessionMapper Namespace
默认状态自动开启需显式配置
共享范围仅当前会话跨SqlSession共享
生命周期随SqlSession关闭而销毁随应用启动而创建,随应用关闭而销毁
存储位置内存(本地)可配置为内存、Ehcache、Redis等

查询顺序:三级穿透模型

MyBatis执行查询时,按照严格的优先级逐层查找:

关键协作规则:

  1. 查询时:先查二级缓存,未命中再查一级缓存,仍未命中则查数据库
  2. 写入时:查询结果先存入一级缓存
  3. 关闭时:SqlSession关闭(或commit)时,将一级缓存中的数据刷入二级缓存
  4. 清空时:执行insert/update/delete后,不仅清空当前SqlSession的一级缓存,还会通知二级缓存清空该命名空间下的相关缓存

开启条件

二级缓存需要同时满足两个条件:

  1. 全局开关:mybatis-config.xml中settings.cacheEnabled = true(默认即为true)
  2. 命名空间声明:Mapper XML中添加<cache/>元素

完整示例

场景说明

乐途公司人事系统的高频查询场景:多个业务模块都需要查询员工基本信息。通过二级缓存,让第一个SqlSession的查询结果被第二个SqlSession直接命中,实现应用级共享。

操作前的数据库表结构及初始数据

CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    age INT,
    major VARCHAR(20),
    score DECIMAL(5,2)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);

当前数据状态:

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00

完整代码与配置

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 开启二级缓存全局开关,默认true -->
        <setting name="cacheEnabled" value="true"/>
        <setting name="logImpl" value="SLF4J"/>
    </settings>
    
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/fly_db?useSSL=false&amp;serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    
    <mappers>
        <mapper resource="com/fly/mapper/StudentMapper.xml"/>
    </mappers>
</configuration>

StudentMapper.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="com.fly.mapper.StudentMapper">
    
    <!-- 声明开启当前命名空间的二级缓存 -->
    <cache/>
    
    <!-- 实体类需实现Serializable,二级缓存会序列化存储 -->
    <select id="selectById" resultType="com.fly.entity.Student">
        SELECT id, name, age, major, score 
        FROM student 
        WHERE id = #{id}
    </select>
    
    <update id="updateScoreById" flushCache="true">
        UPDATE student 
        SET score = #{score} 
        WHERE id = #{id}
    </update>
    
</mapper>

Student.java

package com.fly.entity;

import java.io.Serializable;

public class Student implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private Integer id;
    private String name;
    private Integer age;
    private String major;
    private Double score;
    
    // Getter与Setter省略
}

SecondCacheDemo.java

package com.fly.demo;

import com.fly.entity.Student;
import com.fly.mapper.StudentMapper;
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.InputStream;

public class SecondCacheDemo {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        
        // ========== 第一个SqlSession ==========
        System.out.println("=== 第一个SqlSession 第一次查询 id=2 ===");
        try (SqlSession session1 = sqlSessionFactory.openSession()) {
            StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
            Student s1 = mapper1.selectById(2);
            System.out.println("结果:" + s1.getName() + ",专业:" + s1.getMajor());
            // 必须commit或close,一级缓存才会刷入二级缓存
            session1.commit();
        }
        
        System.out.println("\n=== 第一个SqlSession 已关闭 ===");
        
        // ========== 第二个SqlSession ==========
        System.out.println("\n=== 第二个SqlSession 查询 id=2 ===");
        try (SqlSession session2 = sqlSessionFactory.openSession()) {
            StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
            Student s2 = mapper2.selectById(2);
            System.out.println("结果:" + s2.getName() + ",专业:" + s2.getMajor());
        }
        
        // ========== 第三个SqlSession 执行更新 ==========
        System.out.println("\n=== 第三个SqlSession 更新 id=2 的分数 ===");
        try (SqlSession session3 = sqlSessionFactory.openSession()) {
            StudentMapper mapper3 = session3.getMapper(StudentMapper.class);
            mapper3.updateScoreById(2, 91.0);
            session3.commit();  // commit会清空二级缓存
        }
        
        // ========== 第四个SqlSession 再次查询 ==========
        System.out.println("\n=== 第四个SqlSession 再次查询 id=2 ===");
        try (SqlSession session4 = sqlSessionFactory.openSession()) {
            StudentMapper mapper4 = session4.getMapper(StudentMapper.class);
            Student s4 = mapper4.selectById(2);
            System.out.println("结果:" + s4.getName() + ",分数:" + s4.getScore());
        }
    }
}

实际执行结果

控制台日志输出:

=== 第一个SqlSession 第一次查询 id=2 ===
[com.fly.mapper.StudentMapper.selectById] - Cache Hit Ratio [com.fly.mapper.StudentMapper]: 0.0
[com.fly.mapper.StudentMapper.selectById] - ==>  Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
[com.fly.mapper.StudentMapper.selectById] - ==> Parameters: 2(Integer)
[com.fly.mapper.StudentMapper.selectById] - <==      Total: 1
结果:白歌,专业:软件工程

=== 第一个SqlSession 已关闭 ===

=== 第二个SqlSession 查询 id=2 ===
[com.fly.mapper.StudentMapper.selectById] - Cache Hit Ratio [com.fly.mapper.StudentMapper]: 0.5
结果:白歌,专业:软件工程

=== 第三个SqlSession 更新 id=2 的分数 ===
[com.fly.mapper.StudentMapper.updateScoreById] - ==>  Preparing: UPDATE student SET score = ? WHERE id = ?
[com.fly.mapper.StudentMapper.updateScoreById] - ==> Parameters: 91.0(Double), 2(Integer)
[com.fly.mapper.StudentMapper.updateScoreById] - <==    Updates: 1

=== 第四个SqlSession 再次查询 id=2 ===
[com.fly.mapper.StudentMapper.selectById] - Cache Hit Ratio [com.fly.mapper.StudentMapper]: 0.3333333333333333
[com.fly.mapper.StudentMapper.selectById] - ==>  Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
[com.fly.mapper.StudentMapper.selectById] - ==> Parameters: 2(Integer)
[com.fly.mapper.StudentMapper.selectById] - <==      Total: 1
结果:白歌,分数:91.0

分析

  1. 第一次查询(session1):二级缓存命中率0.0,未命中,发送SQL查库,查询白歌的信息。session1.commit()后,一级缓存数据刷入二级缓存
  2. 第二次查询(session2):二级缓存命中率0.5,命中二级缓存!日志中无SQL输出,直接从二级缓存获取白歌的信息
  3. 执行更新(session3):updateScoreById执行后commit,MyBatis自动清空该命名空间的二级缓存,保证数据一致性
  4. 第三次查询(session4):二级缓存命中率0.333...,缓存已被清空,重新发送SQL查库,获取更新后的分数91.0

注意:Cache Hit Ratio是MyBatis自动计算的二级缓存命中率统计,值越接近1说明缓存效果越好。

易错场景与常见误区

误区正解
加了<cache/>就能跨SqlSession共享缓存必须执行commit()或close(),一级缓存才会刷入二级缓存;只查询不提交,二级缓存不会生效
二级缓存返回的对象与一级缓存一样,是同一引用二级缓存默认序列化存储,返回的是反序列化后的新对象,s1 == s2为false
实体类不实现Serializable也能用二级缓存若readOnly="false"(默认),实体类必须实现Serializable,否则抛出序列化异常
多个Mapper命名空间默认共享同一个缓存每个<cache/>独立维护缓存,跨命名空间需用<cache-ref>显式引用
二级缓存比一级缓存更快二级缓存涉及序列化/反序列化开销,同Session内一级缓存更快;二级缓存的优势在跨Session共享

反例:未commit导致二级缓存不生效

// 反例:查询后未commit,二级缓存不会生效
SqlSession session1 = sqlSessionFactory.openSession();
StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
mapper1.selectById(3);  // 查询小崔
// 没有调用 session1.commit() 或 session1.close()
session1.close();  // 如果close前没有commit,某些事务管理器下不会刷入二级缓存

SqlSession session2 = sqlSessionFactory.openSession();
StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
mapper2.selectById(3);  // 仍然发送SQL!因为一级缓存未刷入二级缓存

正确做法:查询后显式调用session.commit(),确保一级缓存数据同步到二级缓存。

面试考点

Q1:MyBatis查询时的缓存查找顺序是什么?

先查二级缓存,未命中再查一级缓存,仍未命中则查询数据库。查询结果先写入一级缓存,SqlSession关闭或提交时,一级缓存数据刷入二级缓存。

Q2:二级缓存开启需要满足哪些条件?

两个条件:① mybatis-config.xml中cacheEnabled=true(默认已开启);② 目标Mapper XML中声明<cache/>元素。此外,若readOnly="false",缓存实体类必须实现Serializable接口。

Q3:为什么SqlSession关闭后二级缓存才能被其他会话命中?

因为查询结果首先存入一级缓存,只有当前SqlSession执行commit()或close()时,MyBatis才会将一级缓存中的数据序列化后刷入二级缓存。在此之前,数据仅对当前会话可见。

Q4:执行update后二级缓存会怎样?

MyBatis会清空该Mapper命名空间下的整个二级缓存(以及当前SqlSession的一级缓存),这是为了保证缓存与数据库的一致性。如果多个Mapper共享缓存(通过<cache-ref>),被引用的缓存也会被清空。

小结

二级缓存将MyBatis的缓存能力从会话级提升到应用级,是解决热点数据重复查询的核心手段。其查询遵循"二级 → 一级 → 数据库"的三级穿透模型,数据回写则依赖SqlSession的提交或关闭。使用二级缓存时,务必注意实体类的序列化要求、commit时机的把控,以及缓存清空对数据一致性的保障。

下一章引子

二级缓存的默认实现基于内存的HashMap,其淘汰策略、刷新间隔、线程安全等都可以通过<cache>元素的属性精细控制。此外,实际生产环境往往需要将缓存外接到Ehcache、Redis等专业缓存中间件。下一节将系统讲解缓存配置详解,涵盖eviction策略、flushInterval、readOnly等核心属性,以及自定义缓存集成的完整方案。

上一页
一级缓存
下一页
缓存配置详解