说说 Mybatis 的缓存机制?

#Java#面试八股

说说 Mybatis 的缓存机制?

结论先行:MyBatis 有两级缓存。一级缓存在 SqlSession 内,默认开启、无法关闭,本质是一个 PerpetualCache 里的 HashMap;二级缓存在 namespace 级,需要显式配置 <cache/> 才生效,跨 SqlSession 共享。两者都只缓存查询结果,任何增删改都会让相关缓存整体失效,所以「缓存命中率」远不如 Redis,误用反而会读到脏数据。

1. 两级缓存的位置

        ┌──────────────────── SqlSessionFactory ────────────────────┐
        │                                                           │
        │   namespace = com.example.mapper.UserMapper               │
        │   ┌──────────────── 二级缓存(跨 SqlSession)───────────┐  │
        │   │  需要 <cache/> 开启,可配 LRU/FIFO/软引用            │  │
        │   └───────────────────────┬─────────────────────────────┘  │
        │                           │ 未命中                          │
        │   SqlSession A            ▼                                │
        │   ┌──────────── 一级缓存(Session 级)────────────┐        │
        │   │  PerpetualCache { CacheKey -> List<Object> }  │        │
        │   └───────────────────────┬───────────────────────┘        │
        └───────────────────────────┼────────────────────────────────┘
                                    ▼ 未命中才查库

一级缓存的生命周期跟随 SqlSession:Spring 集成后,一个没有事务的查询结束后 SqlSession 就关闭,一级缓存随之消失;只有在同一个事务(同一个 SqlSession)内连续查两次,才能看到命中。

2. 一级缓存的命中条件

CacheKey 由 MappedStatement 的 id、分页参数 RowBounds 的 offset/limit、以及最终传给 JDBC 的 SQL 与参数值共同决定。任何一个不同,都不会命中。

场景是否命中一级缓存原因
同一 Session 内两次相同查询,中间无写操作命中CacheKey 完全一致
同一 Session 内中间执行了 update不命中写操作调用 clearLocalCache()
两次查询参数不同不命中参数参与 CacheKey 计算
手动 sqlSession.clearCache() 后不命中缓存被清空
两次查询在不同事务中不命中属于不同 SqlSession
查询前手动 flushCache="true"不命中执行前清空本地缓存

3. 二级缓存的开关与淘汰策略

<!-- 在 Mapper XML 顶部声明,作用域是这个 namespace -->
<cache eviction="LRU"
       flushInterval="60000"
       size="1024"
       readOnly="false"
       blocking="false"/>
属性含义可选值
eviction淘汰算法LRU(默认)、FIFO、SOFT、WEAK
flushInterval定时清空间隔(毫秒)正整数,不配则只在写操作时清
size最多缓存对象数默认 1024
readOnly是否只读true 直接返回缓存引用(快,但会被调用方改坏);false 序列化后返回副本
blocking缓存未命中时是否加锁默认 false,true 可防止缓存击穿

readOnly="true" 的坑很隐蔽:返回的是同一个对象引用,Service 层改了下游会直接读脏值。需要可修改就用 false,但要求实体实现 Serializable。

4. 为什么二级缓存经常「看起来不生效」

二级缓存挂在 namespace 上,而写操作只会清空当前 namespace 的缓存。多表联查时,A 表的数据被 B 表的 Mapper 改了,A 的缓存不会失效。

// UserMapper.xml 里做了关联查询,缓存了 user + dept
// DeptMapper.updateName() 改了部门名,UserMapper 的缓存依然是旧值

解决办法只有两个:<cache-ref namespace="..."/> 让两个 Mapper 共用一个缓存,或干脆不用二级缓存、交给 Redis。生产上更常见的做法是:关闭二级缓存(mybatis.configuration.cache-enabled: false),把跨请求缓存交给业务层的 Redis,避免缓存粒度与事务边界错位。

5. 配置示例

mybatis:
  configuration:
    cache-enabled: true          # 全局开关,false 时 <cache/> 也不生效
    local-cache-scope: session    # 一级缓存范围:session(默认)/ statement
    default-executor-type: simple

local-cache-scope: statement 常被用来「关掉一级缓存」:每次查询结束就清空本地缓存,适合对实时性要求高、又不想改代码的场景。代价是同一事务内重复查询也要回库。

6. 常见追问

追问一:一级缓存能关掉吗? 没有直接的开关,只有 localCacheScope=STATEMENT 这一种「查询完立即清空」的近似关闭方式。所以严格来说默认开启、不可移除。

追问二:为什么 Spring 下感觉一级缓存没起作用? 因为 Spring 每次查询都会取新的 SqlSession,除非你处在同一个 @Transactional 方法里。这也是一级缓存「看起来不存在」的最常见原因。

追问三:二级缓存和 Redis 缓存冲突吗? 冲突。两者都缓存查询结果,失效时机不同,容易出现「Redis 已更新、MyBatis 二级缓存还是旧值」的双缓存不一致。选一个即可。

追问四:flushCache 和 useCache 有什么区别? useCache="false" 表示这次查询不读也不写缓存;flushCache="true" 表示执行前清空缓存。写语句默认 flushCache="true"。

追问五:二级缓存能解决 N+1 吗? 不能。N+1 是关联查询次数问题,缓存只能缓解重复查询,第一次仍然要发 N 次 SQL。解决 N+1 要靠 fetchType、<collection> 的嵌套查询合并或手写 join。

小结

一级缓存是「同一个 SqlSession 内的查询备忘录」,二级缓存是「同一个 namespace 内的跨会话缓存」。它们省的是重复 SQL,不是分布式缓存;真正需要跨服务、跨请求的缓存,应该交给 Redis,并用业务键控制失效,而不是指望 MyBatis 的缓存边界。

笔记加载中…