MyBatis 一级缓存与二级缓存的源码级深度分析

下面是对 MyBatis 一级缓存与二级缓存的源码级深度分析,涵盖其设计原理、核心类、执行流程、生命周期以及常见问题。通过本篇分析,你将彻底理解 MyBatis 缓存机制的本质。


🧩 一、MyBatis 缓存概览

缓存类型作用范围默认状态数据结构线程安全
一级缓存(Local Cache)SqlSession 级别开启HashMap否(每个 SqlSession 独享)
二级缓存(Global Cache)namespace 级别(Mapper 接口)关闭(需手动开启)ConcurrentHashMap + 装饰器

缓存目标:避免重复 SQL 查询,提升性能。


🔍 二、一级缓存(Local Cache)源码分析

1. 核心类与调用链

// 核心入口
org.apache.ibatis.executor.BaseExecutor
  └── protected PerpetualCache localCache; // 一级缓存实例
  └── query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)

2. 执行流程(关键步骤)

ExecutorLocalCacheDatabasecreateCacheKey() → 基于 SQL、参数、分页等生成唯一键query(cacheKey)返回缓存结果执行 queryFromDatabase()返回 ResultSet将结果 put 进缓存返回结果alt[缓存命中][缓存未命中]ExecutorLocalCacheDatabase

3. 缓存 Key 生成:CacheKey

  • CacheKey 是一个复合键,包含:
    • MappedStatement.id(如 UserMapper.selectById
    • environment.id(环境标识)
    • SQL 语句(带 ? 占位符)
    • 参数值(parameterObject
    • 分页信息(RowBounds
    • 其他配置

💡 即使 SQL 相同,参数不同也会生成不同 CacheKey

4. 缓存失效时机

一级缓存会在以下操作后自动清空

操作源码位置说明
insert/update/deleteBaseExecutor.clearLocalCache()写操作后清除,避免脏读
sqlSession.clearCache()手动调用清空当前会话缓存
sqlSession.close()关闭会话缓存生命周期结束

📌 源码位置:BaseExecutor 中的 update()commit()rollback() 均会触发清空。

5. 源码关键点

// org.apache.ibatis.executor.BaseExecutor
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter);
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> query(...) {
  // 1. 先查本地缓存
  list = localCache.getObject(key);
  if (list == null) {
    // 2. 缓存未命中,查数据库
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }
  return list;
}

private <E> List<E> queryFromDatabase(...) {
  localCache.putObject(key, EXECUTION_PLACEHOLDER); // 占位符防止重复查询
  try {
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    localCache.removeObject(key); // 移除占位符
  }
  localCache.putObject(key, list); // 放入真实结果
  localCache.putObject(queryStack, key); // 支持嵌套查询
}

🌐 三、二级缓存(Second Level Cache)源码分析

1. 核心类与架构

// 核心组件
org.apache.ibatis.session.Configuration
  └── protected final Map<String, Cache> caches; // 所有 namespace 的缓存

org.apache.ibatis.cache.Cache
  └── 实现类:PerpetualCache(基础缓存)
  └── 装饰器模式:LruCache, ScheduledCache, SerializedCache, SynchronizedCacheorg.apache.ibatis.executor.CachingExecutor // 包装原生 Executor

2. 开启方式

<!-- 在 Mapper XML 中开启 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>

或使用注解:

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

3. 执行流程

CachingExecutorTransactionalCacheManagerSecondaryCacheDatabaseBaseExecutorgetObject(cacheKey)返回结果delegate.query(...) → 查询一级缓存或数据库返回结果putObject(cache, key, result)结果暂存事务缓存,等待提交alt[缓存命中 & 事务提交][缓存未命中]commit()commit() → 将事务缓存写入二级缓存CachingExecutorTransactionalCacheManagerSecondaryCacheDatabaseBaseExecutor

4. 关键机制:TransactionalCacheManager

  • 二级缓存不是直接写入,而是通过 TransactionalCache 包装。
  • 在事务未提交前,写入的是“临时缓存”。
  • 提交时才真正写入二级缓存,避免脏读。
// org.apache.ibatis.executor.CachingExecutor
public void commit(boolean required) throws SQLException {
  delegate.commit(required);
  tcm.commit(); // 将事务缓存 flush 到二级缓存
}

5. 缓存装饰器(Decorator 模式)

MyBatis 使用装饰器模式增强缓存功能:

装饰器作用
LruCache最近最少使用淘汰策略
FifoCache先进先出
ScheduledCache定时刷新
SerializedCache序列化存储(跨 JVM)
SynchronizedCache线程安全包装

配置示例:

<cache type="com.example.CustomCache">
  <property name="cacheSize" value="1024"/>
  <cache type="org.apache.ibatis.cache.decorators.LruCache"/>
  <cache type="org.apache.ibatis.cache.decorators.ScheduledCache">
    <property name="interval" value="5000"/>
  </cache>
</cache>

6. 源码关键点

// org.apache.ibatis.executor.CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) {
  // 获取缓存(由 Configuration 初始化时注册)
  Cache cache = ms.getCache();
  if (cache != null) {
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds);
    // 先查二级缓存
    Object value = cache.getObject(key);
    if (value == null) {
      // 未命中,走原生执行器(会查一级缓存或数据库)
      delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      // 结果会被 TransactionalCacheManager 缓存,提交时写入二级缓存
    }
    return (List<E>) value;
  }
  return delegate.query(...);
}

⚠️ 四、一级缓存 vs 二级缓存对比

特性一级缓存二级缓存
作用域SqlSessionnamespace(Mapper 接口)
默认开启否(需 <cache> 标签)
线程安全是(使用 SynchronizedCache 装饰)
跨会话共享
缓存失效写操作、close、clear写操作、flushInterval、size 满
事务支持会话内有效支持事务提交后写入(TransactionalCache
适用场景单次请求内重复查询跨请求的热点数据(如字典表)

🧠 五、常见问题与坑点

❌ 问题 1:二级缓存导致脏读

  • 原因:多个 SqlSession 共享缓存,但事务未隔离。
  • 解决方案
    • 设置 readOnly="false" 并实现 Serializable,避免共享引用。
    • 高并发下建议使用 Redis 替代二级缓存。

❌ 问题 2:缓存未命中,SQL 多次执行

  • 原因:不同 SqlSession 不共享一级缓存。
  • 验证
    SqlSession s1 = factory.openSession();
    s1.getMapper(UserMapper.class).selectById(1); // 执行 SQL
    s1.close();
    
    SqlSession s2 = factory.openSession();
    s2.getMapper(UserMapper.class).selectById(1); // 再次执行 SQL(一级缓存不跨会话)
    

❌ 问题 3:<cache> 配置不生效

  • 原因
    • Mapper 接口没有对应 XML 或注解未启用。
    • SqlSessionFactory 未刷新 Configuration
  • 解决:检查 XML 中是否有 <cache>,或使用 @CacheNamespace

✅ 六、最佳实践建议

  1. 一级缓存:合理使用,避免长会话导致内存泄漏。
  2. 二级缓存
    • 仅用于读多写少、数据一致性要求不高的场景(如配置表、字典)。
    • 避免在高并发写场景使用。
    • 推荐用 Redis + MyBatis 二级缓存接口 替代默认缓存。
  3. 缓存粒度:按 namespace 控制,避免缓存污染。
  4. 序列化:若启用二级缓存,POJO 必须实现 Serializable

📚 总结:MyBatis 缓存本质

维度说明
一级缓存本质SqlSession 内的 HashMap,防止同会话重复查询
二级缓存本质namespace 级的共享缓存,基于装饰器模式 + 事务缓存
设计思想简单有效,不追求复杂分布式缓存,留出扩展空间
源码启示学习了 CacheKey 的设计、装饰器模式、事务缓存管理
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值