原创

MyBatis的三级缓存到底是什么?一文讲清一级缓存、二级缓存与常见误解

很多文章会把 MyBatis 的缓存机制说成“一级缓存、二级缓存、三级缓存”,但严格来说,MyBatis 官方只有一级缓存和二级缓存,没有官方定义的三级缓存

如果你在项目里听到“三级缓存”这个说法,通常指的并不是 MyBatis 框架本身额外提供了一层标准缓存,而是把 MyBatis 二级缓存 + Spring Cache / Redis / 业务自定义缓存 一起统称为“三级缓存”。这个叫法可以作为项目里的口语表达,但不能当作 MyBatis 原生机制来理解

先说结论

MyBatis 的缓存体系可以明确分成两层:

  1. 一级缓存SqlSession 级别,默认开启
  2. 二级缓存Mapper 命名空间级别,需要显式开启

所谓“三级缓存”,一般有两种来源:

  • Spring / Redis / Caffeine / 自定义缓存 误叫成 MyBatis 三级缓存
  • MyBatis 的查询流程中多层包装装饰器 误认为“三级缓存层次”

所以,理解这个主题最重要的一点不是背概念,而是先把边界划清:

MyBatis 原生只提供一级缓存和二级缓存。三级缓存不是官方缓存层级。


一级缓存是什么

一级缓存是 SqlSession 级别缓存。 同一个 SqlSession 中,如果执行了相同条件的查询,MyBatis 会优先从本地缓存中取结果,而不是再次访问数据库。

一级缓存的特点

  • 默认开启
  • 作用域是当前 SqlSession
  • 不需要额外配置
  • 事务会话结束后缓存也随之结束
  • 执行 insertupdatedeletecommitrollbackclose 等操作时,一级缓存通常会被清空

示例

try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    User user1 = mapper.selectById(1L);
    User user2 = mapper.selectById(1L);

    System.out.println(user1 == user2);
}

在同一个 SqlSession 内,如果两次查询的 SQL、参数、分页条件等都一致,第二次通常会命中一级缓存。

一级缓存为什么容易“失效”

一级缓存不是全局缓存,它的生命周期非常短,只在当前会话中有效。 而在 Spring + MyBatis 项目里,很多场景下 SqlSession 的使用是由框架代理管理的,开发者往往感知不到它的存在,因此会出现两种误解:

  • 以为一级缓存是全局生效的
  • 发现两次查询都访问数据库,就误以为一级缓存没开

实际上,大多数情况下不是没开,而是不在同一个 SqlSession 中


二级缓存是什么

二级缓存是 Mapper 命名空间级别缓存。 它的作用范围比一级缓存大,不再局限于单个 SqlSession,而是可以在多个 SqlSession 之间共享。

二级缓存的特点

  • 默认不开启,需要手动配置
  • 作用域是 Mapper namespace
  • 多个 SqlSession 可以共享
  • 缓存对象需要可序列化
  • 只有在 SqlSession 提交或关闭后,一级缓存中的数据才可能刷入二级缓存
  • 任意一次更新操作会清空对应命名空间下的二级缓存

开启方式

1. 全局配置开启二级缓存

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

2. 在 Mapper XML 中声明缓存

<mapper namespace="com.example.mapper.UserMapper">
    <cache/>
</mapper>

也可以指定缓存参数:

<mapper namespace="com.example.mapper.UserMapper">
    <cache
        eviction="LRU"
        flushInterval="60000"
        size="512"
        readOnly="false"/>
</mapper>

参数说明

参数 含义
eviction 淘汰策略,常见有 LRUFIFOSOFTWEAK
flushInterval 刷新间隔,单位毫秒
size 缓存对象数量上限
readOnly 是否只读,true 时返回同一对象实例,性能更高但不适合修改对象

示例查询

<select id="selectById" resultType="com.example.entity.User">
    select id, username, age from user where id = #{id}
</select>

如果开启了二级缓存,多个 SqlSession 对这个查询的结果可以共享缓存数据。


为什么会出现“三级缓存”这个说法

“三级缓存”这个说法,主要不是来自 MyBatis 官方,而是来自实际项目中的扩展使用习惯。

常见理解通常是这样分层的:

层级 常见说法 实际归属
一级缓存 SqlSession 本地缓存 MyBatis 原生
二级缓存 Mapper 级缓存 MyBatis 原生
三级缓存 Redis / Caffeine / Spring Cache / 自定义缓存 业务层或集成层

这种说法的问题在于: 它把不同职责、不同实现边界的缓存强行放进了一个编号体系里。

更准确的表达应该是:

  • MyBatis 有两级原生缓存
  • 项目中可以在 MyBatis 之外再叠加业务缓存或分布式缓存

这才是工程上更严谨的说法。


“三级缓存”在项目里通常指什么

1. MyBatis + Redis

很多系统会把热点数据放到 Redis 中,作为跨应用实例共享的缓存层。 这时候有人会说:

  • 一级缓存:SqlSession
  • 二级缓存:Mapper
  • 三级缓存:Redis

这种说法从“项目架构分层”角度可以理解,但要注意:

Redis 不是 MyBatis 原生三级缓存,而是应用系统额外引入的缓存层。

常见流程

  1. 先查 Redis
  2. Redis 没命中,再走数据库查询
  3. 查询结果写回 Redis
  4. 数据更新时删除或更新 Redis

这套机制通常由以下方式实现:

  • Spring Cache + Redis
  • 自定义 AOP 缓存逻辑
  • 手动编码缓存读写

而不是 MyBatis 自动帮你做完。


2. MyBatis 二级缓存底层接入第三方缓存实现

MyBatis 的二级缓存支持自定义实现,理论上你可以把二级缓存底层接到 Redis、Ehcache 等存储上。

例如通过自定义 Cache 接口实现:

public class RedisCache implements Cache {

    private final String id;

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

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

    @Override
    public void putObject(Object key, Object value) {
        // 写入 Redis
    }

    @Override
    public Object getObject(Object key) {
        // 从 Redis 获取
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        return null;
    }

    @Override
    public void clear() {
        // 清空 namespace 对应缓存
    }

    @Override
    public int getSize() {
        return 0;
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return null;
    }
}

然后在 Mapper 中使用:

<mapper namespace="com.example.mapper.UserMapper">
    <cache type="com.example.cache.RedisCache"/>
</mapper>

这种场景下,很多人也会顺手把 Redis 叫成“三级缓存”。 但严格说,这其实仍然是MyBatis 二级缓存的自定义实现,而不是多出一个新的原生层级。

也就是说:

  • 从 MyBatis 框架视角看,它还是二级缓存
  • 从部署架构视角看,它的存储介质变成了 Redis

这两件事不能混为一谈。


为什么 MyBatis 官方没有三级缓存

原因很简单:MyBatis 的职责边界本来就不应该无限扩张。

MyBatis 是 ORM 持久层框架,它负责:

  • SQL 执行
  • 参数映射
  • 结果映射
  • 会话级缓存
  • 命名空间级缓存

但分布式缓存、跨服务共享缓存、缓存一致性治理,这些已经超出了它作为持久层框架的核心职责。

如果框架继续内建“三级缓存”,会直接带来这些问题:

  • 如何支持集群共享
  • 如何处理序列化
  • 如何处理缓存穿透、击穿、雪崩
  • 如何做更新广播和一致性
  • 如何兼容不同缓存中间件

这些都不是 MyBatis 应该强耦合解决的问题。 所以它只保留了最贴近持久层的两级缓存,把更大的缓存体系交给应用架构层去完成,这个设计本身是合理的。


一级缓存和二级缓存到底怎么配合

理解这个问题很关键,因为很多人虽然知道有两级缓存,但不知道执行顺序。

大致流程如下:

  1. 发起查询
  2. 先查一级缓存
  3. 一级缓存未命中,再查二级缓存
  4. 二级缓存未命中,再查数据库
  5. 查询结果先进入一级缓存
  6. 当前 SqlSession 提交或关闭后,相关结果才有机会写入二级缓存

这意味着:

  • 一级缓存优先级更高
  • 二级缓存不是每次查完立刻就共享
  • 二级缓存依赖会话结束后的写入时机

这也是很多人本地测试二级缓存时“看起来没生效”的原因。 他们通常在一个没提交、没关闭的会话里反复查,自然看到的只是一级缓存行为。


二级缓存为什么在真实项目里经常不推荐重度使用

MyBatis 二级缓存并不是不能用,而是它在复杂业务系统里往往不够灵活。

主要问题有下面几个。

1. 粒度是 namespace,不够细

二级缓存是按 Mapper 命名空间管理的。 只要这个命名空间里发生了更新,相关缓存通常就会被清空。

这会导致一个问题:

  • 明明只更新了一条记录
  • 但整个 Mapper 下相关缓存可能都失效了

对于读多写少、查询模式单一的场景,这还可以接受。 但对复杂业务系统来说,粒度通常太粗。

2. 跨表关联和一致性难处理

如果一个查询依赖多张表,而更新发生在其他 Mapper 中,就容易出现缓存一致性理解复杂的问题。

你会发现:

  • 查询缓存放在 A Mapper
  • 更新发生在 B Mapper
  • 是否及时失效,不一定符合业务预期

这也是为什么很多团队宁愿把缓存控制放在 service 层,而不是依赖 MyBatis 二级缓存。

3. 对分布式系统支持有限

MyBatis 二级缓存天然更偏单体应用内的 namespace 缓存。 在多实例部署场景下,如果没有额外接入统一缓存中间件,它无法天然解决多节点之间的数据共享和一致性问题。

4. 调优空间有限

真正的大型系统缓存设计,通常要考虑:

  • 过期策略
  • 空值缓存
  • 热点数据预热
  • 逻辑过期
  • 双删策略
  • 异步刷新
  • 限流降级

这些能力,MyBatis 二级缓存并不擅长。


什么场景适合用 MyBatis 二级缓存

尽管它有局限,但也不是完全没价值。

比较适合的场景包括:

1. 读多写少的基础字典数据

例如:

  • 地区表
  • 配置表
  • 枚举扩展表
  • 码表

这些数据变化频率低、查询频率高,很适合缓存。

2. 单体应用或简单系统

系统规模不大,部署节点少,对缓存一致性要求没那么高,这时二级缓存可以减少一部分数据库压力。

3. 查询模型稳定

如果 SQL 模式相对固定,没有太多复杂动态条件,也更容易从缓存中受益。


什么场景更适合把缓存放到业务层

下面这些场景,通常更适合 Spring Cache、Redis、Caffeine 或自定义缓存方案,而不是依赖 MyBatis 二级缓存。

1. 分布式部署

多个应用实例共享缓存时,业务层统一接 Redis 更自然。

2. 需要精细控制失效策略

例如只删除某个商品缓存、某个用户缓存,而不是整组 namespace 全清。

3. 需要缓存的不只是数据库查询结果

很多业务缓存并不是某条 SQL 的直接结果,而是经过聚合、转换、拼装后的 DTO、视图对象、统计结果。 这类缓存天然更适合放在 service 层。

4. 需要统一缓存治理

比如监控命中率、统一过期策略、降级兜底,这些一般也是业务缓存体系来做,而不是交给 MyBatis。


一个更准确的工程化理解方式

与其问“MyBatis 的三级缓存是什么”,不如换成下面这种理解方式:

持久层缓存

  • 一级缓存:SqlSession
  • 二级缓存:Mapper namespace

应用层缓存

  • Spring Cache
  • Caffeine
  • Redis
  • 自定义缓存组件

分布式缓存体系

  • Redis Cluster
  • 本地缓存 + 分布式缓存多级架构
  • 缓存一致性方案

这样分层之后,概念会清楚很多。 MyBatis 只负责它职责范围内的缓存,而更高层次的缓存由应用架构负责。

这也是为什么在正式文档、技术评审和面试回答里,最好不要直接说“MyBatis 有三级缓存”,而应该说:

MyBatis 原生提供一级缓存和二级缓存;项目中如果再叠加 Redis 或 Spring Cache,可以形成更完整的多层缓存架构,但那不属于 MyBatis 官方三级缓存。


面试里应该怎么回答

如果面试官问“你说说 MyBatis 的三级缓存”,比较稳妥的回答方式是:

严格来说,MyBatis 官方只有一级缓存和二级缓存,没有官方定义的三级缓存。 一级缓存是 SqlSession 级别,默认开启;二级缓存是 Mapper 命名空间级别,需要显式开启。 实际项目里有人把 Redis、Spring Cache 这类业务层缓存称为三级缓存,但那不是 MyBatis 原生能力,而是系统整体缓存架构的一部分。

这个回答的好处是:

  • 概念边界清晰
  • 不会把误传说法当成官方事实
  • 又能说明你知道项目里为什么会出现这个词

常见误区

误区一:MyBatis 天生有三级缓存

错误。 官方只有一级缓存和二级缓存。

误区二:二级缓存一开启就一定比 Redis 更好用

错误。 二级缓存更接近持久层,配置方便,但在复杂系统中灵活性和治理能力通常不如 Redis 方案。

误区三:一级缓存能跨请求共享

错误。 一级缓存只在当前 SqlSession 内有效,跨请求通常不会共享。

误区四:Redis 接进 MyBatis 就变成三级缓存

不准确。 如果 Redis 只是作为 MyBatis Cache 接口的底层实现,那从 MyBatis 视角看仍然是二级缓存。

误区五:二级缓存适合所有查询场景

错误。 动态 SQL 多、更新频繁、跨表复杂的业务中,二级缓存收益往往不高,甚至可能增加维护成本。


一个简化示意图

请求
  ↓
Service 业务层缓存(可选:Redis / Caffeine / Spring Cache)
  ↓
MyBatis 二级缓存(Mapper 级,可选)
  ↓
MyBatis 一级缓存(SqlSession 级,默认)
  ↓
Database

注意,这张图描述的是项目整体可能出现的多层缓存架构,不是 MyBatis 官方定义的“三层缓存”。


实战建议

如果你在做真实项目,缓存设计可以按下面思路判断:

只做简单 CRUD、小型系统

可以先依赖:

  • 一级缓存默认能力
  • 必要时少量使用二级缓存

业务复杂、读流量大、分布式部署

更推荐:

  • MyBatis 一级缓存作为自然存在的会话内优化
  • 二级缓存谨慎使用,能不用就不用
  • 主缓存体系放在 Service 层,用 Redis / Caffeine / Spring Cache 统一管理

对一致性要求高

优先把缓存策略放在业务层,不要过度依赖 MyBatis 二级缓存的自动行为。 因为业务层更容易控制:

  • 删哪个 key
  • 何时删
  • 是否延迟双删
  • 是否异步重建
  • 是否做热点保护

总结

MyBatis 的“三级缓存”之所以经常让人困惑,根本原因是项目实践中的口语表达和框架官方定义被混在了一起。

把这个问题说清楚,只需要记住三点:

  1. MyBatis 原生只有一级缓存和二级缓存
  2. 所谓三级缓存,通常是项目里额外引入的 Redis、Spring Cache、Caffeine 等业务缓存
  3. 如果 Redis 只是作为 MyBatis 二级缓存的底层实现,本质上它仍然属于二级缓存扩展,不是官方新增层级

所以,讨论 MyBatis 缓存机制时,最严谨的表达不是“三级缓存怎么用”,而是:

  • 一级缓存怎么工作
  • 二级缓存何时适合开启
  • 业务层缓存和持久层缓存如何分工

把这几个问题分开理解,MyBatis 的缓存体系就不会再混乱。

正文到此结束
评论插件初始化中...
Loading...