【Java 开发日记】我们来说一下 Mybatis 的缓存机制

MyBatis 的缓存机制主要分为两级(官方原生支持的),很多人在实际项目中还会自己扩展第三级缓存(比如接 Redis / Caffeine 等)。今天我们按最经典的思路来梳理一下,尽量讲得清楚一点。

一级缓存(PerpetualCache + SqlSession 级别)

  • 范围:同一个 SqlSession 内部
  • 默认一直开启,无法通过配置完全关闭(只能调级别)
  • 存储介质:默认使用 PerpetualCache(就是一个 HashMap)
  • Key 的组成
    statementId + 参数 + 分页参数(RowBounds) + SQL 语句本身(带 ? 的占位符形式)
  • 命中条件:完全相同的 SQL + 完全相同的参数 + 同一个 SqlSession

典型使用场景举例(最容易观察到一级缓存的地方):

try (SqlSession session = factory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);

    User u1 = mapper.selectById(1);     // 查数据库
    User u2 = mapper.selectById(1);     // 直接命中一级缓存
    User u3 = mapper.selectById(2);     // 再查一次数据库

    // 同一个 session 内,哪怕换个方式调用
    User u4 = session.selectOne("com.xxx.UserMapper.selectById", 1);  // 还是命中一级缓存
}

一级缓存什么时候失效 / 清空?(非常重要,面试高频)

  1. SqlSession 调用了 commit() / rollback() / close()
  2. 执行了本 namespace 下的 任何 update/insert/delete(即使不是同一条语句)
  3. 手动调用 session.clearCache()
  4. 设置了 flushCache=true 的查询语句
  5. 查询时用了 ResultHandler(自定义结果处理器)或 Cursor(流式查询)

配置方式(一般不用改,但可以了解):

<setting name="localCacheScope" value="SESSION"/>   <!-- 默认 SESSION -->
<!-- 或者改成 STATEMENT(相当于关闭了跨语句的一级缓存) -->
<setting name="localCacheScope" value="STATEMENT"/>

二级缓存(跨 SqlSession,namespace 级别)

  • 范围:同一个 namespace(通常就是一个 Mapper.xml 或 @Mapper 接口)
  • 默认关闭的,需要手动开启
  • 存储介质:默认 PerpetualCache(HashMap),但强烈建议换成支持序列化的实现(如 Ehcache、Redis、Caffeine 等)
  • 序列化要求:实体类必须实现 Serializable 接口(否则反序列化会报错)

开启二级缓存的完整步骤(最常见写法)

  1. 全局开启(mybatis-config.xml)
<settings>
    <setting name="cacheEnabled" value="true"/>    <!-- 默认就是 true,但建议写上 -->
</settings>
  1. 在具体的 Mapper.xml 上开启(最关键一步)
<mapper namespace="com.example.dao.UserMapper">
    <!-- 开启二级缓存 -->
    <cache 
        eviction="LRU"              <!-- 回收策略:最近最少使用 -->
        flushInterval="60000"       <!-- 刷新间隔(毫秒),不写默认不清 -->
        size="512"                  <!-- 最多缓存多少对象 -->
        readOnly="true"             <!-- 只读=true 性能更好,但不能修改对象 -->
    />

    <!-- 或者最简写法(默认值很多) -->
    <!-- <cache/> -->

    <select id="selectById" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>

或使用注解方式(MyBatis 3.4+ 支持)

@CacheNamespace(
    eviction = LruCache.class,
    flushInterval = 60000,
    size = 512,
    readOnly = true
)
public interface UserMapper { ... }

二级缓存什么时候生效?

  • 同一个 namespace
  • 相同的 SQL + 相同的参数
  • 第一次查询后 SqlSession 正常 close()(数据才会被写到二级缓存)
  • 后续其他 SqlSession 才能读到

二级缓存失效 / 刷新时机(非常重要)

  • 本 namespace 下任意一条 增删改 语句执行(默认会清空整个 namespace 的二级缓存)
  • <cache> 标签配置了 flushInterval,到时间自动清
  • 手动调用 session.clearCache()(会同时清一级+二级)
  • 缓存对象被 LRU/FIFO 等策略淘汰

一级 vs 二级 快速对比表

维度一级缓存二级缓存
作用域SqlSessionnamespace(通常一个 Mapper)
默认开启
共享范围同一个会话多个会话(跨 SqlSession)
存储位置内存(PerpetualCache)可自定义(Ehcache/Redis等)
实体序列化要求需要(Serializable)
失效时机commit/rollback/close/update/delete 等update/delete + flushInterval 等
实际命中率较高(同一次请求多次查)取决于业务(跨请求共享)
风险点脏读风险较低脏读/缓存雪崩风险较高

真实项目中常见的做法(2025-2026 年视角)

  1. 大部分中小项目:只用一级缓存 + 局部热点数据用 @Cacheable(Spring Cache)或 Caffeine
  2. 中大型项目
  • 一级缓存照用
  • 二级缓存基本不用原生的 PerpetualCache(太弱)
  • 常用方案:
    • mybatis-redis-cache(redis 做 L2)
    • mybatis-caffeine-cache
    • mybatis-ehcache(老项目常见)
    • 或者直接在 service 层用 Spring Cache 注解
  1. 高并发读多写少:考虑引入 三级缓存(Redis / Memcached / 多级组合)

你现在项目里是怎么处理 MyBatis 缓存的?
是一级就够用,还是开了二级,或者直接扔到 Redis 了?可以聊聊你的实际做法~

文章已创建 4206

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部