Skip to content

MyBatis 缓存机制详解

一级缓存、二级缓存、缓存配置、分布式缓存


概述

MyBatis 提供两级缓存机制,用于减少数据库访问次数,提升性能。

┌─────────────────────────────────────────────────┐
│                   SqlSession                     │
│  ┌───────────────────────────────────────────┐  │
│  │           一级缓存(会话级)                │  │
│  │  ┌─────────────────────────────────────┐  │  │
│  │  │     二级缓存(Mapper 级)            │  │  │
│  │  │  ┌─────────────┬─────────────┐      │  │  │
│  │  │  │ UserMapper │ OrderMapper │      │  │  │
│  │  │  │  (PerpetualCache)  │              │  │  │
│  │  │  └─────────────┴─────────────┘      │  │  │
│  │  └─────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘

一级缓存

作用域

SqlSession 级别,同一个 SqlSession 的查询会被缓存。

工作原理

查询请求

检查 SqlSession 缓存(Map)

  有 → 返回缓存结果
  无 → 查询数据库 → 写入缓存 → 返回结果

示例

java
try (SqlSession session = sqlSessionFactory.openSession()) {
    // 第一次查询,走数据库
    User user1 = session.selectOne("mapper.UserMapper.selectById", 1L);
    
    // 第二次查询,走一级缓存
    User user2 = session.selectOne("mapper.UserMapper.selectById", 1L);
    
    // true:同一个对象(同一个 SqlSession)
    System.out.println(user1 == user2);  
}

缓存失效情况

java
try (SqlSession session = sqlSessionFactory.openSession()) {
    // 查询
    User user1 = session.selectOne("mapper.UserMapper.selectById", 1L);
    
    // 执行 DML 操作(增删改),缓存被清空
    session.update("mapper.UserMapper.update", user);
    session.commit();  // 提交事务,缓存失效
    
    // 再次查询,走数据库
    User user2 = session.selectOne("mapper.UserMapper.selectById", 1L);
    
    System.out.println(user1 == user2);  // false
}
操作一级缓存状态
commit()清空缓存
close()清空缓存
DML 操作(增删改)清空缓存
clearCache()清空缓存

禁用一级缓存

java
// 每次查询都强制从数据库读取
try (SqlSession session = sqlSessionFactory.openSession()) {
    // 方式一:flushCache=true
    User user = session.selectOne("mapper.UserMapper.selectById", 
        1L, RowBounds.DEFAULT, new CacheKey(), true);
    
    // 方式二:XML 配置禁用
}

二级缓存

作用域

Mapper 级别,跨 SqlSession 共享,只要 Mapper 相同即可共享缓存。

配置步骤

1. 开启二级缓存(全局)

xml
<!-- mybatis-config.xml -->
<settings>
    <!-- 开启二级缓存(默认 true,可不配) -->
    <setting name="cacheEnabled" value="true"/>
</settings>

2. Mapper 中启用缓存

xml
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    
    <!-- 开启当前 Mapper 的二级缓存 -->
    <cache/>
    
</mapper>

3. 实体类实现 Serializable

java
public class User implements Serializable {
    private Long id;
    private String name;
    // getters, setters
}

缓存属性配置

xml
<cache eviction="LRU"        <!-- 淘汰策略:FIFO/LRU/SOFT/WEAK -->
       flushInterval="60000"  <!-- 刷新间隔:60秒 -->
       size="512"             <!-- 缓存条目数 -->
       readOnly="false">      <!-- 只读 -->
</cache>
属性可选值说明
evictionFIFO, LRU, SOFT, WEAK缓存淘汰算法
flushInterval毫秒定时刷新缓存
size正整数缓存最大数量
readOnlytrue, falsetrue=返回同一对象,false=返回拷贝

淘汰策略

策略说明
LRU(默认)最近最少使用
FIFO先进先出
SOFT软引用,基于 GC
WEAK弱引用,基于 GC

二级缓存工作流程

SqlSession 1 查询

一级缓存未命中

检查二级缓存

  有 → 返回缓存结果
  无 → 查询数据库 → 写入一级缓存 + 二级缓存

SqlSession 1 关闭

一级缓存数据写入二级缓存

SqlSession 2 查询

一级缓存未命中

二级缓存命中 → 返回结果

示例

java
// SqlSession 1
try (SqlSession session1 = sqlSessionFactory.openSession()) {
    User user = session1.selectOne("mapper.UserMapper.selectById", 1L);
    // 查询数据库,存入一级/二级缓存
    
    System.out.println(user);  // 查 DB
}
// session1 关闭,一级缓存写入二级缓存

// SqlSession 2(不同会话)
try (SqlSession session2 = sqlSessionFactory.openSession()) {
    User user = session2.selectOne("mapper.UserMapper.selectById", 1L);
    // 二级缓存命中,不查数据库
    
    System.out.println(user);  // 命中缓存
}

readOnly 模式

xml
<!-- readOnly=true:返回同一对象引用,效率高但线程不安全 -->
<cache readOnly="true"/>

<!-- readOnly=false(默认):返回对象拷贝,安全但效率低 -->
<cache readOnly="false"/>
java
// readOnly=true
User user1 = session1.selectOne("selectById", 1L);
User user2 = session2.selectOne("selectById", 1L);
System.out.println(user1 == user2);  // true

// readOnly=false
System.out.println(user1 == user2);  // false

缓存查询设置

useCache

xml
<!-- 禁用当前查询的二级缓存 -->
<select id="selectAll" resultMap="BaseResultMap" useCache="false">
    SELECT * FROM users ORDER BY id
</select>

flushCache

xml
<!-- 执行后清空缓存 -->
<select id="selectById" resultMap="BaseResultMap" flushCache="true">
    SELECT * FROM users WHERE id = #{id}
</select>

<!-- DML 操作默认 flushCache="true" -->
<insert id="insert" flushCache="true">
    INSERT INTO users (name) VALUES (#{name})
</insert>

缓存局限性

1. 脏数据问题

java
// 线程 A 查询数据(缓存)
User user = sessionA.selectOne("selectById", 1L);

// 线程 B 直接修改数据库(未通过 MyBatis)
// UPDATE user SET name = 'New Name' WHERE id = 1;

// 线程 A 再次查询,读到的是缓存中的旧数据
User user2 = sessionA.selectOne("selectById", 1L);
// user2.getName() 仍是旧值!

解决方案

方案说明
设置 flushInterval定时过期
Mapper 粒度缓存按业务拆分
使用分布式缓存Redis 替代
事务内读写一致性确保同事务内读写同 session

分布式缓存(Redis)

使用场景

单实例 MyBatis 缓存只对本 JVM 有效,集群环境下需分布式缓存。

集成 Redis

xml
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>2.0.0</version>
</dependency>
properties
# redis.properties
redis.host=localhost
redis.port=6379
redis.database=0
xml
<cache type="org.mybatis.caches.redis.RedisCache"
       eviction="LRU"
       flushInterval="1800000"
       size="1024"/>

缓存最佳实践

推荐配置

xml
<!-- 通用缓存配置 -->
<cache eviction="LRU"
       flushInterval="1800000"
       size="512"
       readOnly="false"/>

<!-- 查询级配置 -->
<select id="selectById" resultMap="ResultMap" useCache="true">
    SELECT * FROM users WHERE id = #{id}
</select>

<select id="selectAll" resultMap="ResultMap" useCache="false">
    <!-- 不常变化的表禁用缓存 -->
    SELECT * FROM sys_config
</select>

缓存使用场景

场景是否使用缓存
读多写少✅ 使用
实时性要求高❌ 禁用
大数据量查询❌ 禁用
关联查询⚠️ 谨慎使用
分布式环境❌ 使用 Redis

相关页面

  • **Java\MyBatis\MyBatis-完全指南** - 返回总览
  • **Java\MyBatis\MyBatis-进阶应用** - 分页插件、性能优化
  • **Java\MyBatis\MyBatis-关联查询** - 一对多缓存问题

#标签 #mybatis #缓存 #一级缓存 #二级缓存 #cache #redis

Knowledge4J — Java 知识库