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

    • 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执行器类型
    • 分页插件

缓存配置详解

导学

本节学习目标:

  • 掌握<cache>元素所有属性的含义与适用场景
  • 理解四种eviction淘汰策略的差异与选择依据
  • 学会通过flushInterval、size等属性控制缓存行为
  • 掌握<cache-ref>、语句级useCache与flushCache的精细控制

定义

MyBatis二级缓存的默认实现是一个简单的内存HashMap,但生产环境对缓存的容量限制、淘汰策略、刷新周期、线程安全等有更高要求。<cache>元素及其相关配置机制解决的核心痛点是:在不编写代码的情况下,通过声明式配置精确控制缓存的存储策略、生命周期和并发行为,使缓存从"能用"走向"好用"。

核心原理

<cache>元素属性全解

<cache
    eviction="LRU"
    flushInterval="60000"
    size="512"
    readOnly="false"
    blocking="false"/>
属性默认值说明
evictionLRU缓存淘汰策略:LRU、FIFO、SOFT、WEAK
flushInterval无自动清空缓存的时间间隔(毫秒),默认不自动清空
size1024缓存可保存的对象数量上限
readOnlyfalsetrue返回同一实例(快但不安全),false返回序列化副本(线程安全)
blockingfalse是否启用阻塞缓存,防止缓存击穿

四种Eviction策略对比

策略全称淘汰机制适用场景
LRULeast Recently Used最近最少使用通用场景,默认策略,保留热点数据
FIFOFirst In First Out先进先出数据时效性强的场景,按时间顺序淘汰
SOFTSoft Reference软引用内存敏感场景,GC时优先回收,防OOM
WEAKWeak Reference弱引用比SOFT更激进,下次GC即回收,生命周期极短

缓存查询完整顺序流程

语句级缓存控制

MyBatis允许在单条SQL语句上覆盖全局缓存行为:

  • useCache="false":当前查询不使用二级缓存
  • flushCache="true":当前语句执行后清空一级和二级缓存

跨命名空间缓存共享

<cache-ref namespace="com.fly.mapper.OtherMapper"/>可以让当前Mapper引用其他命名空间的缓存实例,实现多个Mapper共享同一缓存区域。执行任一关联Mapper的增删改,都会清空共享缓存。

完整示例

场景说明

乐途公司人事系统需要为不同业务模块配置差异化的缓存策略:

  • 员工基础信息表:数据量大、访问频繁,使用LRU + 5分钟刷新
  • 部门统计报表:数据时效性强,使用FIFO + 1分钟刷新
  • 敏感薪资数据:禁用缓存,每次强制查库

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

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>
        <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"/>
        <mapper resource="com/fly/mapper/ScoreMapper.xml"/>
    </mappers>
</configuration>

StudentMapper.xml(LRU策略 + 5分钟刷新)

<?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">
    
    <!-- LRU淘汰,最多缓存512条,每5分钟自动刷新,返回副本(线程安全) -->
    <cache 
        eviction="LRU"
        flushInterval="300000"
        size="512"
        readOnly="false"
        blocking="true"/>
    
    <select id="selectById" resultType="com.fly.entity.Student">
        SELECT id, name, age, major, score 
        FROM student 
        WHERE id = #{id}
    </select>
    
    <!-- 查询全部学生,使用二级缓存 -->
    <select id="selectAll" resultType="com.fly.entity.Student">
        SELECT id, name, age, major, score FROM student
    </select>
    
    <update id="updateScoreById">
        UPDATE student SET score = #{score} WHERE id = #{id}
    </update>
    
</mapper>

ScoreMapper.xml(引用StudentMapper的缓存 + 禁用缓存查询)

<?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.ScoreMapper">
    
    <!-- 引用StudentMapper的缓存,实现共享 -->
    <cache-ref namespace="com.fly.mapper.StudentMapper"/>
    
    <!-- 敏感操作:查询时禁用二级缓存,强制走数据库 -->
    <select id="selectSensitiveScore" resultType="com.fly.entity.Student" useCache="false">
        SELECT id, name, score FROM student WHERE id = #{id}
    </select>
    
    <!-- 批量更新后强制清空所有缓存 -->
    <update id="batchUpdateScore" flushCache="true">
        UPDATE student SET score = score + #{bonus}
    </update>
    
</mapper>

CacheConfigDemo.java

package com.fly.demo;

import com.fly.entity.Student;
import com.fly.mapper.ScoreMapper;
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 CacheConfigDemo {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        
        // 场景1:验证LRU缓存与readOnly=false的行为
        System.out.println("=== 场景1:LRU缓存 + readOnly=false ===");
        SqlSession session1 = sqlSessionFactory.openSession();
        StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
        Student s1 = mapper1.selectById(1);  // 大翔
        session1.commit();
        session1.close();
        
        SqlSession session2 = sqlSessionFactory.openSession();
        StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
        Student s2 = mapper2.selectById(1);  // 命中二级缓存
        
        System.out.println("s1 == s2 ? " + (s1 == s2));  // false,因为是副本
        System.out.println("s1.name = " + s1.getName() + ", s2.name = " + s2.getName());
        session2.close();
        
        // 场景2:验证useCache=false强制查库
        System.out.println("\n=== 场景2:useCache=false 强制查库 ===");
        SqlSession session3 = sqlSessionFactory.openSession();
        ScoreMapper scoreMapper = session3.getMapper(ScoreMapper.class);
        Student s3 = scoreMapper.selectSensitiveScore(1);  // 强制发送SQL
        System.out.println("敏感查询结果:" + s3.getName() + ",分数:" + s3.getScore());
        session3.close();
        
        // 场景3:验证flushCache=true清空缓存
        System.out.println("\n=== 场景3:flushCache=true 清空缓存 ===");
        SqlSession session4 = sqlSessionFactory.openSession();
        StudentMapper mapper4 = session4.getMapper(StudentMapper.class);
        Student s4 = mapper4.selectById(3);  // 查询小崔,写入缓存
        session4.commit();
        session4.close();
        
        SqlSession session5 = sqlSessionFactory.openSession();
        ScoreMapper scoreMapper5 = session5.getMapper(ScoreMapper.class);
        scoreMapper5.batchUpdateScore(5.0);  // 清空共享缓存
        session5.commit();
        session5.close();
        
        SqlSession session6 = sqlSessionFactory.openSession();
        StudentMapper mapper6 = session6.getMapper(StudentMapper.class);
        Student s6 = mapper6.selectById(3);  // 缓存已被清空,重新查库
        System.out.println("清空缓存后查询:" + s6.getName() + ",分数:" + s6.getScore());
        session6.close();
    }
}

实际执行结果

控制台日志输出:

=== 场景1:LRU缓存 + readOnly=false ===
[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: 1(Integer)
[com.fly.mapper.StudentMapper.selectById] - <==      Total: 1
[com.fly.mapper.StudentMapper.selectById] - Cache Hit Ratio [com.fly.mapper.StudentMapper]: 0.5
s1 == s2 ? false
s1.name = 大翔, s2.name = 大翔

=== 场景2:useCache=false 强制查库 ===
[com.fly.mapper.ScoreMapper.selectSensitiveScore] - ==>  Preparing: SELECT id, name, score FROM student WHERE id = ?
[com.fly.mapper.ScoreMapper.selectSensitiveScore] - ==> Parameters: 1(Integer)
[com.fly.mapper.ScoreMapper.selectSensitiveScore] - <==      Total: 1
敏感查询结果:大翔,分数:95.5

=== 场景3:flushCache=true 清空缓存 ===
[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: 3(Integer)
[com.fly.mapper.StudentMapper.selectById] - <==      Total: 1
[com.fly.mapper.ScoreMapper.batchUpdateScore] - ==>  Preparing: UPDATE student SET score = score + ?
[com.fly.mapper.ScoreMapper.batchUpdateScore] - ==> Parameters: 5.0(Double)
[com.fly.mapper.ScoreMapper.batchUpdateScore] - <==    Updates: 5
[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: 3(Integer)
[com.fly.mapper.StudentMapper.selectById] - <==      Total: 1
清空缓存后查询:小崔,分数:97.0

分析

  1. readOnly=false的效果:s1 == s2为false,说明二级缓存返回的是反序列化后的新对象,线程安全,但牺牲一定性能
  2. useCache=false的效果:selectSensitiveScore每次都发送SQL,不命中任何缓存,适合敏感数据
  3. flushCache=true的效果:batchUpdateScore执行后,由于ScoreMapper引用了StudentMapper的缓存(<cache-ref>),共享缓存被整体清空,后续查询重新发送SQL,且小崔的分数已更新为97.0(原92.0 + 5.0)
  4. LRU策略:当缓存数量超过size="512"时,最近最少被访问的条目会被淘汰

易错场景与常见误区

误区正解
readOnly="true"返回的对象可以安全修改readOnly="true"返回同一实例,多线程修改会互相影响,仅适用于只读场景
flushInterval是缓存过期时间flushInterval是定时清空全部缓存的周期,不是单条数据的TTL;到期后整个缓存区被清空
size设置越大越好size过大占用内存,且二级缓存默认序列化存储,大size会增加GC压力;应根据实际数据量合理设置
blocking="true"会提升查询性能blocking是为了防止缓存击穿(多个线程同时查库),会牺牲一定并发性能换取数据库保护
<cache-ref>只是引用配置<cache-ref>引用的是缓存实例本身,增删改会清空被引用方的全部缓存,影响范围比预期大

反例:readOnly=true的线程安全问题

// 反例:readOnly="true"时修改缓存对象,影响其他线程
// Mapper配置:<cache readOnly="true"/>

SqlSession session1 = sqlSessionFactory.openSession();
StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
Student s1 = mapper1.selectById(1);  // 大翔
s1.setScore(0.0);  // 直接修改缓存中的对象!
session1.commit();
session1.close();

SqlSession session2 = sqlSessionFactory.openSession();
StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
Student s2 = mapper2.selectById(1);  // 命中缓存,但分数已被篡改为0.0!
System.out.println(s2.getScore());  // 输出 0.0,数据已被污染

正解:除非能确保缓存对象绝对只读,否则使用readOnly="false",让MyBatis返回序列化副本。

面试考点

Q1:MyBatis二级缓存的四种eviction策略有什么区别?

LRU(最近最少使用)淘汰最久未被访问的数据,适合保留热点;FIFO(先进先出)按进入顺序淘汰,适合时效性数据;SOFT(软引用)在内存不足时被GC回收,防OOM;WEAK(弱引用)更激进,下次GC即回收。生产环境默认LRU最通用。

Q2:readOnly="true"和readOnly="false"有什么区别?

true返回缓存中的同一对象实例,速度快但线程不安全,修改会影响所有读取者;false(默认)返回序列化后的副本,线程安全但需实现Serializable,有序列化开销。

Q3:flushInterval和flushCache有什么区别?

flushInterval是<cache>的属性,按固定周期自动清空整个命名空间的缓存;flushCache是语句级属性,执行该语句后立即清空缓存,是一次性行为。

Q4:多个Mapper如何共享同一个二级缓存?有什么风险?

通过<cache-ref namespace="xxx"/>引用其他Mapper的缓存实例。风险在于:任一关联Mapper执行增删改,都会清空整个共享缓存区,可能导致缓存命中率骤降。建议只有强关联且数据一致性要求高的Mapper才共享缓存。

小结

<cache>元素是MyBatis二级缓存的"调度中心",通过eviction控制淘汰策略、flushInterval控制自动刷新周期、size控制容量上限、readOnly权衡安全与性能、blocking防御缓存击穿。配合语句级的useCache和flushCache,以及跨命名空间的<cache-ref>,开发者可以在不侵入业务代码的前提下,完成从粗放到精细的缓存治理。

下一章引子

MyBatis内置的二级缓存基于内存HashMap,无法满足分布式、大容量、持久化等企业级需求。下一节将讲解自定义缓存,通过实现Cache接口接入Ehcache、Redis等外部缓存中间件,让MyBatis缓存能力突破单机内存限制。

上一页
二级缓存
下一页
自定义缓存