MyBatis 缓存深度解析(一级 / 二级缓存原理 + 实战 + 避坑)

在高并发业务场景中,数据库 IO 往往是系统性能瓶颈。MyBatis 提供的一级缓存(SqlSession 级别)二级缓存(Mapper 级别) 机制,能通过内存缓存高频查询数据,大幅减少数据库访问次数。本文从核心概念工作原理配置实战性能优化四个维度,全方位拆解 MyBatis 缓存的使用方法、失效场景及最佳实践。

一、缓存核心概念

1. 缓存的定义

缓存是将频繁访问的数据临时存储在内存中的机制,核心价值:

  • 减少数据库磁盘 IO 次数,降低数据库压力;
  • 内存读写速度远高于磁盘,提升查询响应效率(毫秒级→微秒级)。

2. 缓存的适用场景

并非所有数据都适合缓存,符合以下特征的数据优先使用缓存:

特征 说明
访问频率高 系统首页配置、热门商品列表、地区字典等高频查询数据
修改频率低 商品分类、权限角色、数据字典等静态 / 准静态数据
非核心敏感数据 允许短期数据不一致(订单、支付记录等核心数据需谨慎,避免脏数据)
查询成本高 多表关联、复杂统计查询结果(缓存可规避重复计算 / 查询开销)

3. MyBatis 缓存体系

MyBatis 内置两级缓存,同时支持集成第三方分布式缓存(如 Redis/EhCache),体系结构如下:

复制代码
MyBatis 缓存体系
├── 一级缓存(SqlSession 级别,默认开启)
│   └── 作用域:单个 SqlSession,会话隔离
├── 二级缓存(Mapper/Namespace 级别,手动开启)
│   └── 作用域:同一 Mapper 的所有 SqlSession,跨会话共享
└── 第三方缓存(Redis/EhCache 等,分布式场景首选)
    └── 作用域:整个应用集群,跨服务实例共享

二、一级缓存(SqlSession 会话级别)

1. 核心定义

一级缓存是SqlSession 对象级别的本地缓存,MyBatis 默认开启,无需额外配置:

  • SqlSession 内部维护一个 Map 集合(Key:缓存标识;Value:查询结果),存储当前会话的查询数据;
  • 一级缓存生命周期与 SqlSession 完全绑定:SqlSession 创建则缓存生效,关闭 / 清空则缓存失效;
  • 缓存数据以对象引用形式存储(非序列化),读取性能极高。

2. 一级缓存的工作流程

3. 一级缓存的验证实战

(1)测试代码
java 复制代码
package mybatis.test;

import mybatis.mapper.UserMapper;
import mybatis.pojo.User;
import mybatis.util.MyBatisUtil;
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 org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;

public class FirstLevelCacheTest {
    private InputStream in;
    private SqlSession sqlSession;
    private UserMapper mapper;

    // 初始化(每次测试前执行)
    @Before
    public void init() throws IOException {
        // 1. 加载 MyBatis 核心配置文件
        in = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 2. 创建 SqlSessionFactory 工厂
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        // 3. 获取 SqlSession(自动提交事务)
        sqlSession = factory.openSession(true);
        // 4. 获取 Mapper 代理对象
        mapper = sqlSession.getMapper(UserMapper.class);
    }

    // 销毁(每次测试后执行)
    @After
    public void destroy() throws IOException {
        // 关闭 SqlSession
        sqlSession.close();
        // 关闭输入流
        in.close();
    }

    /**
     * 测试一级缓存存在性:同一 SqlSession 下,相同查询条件多次查询
     */
    @Test
    public void testFirstLevelCache() {
        // 1. 第一次查询:缓存无数据,查询数据库并写入缓存
        User user1 = mapper.findById(1);
        System.out.println("第一次查询结果:" + user1);

        // 2. 第二次查询:缓存有数据,直接返回(不执行 SQL)
        User user2 = mapper.findById(1);
        System.out.println("第二次查询结果:" + user2);

        // 3. 验证是否为同一对象(一级缓存返回对象引用)
        System.out.println("是否为同一对象:" + (user1 == user2)); // 输出 true
    }
}
(2)执行日志分析
复制代码
// 第一次查询:执行 SQL,写入一级缓存
DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
第一次查询结果:User{id=1, username='老王', address='北京'}

// 第二次查询:无 SQL 执行,直接读取一级缓存
第二次查询结果:User{id=1, username='老王', address='北京'}
是否为同一对象:true

4. 一级缓存的底层原理

一级缓存由 MyBatis 的 BaseExecutor(基本执行器)实现,核心逻辑在 BaseExecutor.query() 方法(简化源码):

java 复制代码
public <E> List<E> query(MappedStatement ms, Object parameter, 
                         RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 1. 生成缓存 Key(SQL + 参数 + 分页 + Mapper ID 等)
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    
    // 2. 优先查询一级缓存
    List<E> list = (List<E>) localCache.getObject(key);
    if (list != null) {
        return list;
    }
    
    // 3. 缓存无数据,查询数据库
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    // 4. 将结果写入一级缓存
    localCache.putObject(key, list);
    return list;
}

缓存 Key 生成规则MappedStatement ID + SQL 语句 + 参数 + 分页参数 + 环境,确保相同查询条件生成相同 Key。

5. 一级缓存的失效场景

一级缓存并非永久有效,以下操作会触发缓存清空:

失效场景 说明
执行 update/insert/delete 增删改修改数据,MyBatis 自动清空当前 SqlSession 缓存(避免脏数据)
调用 sqlSession.commit() 提交事务时清空缓存(保证事务一致性)
调用 sqlSession.clearCache() 手动清空当前 SqlSession 缓存
调用 sqlSession.close() 关闭 SqlSession,缓存随对象销毁
不同 SqlSession 对象 一级缓存会话隔离,不同 SqlSession 缓存互不干扰
失效场景验证示例
java 复制代码
@Test
public void testFirstLevelCacheInvalid() {
    // 1. 第一次查询:写入缓存
    User user1 = mapper.findById(1);
    System.out.println("第一次查询:" + user1);

    // 2. 执行更新操作:触发缓存清空
    User updateUser = new User();
    updateUser.setId(1);
    updateUser.setUsername("老王更新");
    mapper.updateUser(updateUser);
    sqlSession.commit(); // 提交事务,进一步清空缓存

    // 3. 第二次查询:缓存已清空,重新查询数据库
    User user2 = mapper.findById(1);
    System.out.println("第二次查询:" + user2);
    System.out.println("是否为同一对象:" + (user1 == user2)); // 输出 false
}

三、二级缓存(Mapper/Namespace 级别)

1. 核心定义

二级缓存是Mapper 接口 / Namespace 级别的全局缓存,需手动开启,核心特征:

  • 作用域覆盖同一 Mapper 的所有 SqlSession(跨会话共享);
  • 生命周期与应用程序同步(应用不重启,缓存可一直存在);
  • 缓存数据默认以序列化对象 存储(需实体类实现 Serializable);
  • 查询优先级:一级缓存 > 二级缓存 > 数据库。

2. 二级缓存的工作流程

3. 二级缓存的开启与配置

(1)全局配置(SqlMapConfig.xml)

开启二级缓存总开关(默认开启,可显式配置):

XML 复制代码
<configuration>
    <settings>
        <!-- 开启二级缓存总开关(默认 true,可省略) -->
        <setting name="cacheEnabled" value="true"/>
        <!-- 可选:关闭一级缓存(不推荐,一级缓存是基础) -->
        <!-- <setting name="localCacheScope" value="STATEMENT"/> -->
    </settings>
    
    <!-- 别名配置(简化映射文件) -->
    <typeAliases>
        <package name="mybatis.pojo"/>
    </typeAliases>
</configuration>
(2)Mapper 映射文件配置

在需要开启二级缓存的 Mapper.xml 中添加 <cache/> 标签:

XML 复制代码
<!-- UserMapper.xml -->
<mapper namespace="mybatis.mapper.UserMapper">
    <!-- 开启当前 Mapper 的二级缓存 -->
    <cache
        eviction="LRU"        <!-- 缓存淘汰策略:LRU(默认)/FIFO/SOFT/WEAK -->
        flushInterval="60000" <!-- 缓存自动刷新时间(毫秒),默认不自动刷新 -->
        size="1024"           <!-- 缓存最大存储条目数,默认 1024 -->
        readOnly="false"/>    <!-- 是否只读:false(默认)/true -->

    <!-- 查询方法:useCache="true"(默认,可省略) -->
    <select id="findById" parameterType="int" resultType="User" useCache="true">
        SELECT * FROM user WHERE id = #{id}
    </select>

    <!-- 增删改方法:flushCache="true"(默认,自动清空二级缓存) -->
    <update id="updateUser" parameterType="User" flushCache="true">
        UPDATE user SET username = #{username} WHERE id = #{id}
    </update>
</mapper>
(3)实体类序列化(关键)

二级缓存可能序列化存储数据,实体类必须实现 Serializable 接口:

java 复制代码
package mybatis.pojo;

import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    
    // getter/setter/toString
}

4. 二级缓存的验证实战

(1)测试代码
java 复制代码
package mybatis.test;

import mybatis.mapper.UserMapper;
import mybatis.pojo.User;
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 org.junit.Test;

import java.io.IOException;
import java.io.InputStream;

public class SecondLevelCacheTest {
    // 全局唯一 SqlSessionFactory(避免重复创建)
    private static SqlSessionFactory sqlSessionFactory;

    // 静态初始化 SqlSessionFactory
    static {
        try {
            InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
        } catch (IOException e) {
            throw new RuntimeException("MyBatis 初始化失败:" + e.getMessage());
        }
    }

    /**
     * 测试二级缓存:跨 SqlSession 共享缓存数据
     */
    @Test
    public void testSecondLevelCache() {
        SqlSession sqlSession1 = null;
        SqlSession sqlSession2 = null;
        try {
            // 第一个 SqlSession:查询并写入二级缓存
            sqlSession1 = sqlSessionFactory.openSession(true);
            UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
            User user1 = mapper1.findById(1);
            System.out.println("第一个 SqlSession 查询:" + user1);
            sqlSession1.close(); // 关闭时,一级缓存数据同步到二级缓存

            // 第二个 SqlSession:读取二级缓存
            sqlSession2 = sqlSessionFactory.openSession(true);
            UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
            User user2 = mapper2.findById(1);
            System.out.println("第二个 SqlSession 查询:" + user2);

            // 验证:二级缓存返回序列化拷贝(非同一对象)
            System.out.println("是否为同一对象:" + (user1 == user2)); // 输出 false
        } finally {
            // 关闭资源
            if (sqlSession2 != null) sqlSession2.close();
        }
    }
}
(2)执行日志分析
复制代码
// 第一个 SqlSession:执行 SQL,写入一级缓存,关闭时同步到二级缓存
DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
第一个 SqlSession 查询:User{id=1, username='老王', address='北京'}

// 第二个 SqlSession:无 SQL 执行,读取二级缓存
第二个 SqlSession 查询:User{id=1, username='老王', address='北京'}
是否为同一对象:false

5. 二级缓存核心参数说明

参数 取值 说明
eviction LRU(默认) 最近最少使用:移除最长时间未使用的缓存
FIFO 先进先出:按缓存加入顺序移除
SOFT 软引用:JVM 内存不足时移除(依赖 GC)
WEAK 弱引用:GC 扫描到即移除(比 SOFT 更激进)
flushInterval 数值(毫秒) 缓存自动刷新时间,默认无(仅增删改触发刷新)
size 数值(默认 1024) 缓存最大存储条目数,建议根据业务调整(避免内存溢出)
readOnly false(默认) 返回对象拷贝(序列化),安全但性能稍低
true 返回对象引用,性能高但可能导致脏数据

6. 二级缓存的失效场景

失效场景 说明
执行 update/insert/delete 增删改默认 flushCache="true",清空当前 Mapper 二级缓存
查询方法设置 useCache="false" 该查询不写入 / 读取二级缓存
不同 Namespace 二级缓存按 Namespace 隔离,不同 Mapper 缓存互不干扰
实体类未实现 Serializable 序列化失败,二级缓存无法生效
手动调用 sqlSession.clearCache() 仅清空当前 SqlSession 一级缓存,不影响二级缓存

四、一级缓存 vs 二级缓存(核心对比)

维度 一级缓存(SqlSession 级别) 二级缓存(Mapper 级别)
作用域 单个 SqlSession 同一 Namespace 的所有 SqlSession
开启方式 默认开启,无需配置 需手动开启(全局 + Mapper 配置)
数据存储 内存(对象引用) 内存 / 磁盘(序列化对象)
生命周期 随 SqlSession 销毁 随应用程序启停
数据一致性 会话内一致 跨会话一致,增删改触发刷新
性能 极高(直接取引用) 稍低(序列化 / 反序列化)
适用场景 单次会话内重复查询 跨会话高频静态数据查询

五、MyBatis 缓存进阶使用

1. 缓存的禁用与刷新

  • 全局禁用二级缓存:<setting name="cacheEnabled" value="false"/>
  • 单个查询禁用缓存:<select useCache="false">
  • 强制刷新缓存:<select flushCache="true">(查询时清空缓存);
  • 手动刷新二级缓存:调用 sqlSession.commit()(增删改自动触发)。

2. 集成第三方分布式缓存(Redis)

MyBatis 内置二级缓存为本地缓存,分布式系统中需集成 Redis 实现缓存共享:

(1)引入依赖
XML 复制代码
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>
(2)配置 Redis 缓存
XML 复制代码
<!-- UserMapper.xml -->
<cache type="org.mybatis.caches.redis.RedisCache">
    <property name="host" value="127.0.0.1"/>
    <property name="port" value="6379"/>
    <property name="expire" value="3600"/> <!-- 缓存过期时间(秒) -->
    <property name="timeout" value="1000"/> <!-- 连接超时(毫秒) -->
</cache>

3. 缓存使用最佳实践

实践原则 说明
优先使用一级缓存 无额外开销,是性能优化的基础
二级缓存只缓存静态数据 避免缓存频繁修改的数据,减少脏数据风险
分布式系统用 Redis 替代二级缓存 本地二级缓存无法跨实例共享,Redis 保证分布式缓存一致性
避免缓存大对象 大对象序列化 / 反序列化开销大,建议拆分或不缓存
设置合理的过期时间 即使静态数据,也建议设置过期时间(避免缓存永久有效)
监控缓存命中率 通过 MyBatis 日志或第三方工具监控,命中率低则调整缓存策略

六、常见问题与解决方案

1. 缓存脏数据问题

现象:缓存数据与数据库数据不一致。

原因:增删改未触发缓存刷新、多线程修改数据、只读缓存返回对象引用。

解决方案

  • 增删改操作确保 flushCache="true"
  • 二级缓存设置 readOnly="false"
  • 核心数据避免缓存,或缩短缓存过期时间。

2. 二级缓存不生效排查

  1. 检查 cacheEnabled 是否为 true
  2. 检查 Mapper.xml 是否添加 <cache/> 标签;
  3. 检查实体类是否实现 Serializable
  4. 检查查询方法是否设置 useCache="false"
  5. 检查是否执行了增删改操作(触发缓存清空)。

3. 一级缓存导致事务内数据不一致

现象:同一 SqlSession 内,先查询后更新,再次查询仍取旧数据。

解决方案

  • 更新后调用 sqlSession.clearCache() 清空一级缓存;
  • 事务内避免重复查询,或使用 flushCache="true" 强制刷新。

七、总结

MyBatis 缓存是提升查询性能的核心手段,需结合业务场景合理使用:

  1. 一级缓存是基础,默认开启,适用于单次会话内的重复查询;
  2. 二级缓存需手动配置,适用于跨会话的静态数据缓存;
  3. 分布式系统优先使用 Redis 替代本地二级缓存,保证缓存共享;
  4. 缓存的核心是 "权衡":既要提升性能,也要保证数据一致性,避免过度缓存。

通过合理配置一级 / 二级缓存,结合业务特征调整缓存策略,可在不修改业务代码的前提下,大幅降低数据库压力,提升系统响应速度。

相关推荐
故事不长丨6 小时前
C#集合:解锁高效数据管理的秘密武器
开发语言·windows·c#·wpf·集合·winfrom·字典
故事不长丨8 小时前
C#队列深度剖析:解锁高效编程的FIFO密码
visualstudio·c#·wpf·多线程·winfrom·队列·queue
lhrimperial14 小时前
微服务架构深度解析-微服务理论基础(一)
微服务·架构·wpf
艾斯比的日常16 小时前
XXL-Job 核心原理深度解析
wpf
泉飒2 天前
WinForm与WPF的异同点
wpf·winform
fireworkseasycold2 天前
wpf 基于 JSON 的扩展配置 (Extended Config)” 功能
oracle·json·wpf
脩衜者3 天前
极其灵活且敏捷的WPF组态控件ConPipe 2026
前端·物联网·ui·wpf
张人玉3 天前
西门子 S7 PLC 通信 WPF 应用分析笔记
笔记·c#·wpf·plc
张人玉3 天前
整合 Sugar ORM 连接 SQLite 数据库到 WPF 折线图项目
数据库·sqlite·c#·wpf