MyBatis一级与二级缓存机制详解及性能优化实践

发布时间:2026/11/6 2:37:44
MyBatis一级与二级缓存机制详解及性能优化实践

1. MyBatis缓存机制全景解析

作为Java生态中最受欢迎的ORM框架之一,MyBatis的缓存设计直接影响着应用性能。很多开发者在面试中被问到"MyBatis一级缓存和二级缓存有什么区别"时,往往只能背出标准答案,却说不清实际项目中如何合理运用。今天我就结合自己五年来的实战经验,带大家深入理解这两个缓存的运作机制。

先看一个真实案例:某电商平台的商品详情接口,在未启用二级缓存时QPS只能维持在200左右,而合理配置缓存后直接飙升到1500+。这充分说明理解MyBatis缓存对性能优化的重要性。接下来我们将从底层原理到实战配置,完整解析这两级缓存的工作机制。

2. 一级缓存深度剖析

2.1 默认开启的会话级缓存

一级缓存是SqlSession级别的缓存,默认开启且不能关闭。它的生命周期与SqlSession绑定,当调用close()方法时缓存就会被清空。我通过一个简单测试验证其效果:

try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); User user1 = mapper.selectById(1); // 第一次查询,访问数据库 User user2 = mapper.selectById(1); // 从一级缓存获取 System.out.println(user1 == user2); // 输出true,是同一个对象 }

注意:一级缓存存储的是对象引用而非值拷贝,这意味着修改user2会直接影响user1

2.2 失效场景与应对策略

一级缓存在以下情况会自动失效:

  • 执行了INSERT/UPDATE/DELETE操作
  • 调用了commit()或rollback()
  • 手动调用clearCache()

我曾遇到一个典型问题:在同一个事务中先查询后更新,期望下次查询能获取最新数据。解决方案是:

session.clearCache(); // 显式清空缓存 mapper.selectById(1); // 重新查询数据库

2.3 底层实现原理

一级缓存的实现类是PerpetualCache,本质就是一个HashMap:

public class PerpetualCache implements Cache { private final String id; private final Map<Object, Object> cache = new HashMap<>(); //...省略其他方法 }

缓存Key的生成规则包含:

  1. Mapper方法ID(如com.example.UserMapper.selectById)
  2. 分页参数(offset/limit)
  3. SQL语句
  4. 参数值
  5. 环境ID(多数据源时)

3. 二级缓存全面指南

3.1 跨会话共享的命名空间缓存

二级缓存是Mapper级别的缓存,多个SqlSession可以共享。需要满足三个条件才能生效:

  1. 在mybatis-config.xml中全局启用:
<settings> <setting name="cacheEnabled" value="true"/> </settings>
  1. 在Mapper.xml中声明使用:
<cache eviction="LRU" flushInterval="60000" size="512"/>
  1. 查询结果必须可序列化(实现Serializable接口)

3.2 配置参数详解

二级缓存支持多种策略配置:

  • eviction:淘汰策略(LRU/FIFO/SOFT/WEAK)
  • flushInterval:自动刷新间隔(毫秒)
  • size:缓存对象数量
  • readOnly:是否只读(性能优化关键)

建议生产环境配置示例:

<cache eviction="LRU" flushInterval="300000" size="1024" readOnly="true"/>

3.3 分布式环境下的挑战

当系统采用集群部署时,本地二级缓存会导致数据不一致。解决方案有:

  1. 使用集中式缓存(如Redis)
  2. 实现自定义Cache接口:
public class RedisCache implements Cache { // 实现所有接口方法... }

4. 两级缓存对比与最佳实践

4.1 核心差异对照表

特性一级缓存二级缓存
作用范围SqlSession内部跨SqlSession
存储位置JVM内存可配置(本地/远程)
默认状态开启需要显式配置
生命周期随SqlSession销毁随应用持续
适用场景事务内重复查询全局高频读低频写数据

4.2 性能优化黄金法则

根据我的项目经验,给出以下建议:

  1. 写多读少的场景建议关闭二级缓存
  2. 对实时性要求高的数据设置较短flushInterval
  3. 大对象考虑使用SOFT引用防止OOM
  4. 集群环境必须使用分布式缓存实现

典型问题案例:某金融系统查询账户余额时,由于缓存未及时刷新导致显示错误金额。最终解决方案是:

<cache flushInterval="1000"/> <!-- 1秒刷新 -->

5. 高级应用与疑难解答

5.1 缓存穿透防御方案

当查询不存在的数据时,会导致缓存失效。解决方案:

  1. 缓存空对象:
if (result == null) { cache.put(key, NULL_OBJECT); // 特殊标记 }
  1. 使用布隆过滤器预先校验

5.2 事务隔离级别的影响

在REPEATABLE_READ隔离级别下,可能出现"幻读"问题。此时需要:

@Transactional(isolation = Isolation.READ_COMMITTED) public void businessMethod() { // ...业务逻辑 }

5.3 监控与调优工具

推荐使用以下方式监控缓存命中率:

  1. 实现Cache接口的getHitRatio()方法
  2. 通过JMX暴露缓存统计信息
  3. 使用Arthas在线诊断

我在实际项目中总结的缓存命中率优化路径:

  1. 80%以下:检查缓存配置合理性
  2. 80%-90%:优化查询条件
  3. 90%以上:考虑缓存容量扩展

6. 实战中的避坑指南

  1. 对象关联陷阱:当查询结果包含关联对象时,二级缓存可能导致N+1查询问题。解决方案是在 或 上单独配置缓存:
<collection ... fetchType="lazy" useCache="false"/>
  1. 批量操作失效:批量插入时默认不会刷新缓存,需要手动处理:
sqlSession.commit(); // 触发缓存刷新
  1. 测试环境验证:建议在单元测试中加入缓存验证:
@Test public void testCacheHit() { // 第一次查询 mapper.selectById(1); // 验证第二次查询是否命中缓存 verify(cache, times(1)).getObject(any()); }
  1. Spring集成注意:当MyBatis与Spring集成时,事务管理器会影响缓存行为。建议明确指定事务传播属性:
@Transactional(propagation = Propagation.REQUIRES_NEW)

最后分享一个真实案例:某物流系统在高峰期出现缓存雪崩,原因是多个Mapper共用了同一个缓存实例。解决方案是为每个Mapper配置独立的缓存空间:

<cache-ref namespace="com.example.OtherMapper"/> <!-- 避免使用 -->