Skip to content

MyBatis缓存机制

MyBatis 缓存分为一级缓存和二级缓存。缓存可以减少数据库访问,但也可能带来数据一致性问题。

为什么 MyBatis 要有缓存

同一个业务请求中,可能会多次查询同一条记录。如果每次都访问数据库,会产生重复 IO。一级缓存的目的就是在同一个 SqlSession 中复用查询结果。

但缓存不是免费的:缓存命中可以减少查询,缓存失效不及时就会返回旧数据。学习 MyBatis 缓存时要同时理解收益和一致性风险。

缓存层级

mermaid
flowchart TD
    A[查询请求] --> B{一级缓存是否命中?}
    B -->|是| C[返回结果]
    B -->|否| D{二级缓存是否命中?}
    D -->|是| E[写入一级缓存并返回]
    D -->|否| F[查询数据库]
    F --> G[写入缓存]
    G --> C

一级缓存

一级缓存是 SqlSession 级别,默认开启。

同一个 SqlSession 中,执行相同 SQL 和相同参数时,可能命中一级缓存。

一级缓存会在以下情况清空:

  • 执行 insert/update/delete。
  • commit。
  • rollback。
  • close。

二级缓存

二级缓存是 Mapper namespace 级别,需要配置开启。

xml
<cache/>

多个 SqlSession 可以共享同一个 namespace 下的二级缓存。

缓存Key

MyBatis 缓存 key 通常和以下信息有关:

  • MappedStatement ID。
  • SQL 文本。
  • 参数。
  • 分页信息。
  • 环境信息。

只要这些信息不同,就不是同一条缓存记录。

工作原理

一级缓存绑定 SqlSession,查询时先根据缓存 Key 查本地缓存;未命中才访问数据库。执行写操作、提交、回滚或关闭会清空一级缓存。

mermaid
flowchart TD
    A["同一 SqlSession 查询"] --> B["生成 CacheKey"]
    B --> C{"一级缓存是否命中"}
    C -- "是" --> D["返回缓存对象"]
    C -- "否" --> E["查询数据库"]
    E --> F["写入一级缓存"]

二级缓存绑定 Mapper namespace,作用范围更大,因此一致性风险也更大。分布式应用中,不同 JVM 的本地二级缓存无法自动互相通知,通常不建议依赖它缓存强一致业务数据。

一致性风险

二级缓存最大的问题是数据一致性。

例如:

  • UserMapper 查询用户。
  • OrderMapper 更新了用户相关统计。
  • UserMapper 的二级缓存未必知道 OrderMapper 的变更。

这会导致查询到旧数据。

适合使用缓存的场景

  • 字典表。
  • 配置表。
  • 很少变化的数据。
  • 读多写少且一致性要求不高的数据。

不适合:

  • 订单、库存、余额。
  • 高频更新数据。
  • 多表关联变化复杂的数据。

开发建议

  1. 一级缓存了解即可,默认使用。
  2. 二级缓存谨慎开启。
  3. 分布式项目不要轻易依赖 MyBatis 本地二级缓存。
  4. 高频业务缓存更建议使用 Redis,并设计失效策略。
  5. 对强一致数据,优先查数据库。

Demo:一级缓存命中

java
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user1 = mapper.selectById(1L);
    User user2 = mapper.selectById(1L);
    // 同一个 SqlSession、同一条 SQL、同样参数,第二次可能命中一级缓存。
}

这个 Demo 只适合理解机制。真实业务不要为了命中 MyBatis 缓存而扩大 SqlSession 或事务范围,否则会带来连接占用和一致性风险。

一级缓存完整生命周期

一级缓存是 SqlSession 级别,也叫本地缓存。它存在的时间和 SqlSession 生命周期一致。

mermaid
flowchart TD
    A["打开 SqlSession"] --> B["第一次查询"]
    B --> C["生成 CacheKey"]
    C --> D["查询数据库"]
    D --> E["结果放入一级缓存"]
    E --> F["第二次相同查询"]
    F --> G["命中一级缓存"]
    G --> H{"发生 insert/update/delete/commit/rollback/close"}
    H -- "是" --> I["清空一级缓存"]

一级缓存命中的前提通常包括:

  1. 同一个 SqlSession
  2. 同一个 MappedStatement
  3. 最终 SQL 相同。
  4. 参数相同。
  5. 分页信息相同。
  6. 环境一致。

为什么一级缓存可能读到旧数据

java
AssetDO a1 = assetMapper.selectById(1L);
otherMapper.updateAssetName(1L, "new-name");
AssetDO a2 = assetMapper.selectById(1L);

如果两个操作不在同一条 MyBatis 更新链路里,第二次查询可能仍然命中本地缓存。实际项目中 MyBatis 执行写操作会清本地缓存,但跨会话、跨线程、跨系统更新时,一级缓存并不知道外部变化。

所以一级缓存只能理解为 SqlSession 内部优化,不要当业务缓存。

Spring 项目里的一级缓存

Spring + MyBatis 常用 SqlSessionTemplate。开发者注入 Mapper,并不会手动持有 SqlSession

mermaid
flowchart TD
    A["Service 调用 Mapper"] --> B["SqlSessionTemplate"]
    B --> C{"当前线程是否有事务"}
    C -- "有" --> D["复用事务绑定 SqlSession/Connection"]
    C -- "无" --> E["创建会话执行后释放"]

这意味着:

  1. 没有事务时,一次 Mapper 调用可能用完就关闭会话,一级缓存范围很小。
  2. 有事务时,同一个事务中的多次 Mapper 调用可能共享会话和一级缓存。
  3. 不要为了缓存而故意扩大事务范围。

事务范围越大,连接占用越久,锁持有越久,失败回滚成本越高。

二级缓存详细流程

二级缓存不是查询后立刻对所有会话可见。通常要等 SqlSession 提交或关闭后,结果才会进入二级缓存。

mermaid
flowchart TD
    A["SqlSession1 查询数据库"] --> B["结果先进入本地会话"]
    B --> C["SqlSession1 commit/close"]
    C --> D["写入 namespace 二级缓存"]
    E["SqlSession2 查询同一 namespace"] --> F{"二级缓存命中"}
    F -- "是" --> G["返回缓存并写入一级缓存"]
    F -- "否" --> H["查询数据库"]

为什么要提交后才进二级缓存?因为事务未提交前的数据不应该被其他会话看到。

二级缓存的一致性边界

二级缓存的作用域是 Mapper namespace。它只知道本 namespace 的写操作,不一定知道其他 Mapper 对相关表的修改。

mermaid
flowchart TD
    A["AssetMapper 查询资产列表<br/>缓存资产+科室名称"] --> B["AssetMapper 二级缓存"]
    C["DeptMapper 修改科室名称"] --> D["DeptMapper 清自己的缓存"]
    D --> E["AssetMapper 缓存不知道科室变了"]
    E --> F["再次查资产列表可能读旧科室名称"]

这个例子说明:多表关联查询最不适合随便放进 MyBatis 二级缓存。

分布式部署风险

如果二级缓存使用 JVM 本地内存:

mermaid
flowchart TD
    A["服务节点 A"] --> B["本地二级缓存 A"]
    C["服务节点 B"] --> D["本地二级缓存 B"]
    A --> E["更新数据库"]
    E --> F["只清理节点 A 相关缓存"]
    D --> G["节点 B 可能仍有旧缓存"]

分布式系统中,一个节点清了本地缓存,不代表其他节点都清了。除非接入统一缓存和失效通知,否则一致性边界很难控制。

为什么字典表也要谨慎

字典表、配置表、地区表看起来适合缓存,但仍要明确:

  1. 谁会修改。
  2. 修改后多久生效。
  3. 是否允许短时间旧值。
  4. 是否有后台刷新或发布机制。
  5. 多节点是否一致。

如果业务要求“配置改完立刻所有节点生效”,MyBatis 二级缓存就不是好选择,更适合配置中心或 Redis + 消息通知。

Redis 和 MyBatis 缓存怎么选

维度MyBatis 一级缓存MyBatis 二级缓存Redis
作用范围单个 SqlSessionMapper namespace跨服务共享
默认状态默认开启需开启独立设计
一致性控制会话内namespace 粒度应用自己控制
分布式能力本地实现通常无
适合场景减少会话内重复查询少量低频稳定数据业务缓存、热点数据

结论:

  1. 一级缓存理解机制,不做业务设计依赖。
  2. 二级缓存谨慎开启。
  3. 真正业务缓存优先显式设计 Redis 缓存。
  4. 强一致数据优先查库。

商业场景:资产状态为什么不适合二级缓存

资产状态可能被采集任务、审核流程、人工编辑、批处理任务修改。如果打开 AssetMapper 二级缓存:

  1. 用户 A 查资产详情,进入缓存。
  2. 采集任务更新资产状态。
  3. 审核任务更新资产科室。
  4. 另一个服务节点继续从本地缓存读旧状态。

结果就是页面看到的状态落后,甚至审批基于旧状态做判断。

这类数据应该:

  1. 查询数据库。
  2. 或使用 Redis,但在状态变更后明确删除缓存。
  3. 消费端和更新端都要做幂等和版本判断。

常见配置项

xml
<settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="localCacheScope" value="SESSION"/>
</settings>
配置说明
cacheEnabled是否全局启用二级缓存能力
localCacheScope=SESSION一级缓存作用于整个 SqlSession
localCacheScope=STATEMENT每条语句后清本地缓存,减少旧数据风险

如果业务非常担心同一事务内重复读到旧对象,可以考虑 STATEMENT,但更常见的是从事务边界和数据更新路径上治理。

线上排查

现象可能原因排查
第二次查询没打 SQL一级缓存命中看是否同一事务/SqlSession
修改后读到旧数据二级缓存或业务缓存检查 Mapper <cache/>、Redis
多节点数据不一致本地缓存不共享检查部署节点和缓存实现
多表 join 结果旧namespace 缓存无法感知其他表更新禁用该查询缓存
内存上升缓存对象过多检查二级缓存大小和对象体积

面试标准回答

text
MyBatis 缓存分一级缓存和二级缓存。一级缓存是 SqlSession 级别,默认开启,同一个 SqlSession 中相同 SQL 和参数可能命中缓存,执行写操作、commit、rollback、close 会清空。二级缓存是 Mapper namespace 级别,需要显式开启,多个 SqlSession 可以共享,但一致性风险更大,尤其是多表关联、跨 Mapper 更新、分布式多节点场景。生产中通常不依赖 MyBatis 二级缓存做强一致业务缓存,性能问题优先从 SQL、索引、分页和 Redis 显式缓存方案解决。