1. MyBatis缓存机制深度解析
作为Java生态中最受欢迎的ORM框架之一,MyBatis的缓存设计直接影响着应用性能表现。在实际项目中,我们经常遇到这样的场景:当查询压力增大时,数据库连接池很快成为瓶颈,而合理的缓存配置往往能带来数倍的性能提升。
MyBatis采用二级缓存架构设计,这与Hibernate等ORM框架有明显区别。一级缓存(本地缓存)默认开启,作用域为SqlSession级别,在同一个会话中执行的相同SQL会直接返回缓存结果。我曾在一个用户订单查询接口中实测过,启用一级缓存后,重复查询效率提升了近8倍。但要注意的是,在分布式环境下,一级缓存会因为会话隔离而失效。
二级缓存(全局缓存)需要显式配置,其作用域是Mapper级别,多个SqlSession可以共享缓存数据。通过分析MyBatis源码可以发现,二级缓存默认采用基于Map的内存存储,当缓存命中时,查询结果会跳过SQL解析、参数绑定、JDBC通信等耗时操作。但这也带来了数据一致性问题——我在电商项目中就遇到过缓存未及时更新导致商品库存显示错误的案例。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 缓存优化实战方案
2.1 缓存策略选型
对于读多写少的场景,比如商品详情页,推荐使用FIFO策略:
xml复制<cache eviction="FIFO" flushInterval="60000" size="512"/>
这里的flushInterval设置为60秒,意味着即使没有写操作,缓存也会自动刷新,避免展示过期数据。size参数需要根据业务特点调整,过小会导致频繁淘汰,过大会增加GC压力。
对于配置类数据,可以采用更激进的LRU策略:
java复制@CacheNamespace(implementation=MybatisRedisCache.class,
eviction=MybatisRedisCache.class,
size=1024)
public interface ConfigMapper {}
这个案例中我们结合了自定义的Redis缓存实现,将配置信息缓存到分布式缓存中,size设置为1024条记录,经测试可将QPS从200提升到1500+。
2.2 缓存击穿防护
在高并发场景下,缓存击穿是常见问题。我的解决方案是采用双重检查锁:
java复制public Product getProductById(Long id) {
Product product = cache.get(id);
if(product == null) {
synchronized(this) {
product = cache.get(id);
if(product == null) {
product = mapper.selectById(id);
cache.put(id, product);
}
}
}
return product;
}
在秒杀系统中,这种方案将缓存未命中时的数据库查询降低了90%。注意要在synchronized块内再次检查缓存,避免重复查询。
3. 高级缓存技巧
3.1 批量操作优化
MyBatis的批量插入默认不会刷新缓存,这可能导致查询获取到部分数据。解决方法是在批量操作后手动清除缓存:
java复制try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
ProductMapper mapper = session.getMapper(ProductMapper.class);
for(Product product : products) {
mapper.insert(product);
}
session.commit();
// 手动清除关联缓存
cache.clear();
}
在导入10万条数据的测试中,这种方案比自动刷新缓存快了近3倍。
3.2 多级缓存架构
对于千万级数据量的系统,我推荐使用多级缓存方案:
- 一级缓存:SqlSession级别,存储热点数据
- 二级缓存:Redis集群,存储全量数据
- 本地缓存:Caffeine,存储高频访问数据
实现时需要特别注意缓存一致性问题。我的做法是通过Redis的Pub/Sub机制通知各节点更新本地缓存:
java复制// 缓存更新监听器
public class CacheUpdateListener implements MessageListener {
@Override
public void onMessage(Message message) {
String key = message.toString();
localCache.invalidate(key);
}
}
在某金融项目中,这种架构将平均响应时间从120ms降到了35ms。
4. 性能调优实战
4.1 缓存命中率监控
通过实现MyBatis的Cache接口,我们可以收集关键指标:
java复制public class MonitorCache implements Cache {
private final Cache delegate;
private AtomicLong hits = new AtomicLong();
private AtomicLong misses = new AtomicLong();
@Override
public Object getObject(Object key) {
Object value = delegate.getObject(key);
if(value != null) {
hits.incrementAndGet();
} else {
misses.incrementAndGet();
}
return value;
}
public double getHitRate() {
return (double)hits.get()/(hits.get()+misses.get());
}
}
建议将命中率维持在80%以上,过低说明缓存策略需要调整。我曾通过这个监控发现某个查询缓存命中率只有15%,原因是参数组合太多,后来改为缓存固定条件查询结果,命中率提升到75%。
4.2 缓存预热策略
对于系统启动时的冷启动问题,我的解决方案是:
- 定义预热SQL列表
- 系统启动时异步执行
- 结果存入缓存
java复制@PostConstruct
public void preheatCache() {
executor.execute(() -> {
List<Long> hotProductIds = getHotProductIds();
hotProductIds.forEach(id -> {
productMapper.getById(id); // 触发缓存
});
});
}
在618大促前,通过这种方式我们成功将核心接口的RT从初始的800ms稳定到了120ms左右。
5. 常见问题排查
5.1 缓存穿透案例
现象:某个查询接口突然响应变慢,数据库CPU飙升
排查过程:
- 检查日志发现大量缓存未命中
- 分析参数发现有人频繁请求不存在的ID
- 解决方案:增加布隆过滤器
java复制public Product getProductWithBloomFilter(Long id) {
if(!bloomFilter.mightContain(id)) {
return null;
}
return getProductById(id);
}
5.2 事务与缓存冲突
现象:开启事务后查询不到最新数据
原因分析:MyBatis一级缓存不会读取其他事务的未提交数据
解决方案:
java复制@Transactional
public void updateProduct(Product product) {
productMapper.update(product);
// 强制清除当前会话缓存
sqlSession.clearCache();
}
在库存扣减场景中,这个技巧帮助我们避免了多个事务间的数据可见性问题。记住要在写操作后立即清空缓存,而不是等待事务提交。
