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

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

一级缓存

导学

本节学习目标:

  • 理解一级缓存的作用域与生命周期
  • 掌握一级缓存的命中规则与缓存Key构成
  • 学会通过日志验证缓存命中与失效行为
  • 规避多SqlSession场景下的一级缓存误用

定义

一级缓存(Local Cache)是MyBatis在SqlSession级别维护的本地内存缓存,默认自动开启,无需任何额外配置。它解决的核心性能痛点是:在同一个数据库会话中,重复执行完全相同的查询时避免多次访问数据库,从而降低网络开销与数据库压力。

例如,在一次业务处理中,先查询学生信息做校验,随后同一流程中再次查询同一条记录,一级缓存可直接返回内存中的结果,无需再次执行SQL。

核心原理

作用域与生命周期

一级缓存的作用域严格绑定在SqlSession实例上:

  • 创建:SqlSession被SqlSessionFactory.openSession()创建时,一级缓存随之初始化
  • 生效:仅在当前SqlSession内部生效,对外不可见
  • 清空:执行sqlSession.clearCache()、执行insert/update/delete语句后自动清空,或SqlSession关闭时销毁

缓存Key的构成

MyBatis通过CacheKey对象判断两次查询是否相同。Key由以下要素组合生成:

组成要素说明
MappedStatement IDMapper命名空间 + 语句ID,如com.fly.mapper.StudentMapper.selectById
RowBounds偏移量offset值
RowBounds限制数limit值
SQL语句最终生成的带?占位符的SQL
参数值所有传入参数的实际值

只要上述任一要素不同,即视为不同查询,不会命中缓存。

查询时序

从时序图可见,第二次查询完全绕过了数据库,直接从内存获取结果。

完整示例

场景说明

乐途公司人事系统需要查询员工信息。在同一个业务流程中,先查询员工做权限校验,再查询同一条记录展示详情。利用一级缓存,第二次查询不应再发送SQL。

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

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>
        <!-- 开启日志,观察SQL执行情况 -->
        <setting name="logImpl" value="SLF4J"/>
        <!-- 一级缓存默认开启,此处显式声明 -->
        <setting name="localCacheScope" value="SESSION"/>
    </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">
    
    <select id="selectById" resultType="com.fly.entity.Student">
        SELECT id, name, age, major, score 
        FROM student 
        WHERE id = #{id}
    </select>
    
    <update id="updateScoreById">
        UPDATE student 
        SET score = #{score} 
        WHERE id = #{id}
    </update>
    
</mapper>

Student.java

package com.fly.entity;

public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String major;
    private Double score;
    
    // Getter与Setter省略
}

FirstCacheDemo.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 FirstCacheDemo {
    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
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
            
            System.out.println("=== 第一次查询 id=1 ===");
            Student s1 = mapper.selectById(1);
            System.out.println("结果:" + s1.getName() + ",分数:" + s1.getScore());
            
            System.out.println("\n=== 第二次查询 id=1 ===");
            Student s2 = mapper.selectById(1);
            System.out.println("结果:" + s2.getName() + ",分数:" + s2.getScore());
            
            System.out.println("\n=== 两次结果是否为同一对象 ===");
            System.out.println(s1 == s2);  // 应为true
            
            System.out.println("\n=== 执行更新操作 ===");
            mapper.updateScoreById(1, 99.0);
            
            System.out.println("\n=== 第三次查询 id=1(缓存已清空)===");
            Student s3 = mapper.selectById(1);
            System.out.println("结果:" + s3.getName() + ",分数:" + s3.getScore());
            
            System.out.println("\n=== s1与s3是否为同一对象 ===");
            System.out.println(s1 == s3);  // 应为false
        }
    }
}

实际执行结果

控制台SQL日志输出(使用SLF4J + Logback):

=== 第一次查询 id=1 ===
[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
结果:大翔,分数:95.5

=== 第二次查询 id=1 ===
结果:大翔,分数:95.5

=== 两次结果是否为同一对象 ===
true

=== 执行更新操作 ===
[com.fly.mapper.StudentMapper.updateScoreById] - ==>  Preparing: UPDATE student SET score = ? WHERE id = ?
[com.fly.mapper.StudentMapper.updateScoreById] - ==> Parameters: 99.0(Double), 1(Integer)
[com.fly.mapper.StudentMapper.updateScoreById] - <==    Updates: 1

=== 第三次查询 id=1(缓存已清空)===
[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
结果:大翔,分数:99.0

=== s1与s3是否为同一对象 ===
false

分析

  1. 第一次查询:发送SQL到数据库,返回大翔的信息,并将结果存入一级缓存
  2. 第二次查询:日志中无SQL输出,直接从一级缓存获取,且s1 == s2为true,说明返回的是同一个对象引用
  3. 执行update后:MyBatis自动清空当前SqlSession的一级缓存,保证数据一致性
  4. 第三次查询:重新发送SQL,获取更新后的分数99.0,且s1 == s3为false,说明是新的对象实例

易错场景与常见误区

误区正解
认为一级缓存在多个SqlSession之间共享一级缓存仅在当前SqlSession内有效,跨SqlSession必然查库
手动修改缓存返回的对象属性不会影响后续读取一级缓存返回同一对象引用,修改后再次读取会得到修改后的值(脏读风险)
查询后只要不关闭SqlSession,缓存永远有效执行任何insert/update/delete都会自动清空一级缓存
关闭SqlSession后重新打开,缓存还在SqlSession关闭即销毁一级缓存,新SqlSession没有任何历史缓存
认为localCacheScope=STATEMENT可以保留一级缓存STATEMENT表示每条语句执行后都清空缓存,一级缓存几乎失效

反例:跨SqlSession不共享一级缓存

// 反例演示:两个不同SqlSession查询同一数据
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();

try {
    StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
    StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
    
    // session1 第一次查询
    mapper1.selectById(1);  // 发送SQL
    
    // session2 查询同一数据
    mapper2.selectById(1);  // 仍然发送SQL!一级缓存不共享
} finally {
    session1.close();
    session2.close();
}

面试考点

Q1:MyBatis一级缓存的作用域是什么?默认是否开启?

一级缓存作用域是SqlSession级别,默认自动开启,无需配置。生命周期与SqlSession绑定,随其创建而创建,随其关闭或执行增删改操作而清空。

Q2:一级缓存的Key由哪些因素决定?

由MappedStatement ID、RowBounds的offset和limit、SQL语句(含占位符)、以及所有参数值共同组成。任一要素不同,Key就不同,不会命中缓存。

Q3:为什么执行insert/update/delete后会清空一级缓存?

因为增删改操作可能改变数据库状态,MyBatis为了保证缓存数据与数据库一致,会自动清空当前SqlSession的一级缓存,防止脏读。

Q4:一级缓存返回的对象是副本还是同一引用?有什么风险?

默认返回同一对象引用(readOnly语义由一级缓存内部实现决定)。如果业务代码修改了返回对象的属性,再次查询会从缓存拿到已被修改的对象,造成逻辑错误。建议不要直接修改Mapper返回的实体对象。

小结

一级缓存是MyBatis最基础的性能优化机制,它在SqlSession级别自动维护查询结果,无需任何配置即可生效。其核心特点是:同Session、同查询、命中内存。但开发者必须清醒认识其作用域边界——跨SqlSession不共享、增删改后自动清空、返回同一对象引用。合理利用一级缓存,可以在单会话的多层业务逻辑中显著减少重复查询。

下一章引子

一级缓存解决了单会话内的重复查询问题,但如果多个SqlSession需要共享缓存数据,一级缓存就无能为力了。MyBatis提供了二级缓存,将缓存作用域提升到Mapper命名空间级别,实现跨SqlSession的全局缓存共享。下一节将深入讲解二级缓存的开启方式、查询顺序以及与一级缓存的协作关系。

上一页
本章定位
下一页
二级缓存