1. 为什么需要整合MyBatis与EHCache
在数据密集型应用中,数据库访问往往是性能瓶颈的主要来源。我们团队在电商促销系统开发中就深有体会——当秒杀活动开始时,商品详情查询的QPS瞬间从200飙升到8000+,直接导致数据库连接池耗尽。这时候,引入缓存层就成了救命稻草。
MyBatis作为优秀的ORM框架,虽然提供了一级缓存(SqlSession级别)和二级缓存(Mapper级别)机制,但内置的缓存实现存在明显局限:
- 内存管理简单粗暴,容易OOM
- 缺乏灵活的过期策略
- 不支持分布式环境
- 监控功能几乎为零
EHCache作为老牌Java缓存框架,恰好能弥补这些不足。它支持:
- 内存+磁盘的多级存储
- LRU/LFU/FIFO等多种淘汰算法
- 细粒度的TTL设置
- 完善的JMX监控
2. 整合方案设计与技术选型
2.1 整体架构设计
我们采用的缓存架构分为三层:
- MyBatis一级缓存:会话级缓存,默认开启
- MyBatis二级缓存:通过EHCache实现
- 应用层缓存:使用Spring Cache + EHCache
// 典型调用链路 @Transactional public Product getProduct(Long id) { // 先查二级缓存(EHCache) // 未命中则查询数据库 // 结果存入二级缓存 return productMapper.selectById(id); }2.2 版本兼容性验证
经过实际测试,以下版本组合最稳定:
- MyBatis 3.5.6+
- mybatis-ehcache 1.2.1
- ehcache 2.10.6(注意3.x版本API变化较大)
重要提示:Spring Boot项目需排除自带的ehcache3依赖,否则会出现ClassLoader冲突
3. 详细整合步骤
3.1 基础环境搭建
首先添加Maven依赖:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.1</version> </dependency> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> <version>2.10.6</version> </dependency>3.2 EHCache配置文件
在resources目录下创建ehcache.xml:
<ehcache> <diskStore path="java.io.tmpdir/ehcache"/> <defaultCache maxEntriesLocalHeap="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" diskSpoolBufferSizeMB="30" maxEntriesLocalDisk="100000" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> <cache name="productCache" maxEntriesLocalHeap="5000" eternal="true" overflowToDisk="true"/> </ehcache>关键参数说明:
- timeToIdleSeconds:最大闲置时间
- timeToLiveSeconds:最大存活时间
- overflowToDisk:内存不足时是否溢出到磁盘
3.3 MyBatis配置调整
在mybatis-config.xml中启用二级缓存:
<settings> <setting name="cacheEnabled" value="true"/> </settings>在Mapper接口上添加注解:
@CacheNamespace(implementation = org.mybatis.caches.ehcache.EhcacheCache.class) public interface ProductMapper { @Select("SELECT * FROM product WHERE id=#{id}") Product selectById(Long id); }4. 高级优化技巧
4.1 缓存预热策略
我们开发了定时任务在系统启动时预热热点数据:
@Scheduled(cron = "0 0 3 * * ?") public void preloadHotProducts() { List<Long> hotIds = getHotProductIds(); hotIds.forEach(id -> productMapper.selectById(id)); }4.2 缓存雪崩防护
通过随机TTL避免集体失效:
<cache name="productCache" timeToLiveSeconds="#{T(java.util.concurrent.ThreadLocalRandom).current().nextInt(300,600)}" ... />4.3 监控配置
在Spring中暴露JMX监控:
@Bean public MBeanServer mBeanServer() { MBeanServerFactoryBean factory = new MBeanServerFactoryBean(); factory.setLocateExistingServerIfPossible(true); return factory.getObject(); } @Bean public ManagementService managementService() { ManagementService service = new ManagementService(cacheManager(), mBeanServer(), true, true, true, true); service.init(); return service; }5. 生产环境踩坑记录
5.1 序列化问题
我们发现当缓存对象实现Serializable接口时,如果修改了类结构会导致反序列化失败。解决方案:
- 添加serialVersionUID
- 或改用JSON序列化方式
5.2 脏读问题
在分布式环境下,我们遇到过节点间缓存不一致的情况。最终采用两种方案:
- 为缓存key添加版本号
- 通过Redis Pub/Sub实现缓存失效通知
5.3 性能调优
通过JProfiler分析发现,默认配置下频繁的磁盘操作导致性能下降。优化方案:
- 增大diskSpoolBufferSizeMB到100MB
- 设置diskExpiryThreadIntervalSeconds=3600(减少磁盘扫描频率)
6. 效果验证与监控
我们通过Grafana搭建了监控看板,关键指标包括:
- 缓存命中率(稳定在92%以上)
- 平均响应时间(从120ms降至28ms)
- GC频率(Full GC从每天3次降至每周1次)
压测数据显示,在10,000 QPS下:
- 纯DB方案:平均RT 150ms,错误率8%
- 缓存方案:平均RT 35ms,错误率0.1%