1. MyBatis缓存机制的基本架构
MyBatis作为Java生态中最流行的ORM框架之一,其缓存设计直接影响着应用性能表现。理解缓存查询顺序的前提是掌握MyBatis缓存的基本结构。MyBatis采用二级缓存架构:一级缓存(Local Cache)和二级缓存(Second Level Cache)。
一级缓存是SqlSession级别的缓存,默认开启且无法关闭。当同一个SqlSession执行相同的SQL时,MyBatis会直接从内存返回结果而非查询数据库。我在实际项目中发现,这个设计对事务内的重复查询特别有效,比如在循环中反复获取同一实体时性能提升明显。
二级缓存是Mapper级别的缓存,多个SqlSession可以共享。需要手动在mapper.xml中配置<cache/>标签开启。二级缓存的实际存储可以通过实现Cache接口来扩展,常见的有:
- 默认的PerpetualCache(基于HashMap)
- 集成Redis等分布式缓存
- 使用Ehcache等第三方缓存库
重要提示:二级缓存的作用域是整个Mapper namespace,这意味着不同SqlSession通过相同Mapper执行查询时会共享缓存。这也带来了数据一致性问题,需要特别注意缓存更新策略。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 查询请求的完整处理流程
当执行Mapper接口方法时,MyBatis的查询操作会经历以下完整链路:
2.1 请求入口处理
调用MapperProxy.invoke()方法后,会创建MappedStatement对象,其中包含SQL定义、参数映射等信息。这里有个关键点:框架会先检查该方法是否被@CacheNamespaceRef等注解标记,这会影响后续的缓存行为。
2.2 参数绑定与SQL构建
通过ParameterHandler处理参数后,SqlSource会生成最终的SQL语句。此时如果启用了缓存,会先计算缓存key。缓存key的构成包括:
- MappedStatement的id
- 分页参数
- 实际查询参数值
- 环境ID等
我在排查一个缓存命中异常问题时发现,当使用Map作为参数时,由于Map的toString()方法输出顺序不固定,可能导致相同的查询生成不同的缓存key。解决方案是改用@Param注解明确参数名。
2.3 执行器选择与拦截
经过插件拦截链后,请求到达Executor实现类。根据配置不同可能是:
- SimpleExecutor(默认)
- ReuseExecutor(重用Statement)
- BatchExecutor(批处理)
- CachingExecutor(带二级缓存)
这里有个性能优化点:如果确定不需要二级缓存,可以直接使用BaseExecutor的子类而非CachingExecutor包装,减少缓存检查开销。
3. 缓存查询的优先级与判断逻辑
3.1 一级缓存优先原则
MyBatis总是先检查一级缓存,查询顺序如下:
- 根据statementId、参数等生成cacheKey
- 查询BaseExecutor中的localCache(HashMap实现)
- 如果命中且未过期,直接返回结果
- 未命中则继续后续流程
一级缓存的失效场景包括:
- 执行了insert/update/delete操作
- 调用了SqlSession.clearCache()
- 设置了Statement级别的flushCache=true
- 提交或回滚事务
3.2 二级缓存查询条件
当一级缓存未命中时,如果配置了二级缓存:
- 检查CachingExecutor.delegate是否允许使用缓存
- 验证MappedStatement的useCache配置
- 检查本次查询的flushCacheRequired标记
- 最终通过TransactionalCacheManager查询缓存
特别注意:二级缓存是否生效还取决于:
- 缓存实现是否支持序列化(分布式场景)
- 是否配置了正确的cache-ref
- 是否有其他SqlSession执行了更新操作
3.3 缓存穿透防护
当两级缓存都未命中时,才会真正查询数据库。为防止缓存穿透,可以采用以下策略:
xml复制<cache eviction="LRU" flushInterval="60000"
size="1024" readOnly="true"/>
其中:
- eviction:淘汰策略(LRU/FIFO等)
- flushInterval:强制刷新间隔
- size:缓存对象数量
- readOnly:是否只读(性能更好)
4. 典型场景下的缓存行为分析
4.1 事务内的重复查询
java复制try(SqlSession session = sqlSessionFactory.openSession()){
UserMapper mapper = session.getMapper(UserMapper.class);
User user1 = mapper.selectById(1); // 查数据库
User user2 = mapper.selectById(1); // 命中一级缓存
session.commit(); // 一级缓存失效
}
这个案例展示了事务内缓存的有效性。但要注意:如果中途执行了更新操作,会清空一级缓存。
4.2 跨SqlSession的缓存共享
java复制try(SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession()){
User user1 = session1.getMapper(UserMapper.class).selectById(1); // 查DB并缓存
User user2 = session2.getMapper(UserMapper.class).selectById(1); // 命中二级缓存
}
二级缓存的共享特性可能导致脏读问题。建议对频繁变更的数据关闭二级缓存,或设置较短的flushInterval。
4.3 批量操作与缓存一致性
当执行批量更新时,MyBatis的缓存清除是同步进行的。但在分布式环境中,需要考虑:
- 使用集中式缓存(如Redis)
- 实现Cache接口时处理并发问题
- 对关键数据设置较短的过期时间
5. 缓存调优实战经验
5.1 监控缓存命中率
通过实现Cache接口并添加统计逻辑,可以监控缓存效果:
java复制public class MonitoringCache implements Cache {
private final Cache delegate;
private AtomicLong hits = new AtomicLong();
private AtomicLong misses = new AtomicLong();
// 实现接口方法时记录命中/未命中
public Object getObject(Object key) {
Object value = delegate.getObject(key);
if(value != null) hits.incrementAndGet();
else misses.incrementAndGet();
return value;
}
}
5.2 合理设置缓存粒度
过度缓存会导致内存浪费,建议:
- 对静态数据(如省份列表)使用全局缓存
- 对用户私有数据使用带用户ID的细粒度缓存
- 对聚合查询结果实现自定义缓存策略
5.3 避免缓存雪崩
当大量缓存同时失效时,可能导致数据库压力激增。解决方案包括:
- 设置不同的过期时间(基础值+随机偏移)
- 实现缓存预热机制
- 使用互斥锁控制重建缓存的并发度
6. 常见问题排查指南
6.1 缓存未生效检查清单
- 确认mybatis-config.xml中cacheEnabled=true(默认true)
- 检查mapper.xml是否有
声明 - 验证SQL的statementId是否在正确的namespace下
- 检查参数是否影响cacheKey生成
- 确认没有执行过会清空缓存的操作
6.2 脏读问题处理
当发现从缓存获取到过期数据时:
- 检查更新操作是否调用了clearCache()
- 确认TransactionalCache的提交行为
- 在集群环境中检查缓存同步机制
6.3 性能问题定位
如果怀疑缓存导致性能下降:
- 使用Arthas监控缓存操作耗时
- 检查缓存实现的内存使用情况
- 评估缓存命中率与预期是否匹配
我在处理一个性能问题时发现,由于缓存value过大(超过10MB的查询结果),导致反序列化耗时增加。最终通过拆分查询和限制单次结果集大小解决了问题。
