Skip to content

MyBatis 缓存机制

  MyBatis 提供两级缓存机制来减少数据库访问次数,提升查询性能。理解缓存的作用域和失效机制是优化 MyBatis 性能的关键。

一、缓存架构图

MyBatis 缓存层级:

  请求 1                   请求 2
    │                        │
    ▼                        ▼
  SqlSession 1            SqlSession 2
    │                        │
    ├── 一级缓存(SqlSession 级别)    ├── 一级缓存(SqlSession 级别)
    │   默认开启,不可关闭              │   默认开启,不可关闭
    │   作用域:同一个 SqlSession       │   作用域:同一个 SqlSession
    │                        │
    └──────┬──────────────┘


  ┌─────────────────────────────────┐
  │       二级缓存(Mapper 级别)     │
  │   作用域:同一个 namespace 的所有  │
  │   SqlSession 共享                         │
  │   默认关闭,需要手动开启               │
  └─────────────────────────────────┘


         数据库

二、一级缓存(Local Cache)

特点:
  - 默认开启,无法关闭
  - SqlSession 级别,同一个 SqlSession 内的查询共享
  - 基于 PerpetualCache(HashMap)实现
  - 执行增删改、commit、close、clearCache 时失效

2.1 一级缓存演示

java
// 同一个 SqlSession 内的两次相同查询,只查一次数据库
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

User user1 = mapper.selectById(1L);  // 查数据库,SQL 打印
User user2 = mapper.selectById(1L);  // 命中缓存,不打印 SQL

System.out.println(user1 == user2);  // true,同一个对象

2.2 一级缓存失效场景

java
// 场景一:不同 SqlSession
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);

mapper1.selectById(1L);  // 查数据库
mapper2.selectById(1L);  // 查数据库(不同 SqlSession,缓存不共享)

// 场景二:执行增删改
mapper1.selectById(1L);  // 查数据库,放入缓存
mapper1.insert(new User());  // 增删改清空缓存
mapper1.selectById(1L);  // 再次查数据库

// 场景三:手动清空缓存
mapper1.selectById(1L);  // 查数据库
sqlSession.clearCache();  // 手动清空
mapper1.selectById(1L);  // 再次查数据库

// 场景四:查询条件不同
mapper1.selectById(1L);  // 查数据库
mapper1.selectById(2L);  // 查数据库(参数不同,不走缓存)

2.3 一级缓存作用域

xml
<settings>
    <!-- 一级缓存作用域:SESSION(默认)/ STATEMENT -->
    <setting name="localCacheScope" value="SESSION"/>
</settings>
作用域说明
SESSIONSqlSession 级别整个 SqlSession 生命周期内共享(默认)
STATEMENT语句级别仅当前语句内共享,执行完就清除

三、二级缓存(Second Level Cache)

特点:
  - 默认关闭,需要手动开启
  - Mapper 级别(namespace 级别),多个 SqlSession 共享
  - 事务提交后才会写入缓存
  - 可插拔设计,支持自定义缓存实现

3.1 开启二级缓存

步骤一:全局配置开启

xml
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

步骤二:Mapper XML 中添加 <cache/> 标签

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

    <!-- 自定义缓存参数 -->
    <cache
        eviction="LRU"              <!-- 回收策略 -->
        flushInterval="60000"       <!-- 刷新间隔(毫秒) -->
        size="512"                  <!-- 最多缓存 512 个对象引用 -->
        readOnly="true"             <!-- 只读缓存,性能更好 -->
    />
</mapper>

步骤三:实体类实现 Serializable

java
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // ...
}

3.2 二级缓存演示

java
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();

UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);

User user1 = mapper1.selectById(1L);  // 查数据库,SQL 打印
session1.commit();  // 提交后,数据写入二级缓存

User user2 = mapper2.selectById(1L);  // 命中二级缓存,不打印 SQL

// 注意:user1 和 user2 不是同一个对象(不同 SqlSession)
// 二级缓存存的是数据,不是对象引用

3.3 缓存参数

属性说明默认值
eviction回收策略:LRU/FIFO/SOFT/WEAKLRU
flushInterval刷新间隔(毫秒),不设置则调用时刷新不设置
size最大缓存对象数1024
readOnly只读(true=性能好,false=安全)false

回收策略:

LRU   → 最近最少使用(默认)
FIFO  → 先进先出
SOFT  → 软引用(内存不足时 GC 回收)
WEAK  → 弱引用(更积极回收)

3.4 useCache 和 flushCache

xml
<!-- useCache:是否使用二级缓存(默认 true) -->
<select id="selectById" resultType="User" useCache="true">
    SELECT * FROM user WHERE id = #{id}
</select>

<!-- flushCache:执行后是否清空缓存(默认 false,增删改默认为 true) -->
<select id="selectAll" resultType="User" flushCache="false">
    SELECT * FROM user
</select>

四、自定义缓存

xml
<!-- 方式一:使用第三方缓存(如 Ehcache) -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

<!-- 方式二:Redis 缓存 -->
<cache type="org.mybatis.caches.redis.RedisCache"/>
java
// 方式三:自定义缓存实现
public class MyCache implements Cache {
    private String id;
    private Map<Object, Object> cache = new ConcurrentHashMap<>();

    public MyCache(String id) {
        this.id = id;
    }

    @Override
    public String getId() { return id; }

    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value);
    }

    @Override
    public Object getObject(Object key) {
        return cache.get(key);
    }

    @Override
    public Object removeObject(Object key) {
        return cache.remove(key);
    }

    @Override
    public void clear() {
        cache.clear();
    }

    @Override
    public int getSize() {
        return cache.size();
    }
}

五、缓存执行顺序

查询时缓存查找顺序:

  ① 先查二级缓存
      ↓ 没命中
  ② 再查一级缓存
      ↓ 没命中
  ③ 查数据库

  ④ 结果放入一级缓存

  ⑤ 事务提交后,一级缓存数据写入二级缓存

六、缓存使用建议

使用建议:
  ① 不要滥用二级缓存
     → 只对读多写少、数据量小的表启用
     → 频繁更新的表禁用二级缓存

  ② 避免脏读
     → 多表关联查询时,关联表更新会导致缓存不一致
     → 单表查询才适合用二级缓存

  ③ 分布式环境
     → 内置缓存只在单机有效
     → 分布式环境需要使用 Redis 等集中式缓存

  ④ 预防内存溢出
     → 控制 size 大小
     → 避免缓存大结果集

  ⑤ 一级缓存无法关闭
     → 但可以设置 localCacheScope=STATEMENT 来限制作用域

七、速记总结

MyBatis 两级缓存:

  一级缓存:SqlSession 级别,默认开启,不可关闭
    → 同一个 SqlSession 内查询共享
    → 增删改/commit/close 后失效

  二级缓存:Mapper 级别,需要手动开启
    → 同一个 namespace 的多个 SqlSession 共享
    → 实体类必须实现 Serializable
    → 事务提交后才写入缓存

缓存查找顺序:
  二级 → 一级 → 数据库

速记口诀:
  一级会话内,二级跨会话,
  一级默认开,二级要配置,
  先查二级再一级,都没有才走库。

八、面试要点

问题答案要点
一级缓存和二级缓存的区别?一级 SqlSession 级别,二级 Mapper 级别
一级缓存什么情况下失效?增删改、commit、close、不同 SqlSession
为什么实体类要实现 Serializable?二级缓存需要序列化存储
分布式环境下缓存怎么处理?用 Redis 等集中式缓存替代内置缓存