☰
MyBatis集成Ehcache实现高性能二级缓存实战
2026/10/17 4:19:38 网站建设 项目流程

1. 项目概述:为什么在MyBatis里“硬塞”一个Ehcache进去?

你有没有遇到过这样的场景:系统刚上线时响应飞快,用户点个列表秒出;可等数据量涨到百万级、并发请求一上来,数据库CPU直接飙到95%,慢SQL告警像闹钟一样叮咚响——查来查去,发现80%的查询都是重复读同一张字典表、同一个用户配置、同一批商品分类。这时候光靠加索引、调SQL已经杯水车薪,真正该动刀的地方,是把高频、低变、计算成本高的结果,从数据库里“搬出来”,就近存着,随取随用。

这就是缓存的价值,而MyBatis原生只提供一级缓存(SqlSession级,生命周期短、作用域窄)和二级缓存(Mapper级,但默认基于内存实现,不支持集群、无淘汰策略、无监控能力)。它就像给你配了一把能锁住门的钥匙,但没给配保险柜——东西放得近,可放不稳、放不久、放多了还容易挤爆。

Ehcache恰恰补上了这块短板:它是一个成熟、稳定、可嵌入的Java本地缓存库,支持LRU/LFU淘汰、堆外内存、磁盘溢出、缓存监听、JMX监控,更重要的是——它能通过标准的JSR-107(JCache)接口与MyBatis无缝对接。我们不是在“整合”两个工具,而是在给MyBatis装上一套可配置、可运维、可诊断的缓存神经系统。

这个项目标题里的每一个词都带着明确意图:“MyBatis”是执行层,“第三方缓存库”说明不依赖框架内置方案,“Ehcache”是具体选型——它不是唯一解,但却是最务实的选择:比Caffeine更重(适合需要持久化和集群协同的场景),比Redis轻(无需额外部署服务,零网络开销),比Guava Cache强(支持分布式缓存协同和企业级管理能力)。我带过的三个中型项目里,凡是有“读多写少+单机为主+需快速落地”的需求,Ehcache都是第一轮技术选型的胜出者。

如果你正卡在数据库压力瓶颈、想用最小改动撬动最大性能提升、又不想引入新中间件增加运维复杂度——那这篇内容就是为你写的。它不讲抽象理论,只拆真实配置、参数含义、踩坑现场和压测对比数据。接下来,我会带你从零开始,把Ehcache真正“焊”进MyBatis的执行链路里,而不是贴个标签就完事。

2. 整体设计思路与方案选型逻辑

2.1 为什么不是Redis?也不是Caffeine?更不是自己手写Map?

这是每次技术评审必被问的问题。答案不能只说“我觉得好”,得拿出可验证的决策依据。我们当时列了张横向对比表,覆盖6个核心维度,实测数据来自某电商后台系统的压测环境(QPS 3000,热点SKU查询占比62%):

维度Ehcache 3.xRedis(单节点)Caffeine 3.x自研ConcurrentHashMap
接入成本MyBatis原生支持,改1行XML+加3个jar需自定义CacheManager,写序列化/反序列化逻辑需扩展MyBatis Cache接口,重写put/get/clear改动最小,但无淘汰、无统计、无过期
本地性能(μs/次)85(堆内)~120(堆外)320(含网络RTT)45(纯内存)25(无锁,但无管理)
集群一致性支持RMI/Terracotta协议同步,延迟<15ms天然支持,但需处理缓存穿透/雪崩仅本地,需额外集成Spring Cloud Bus等完全不支持
内存控制能力可设堆内/堆外/磁盘三级容量,支持LRU/LFU/Soft引用依赖maxmemory+策略,OOM风险高基于W-TinyLFU,精度高但不可配磁盘溢出无任何控制,OOM高危
可观测性内置JMX MBean,暴露命中率、大小、加载时间等12项指标INFO命令可查,但需解析文本,无标准接口提供StatsCounter,但需自行埋点上报零监控能力
故障隔离性缓存异常自动降级为直连DB,不影响主流程网络抖动导致线程阻塞,需熔断兜底同Ehcache,但无磁盘兜底异常即崩溃

结论很清晰:Redis适合跨语言、高一致性、大容量场景;Caffeine适合极致性能、纯内存、无运维诉求的微服务;而Ehcache是Java单体应用在“性能+可控性+可维护性”三角中最优平衡点。尤其当你的团队没有专职中间件运维、又要求缓存故障时业务不中断——Ehcache的自动降级机制(比如磁盘缓存失效后静默走DB)比任何人工兜底代码都可靠。

2.2 MyBatis缓存链路的“插入点”在哪?为什么选二级缓存而非一级?

MyBatis执行SQL有两条路径:

  • 一级缓存:绑定SqlSession,生命周期=事务周期,作用域极小,且多个SqlSession间完全隔离。它本质是防止“同一个会话里重复查同一条SQL”,解决的是开发阶段的手误问题,不是架构级优化。
  • 二级缓存:绑定Mapper接口(即namespace),所有SqlSession共享,只要namespace相同,缓存就生效。这才是真正的“跨请求、跨事务”的数据复用层。

我们选择二级缓存,是因为它的作用域匹配业务真实痛点:

  • 用户A查“北京朝阳区热门楼盘”,用户B两秒后查同样条件——这该命中;
  • 订单服务更新了商品库存,立即通知商品服务清掉对应缓存——这需要跨SqlSession广播;
  • 后台定时任务批量刷新城市配置,所有前端请求立刻看到新值——这依赖统一的缓存管理入口。

而一级缓存连“用户A自己第二次查”都保不住(因为新请求会创建新SqlSession),更别说跨用户了。所以整合Ehcache,必须插在二级缓存这一环。MyBatis为此预留了标准扩展点:org.apache.ibatis.cache.Cache接口。只要我们写一个类实现它,并把Ehcache的CacheManager和javax.cache.Cache实例封装进去,再在Mapper XML里声明<cache type="com.xxx.EhcacheAdapter"/>,整个链路就通了。

提示:别试图绕过MyBatis Cache接口直接操作Ehcache。我见过有团队在Service层手动get/put,结果事务回滚时缓存没清理,造成脏数据。MyBatis的缓存生命周期是和Executor深度绑定的,只有走标准接口,才能保证commit时自动刷新、rollback时自动清空。

2.3 架构分层设计:三层缓存如何协同工作?

真实生产环境从来不是“一刀切”。我们采用三级缓存策略,让每层干自己最擅长的事:

  1. L1:MyBatis一级缓存(SqlSession级)

    • 保持默认开启,不做任何配置。
    • 它是免费的“防呆层”,避免同一个事务里反复查同一条记录。
    • 注意:selectList和selectOne对同一SQL的多次调用会命中,但insert/update/delete会清空它——这是MyBatis的默认行为,无需干预。
  2. L2:Ehcache二级缓存(Mapper级)

    • 这是本次整合的核心,承载90%的热点数据。
    • 按业务域划分缓存区:user_info、product_sku、sys_dict,各自配置不同TTL和大小。
    • 关键设计:所有写操作(insert/update/delete)必须触发对应缓存区的clear,而不是单条evict——因为MyBatis无法精准知道哪条SQL影响了哪些key(尤其涉及关联查询时),全局清空是最稳妥的。
  3. L3:数据库查询缓存(MySQL Query Cache,已弃用)→ 替换为应用层预热

    • MySQL 8.0已移除Query Cache,且它本身有严重缺陷(如表级锁、碎片化)。
    • 我们改为启动时异步加载高频字典数据到Ehcache,用CacheLoader实现,避免首屏冷启动延迟。

这种分层不是叠床架屋,而是责任分离:L1保事务内一致,L2保跨请求性能,L3保系统启动体验。三者共存,互不干扰,也互为备份。

3. 核心细节解析与实操要点

3.1 Ehcache 3.x vs 2.x:为什么必须升到3.x?

很多老项目还在用Ehcache 2.x,但MyBatis 3.4+官方只适配JSR-107(JCache)标准,而Ehcache 2.x是私有API。强行桥接会导致:

  • CacheManager初始化失败(NoClassDefFoundError: net.sf.ehcache.CacheException);
  • 缓存key序列化异常(2.x用Serializable,3.x用ByteSource);
  • 无法使用JMX监控(2.x的MBean注册方式已废弃)。

Ehcache 3.x是彻底重写的版本,核心变化:

  • 完全基于JSR-107:javax.cache.Cache成为唯一入口,MyBatis只需依赖jcache-api即可;
  • 堆外内存支持:通过OffHeapStore将缓存对象存到堆外,避免GC停顿(实测大对象场景GC时间下降70%);
  • 更精细的淘汰策略:除了LRU/LFU,还支持LinkedHashMap式访问顺序、SoftReference式软引用;
  • 模块化设计:ehcache-core(核心)、ehcache-transactions(事务支持)、ehcache-clustered(集群)可按需引入。

升级步骤很简单,但必须一步到位:

  1. 删除旧jar:ehcache-2.10.6.jar、slf4j-log4j12-1.7.25.jar(冲突);
  2. 引入新坐标(Maven):
<dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.10.8</version> </dependency> <dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> <version>1.1.1</version> </dependency>
  1. 重写CacheManager初始化逻辑——这是最关键的迁移点。

注意:Ehcache 3.x默认不启用堆外内存,必须显式配置。很多人升级后发现内存占用反而更高,就是因为没关掉堆内缓存。正确姿势是:要么全堆内(heap),要么堆内+堆外(heap, offheap),不要只开offheap(它需要堆内做元数据管理)。

3.2 CacheManager初始化:单例、懒加载与配置来源

Ehcache 3.x的CacheManager是重量级对象,必须全局单例,且推荐懒加载(避免Spring容器启动时就初始化所有缓存)。我们采用双重检查锁+静态内部类方式:

public class EhcacheManager { private static volatile org.ehcache.CacheManager cacheManager; private EhcacheManager() {} public static org.ehcache.CacheManager getInstance() { if (cacheManager == null) { synchronized (EhcacheManager.class) { if (cacheManager == null) { // 从classpath加载ehcache.xml,或用代码构建 cacheManager = org.ehcache.config.ConfigurationBuilder .newConfigurationBuilder() .withCache("user_info", org.ehcache.config.CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, User.class, org.ehcache.config.ResourcePoolsBuilder.heap(1000)) .withExpiry(org.ehcache.config.ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofHours(2))) .build()) .build(true); } } } return cacheManager; } }

但生产环境强烈建议用XML配置,原因有三:

  • 可运维:运维同学能直接修改ehcache.xml调整TTL,无需发版;
  • 可审计:所有缓存策略集中管理,避免代码里散落Duration.ofMinutes(30);
  • 可复用:测试环境可加载ehcache-test.xml,用disk策略替代offheap,节省内存。

ehcache.xml关键片段:

<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns='http://www.ehcache.org/v3' xmlns:jsr107='http://www.ehcache.org/v3/jsr107'> <!-- 全局默认策略 --> <service> <jsr107:defaults enable-management="true" enable-statistics="true"/> </service> <!-- user_info缓存区 --> <cache alias="user_info"> <key-type>java.lang.String</key-type> <value-type>com.example.model.User</value-type> <resources> <heap unit="entries">1000</heap> <offheap unit="MB">512</offheap> <disk unit="GB">2</disk> </resources> <expiry> <ttl unit="hours">2</ttl> <tti unit="minutes">30</tti> </expiry> <eviction> <prioritizer>org.ehcache.impl.eviction.LFU</prioritizer> </eviction> </cache> <!-- sys_dict缓存区 --> <cache alias="sys_dict"> <key-type>java.lang.String</key-type> <value-type>java.util.List</value-type> <resources> <heap unit="entries">5000</heap> </resources> <expiry> <ttl unit="days">7</ttl> </expiry> </cache> </config>

这里有几个易错点必须强调:

  • <heap unit="entries">表示最多存多少个对象,不是内存大小!如果存的是大对象(如含100个字段的POJO),1000 entries可能占几百MB;
  • <offheap unit="MB">是实际内存,但必须配合<heap>使用,单独配置无效;
  • <ttl>(Time To Live)是创建后多久过期,<tti>(Time To Idle)是最后访问后多久过期,两者是“或”关系,任一满足即过期;
  • <eviction>里的LFU优先级高于LRU,适合访问频率差异大的场景(如80%流量集中在20%的用户上)。

3.3 MyBatis Cache适配器:如何把Ehcache包装成MyBatis认识的Cache?

MyBatis的Cache接口只有5个方法:get/putObject/remove/clear/getSize。我们要做的,就是把Ehcache的javax.cache.Cache操作翻译过去。关键难点在于:

  • key的序列化:MyBatis传来的key是CacheKey对象(含SQL、参数、环境ID等),不能直接当String用;
  • 线程安全:Cache实例会被多个线程并发调用,必须保证put/get原子性;
  • 生命周期绑定:clear()必须清空底层Ehcache,不能只清内存Map。

适配器核心代码(已脱敏):

public class EhcacheAdapter implements Cache { private final String id; // Mapper namespace private final javax.cache.Cache<Object, Object> cache; public EhcacheAdapter(String id) { this.id = id; // 从EhcacheManager获取CacheManager,再getCache this.cache = EhcacheManager.getInstance() .getCache(id, Object.class, Object.class); } @Override public String getId() { return id; } @Override public void putObject(Object key, Object value) { // 关键:用CacheKey的hashCode作为key,避免序列化开销 // 实测比toString()快3倍,且不会因参数顺序不同产生重复key cache.put(key.hashCode(), value); } @Override public Object getObject(Object key) { return cache.get(key.hashCode()); } @Override public Object removeObject(Object key) { return cache.remove(key.hashCode()); } @Override public void clear() { // 必须调用底层clear,不能只清Map cache.clear(); } @Override public int getSize() { return cache.size(); } }

为什么用key.hashCode()而不是key.toString()?

  • CacheKey的toString()会拼接所有参数,字符串很长(如CacheKey{123456789, selectUserById, [1], 123456789}),生成和比较都慢;
  • hashCode()是int,存取快,且CacheKey重写了equals/hashCode,保证逻辑相等的key有相同hash;
  • 即使hash冲突(概率极低),getObject拿到value后MyBatis还会用CacheKey.equals()二次校验,安全性不打折。

实操心得:别在putObject里做日志打印!我曾在线上环境加了log.debug("Cache put: {} -> {}", key, value),结果日志文件每秒写入2GB,磁盘IO打满。缓存层必须零副作用,所有监控走JMX。

4. 实操过程与核心环节实现

4.1 从零开始的完整配置流程(附参数计算逻辑)

假设你正在重构一个用户中心服务,目标是将UserMapper.selectById的查询响应时间从120ms降到15ms以内。以下是严格按顺序执行的7步操作,每步都附带参数选择依据:

Step 1:确认MyBatis版本与依赖兼容性

  • 检查pom.xml:mybatis-spring必须≥2.0.0(支持JSR-107),spring-framework≥5.0.0;
  • 若用Spring Boot 2.1+,直接引入spring-boot-starter-cache会冲突,必须排除它:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <exclusions> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </exclusion> </exclusions> </dependency>

理由:Spring Cache是另一套抽象,和MyBatis Cache并存会引发双重代理、缓存不一致等问题。

Step 2:准备ehcache.xml并放置到classpath

  • 文件路径:src/main/resources/ehcache.xml;
  • 根据业务数据特征计算初始参数:
    • user_info缓存区:日活用户50万,其中20%为活跃用户(10万),平均每人每天查3次 → 日查询量30万;
    • 按80/20法则,20%的用户贡献80%的查询 → 热点用户约2万,日查询24万;
    • 单次查询结果平均大小1KB → 热点数据总内存需求 = 2万 × 1KB ≈ 20MB;
    • 因此<heap>设为5000 entries(留冗余),<offheap>设为128MB(应对突发流量);
    • TTL设为2小时:用户资料变更不频繁,但需保证最终一致性。

Step 3:编写EhcacheAdapter并注册到Spring

  • 类路径:com.example.cache.EhcacheAdapter;
  • 在Spring配置类中声明Bean(注意:必须用@Primary,否则MyBatis找不到):
@Configuration public class CacheConfig { @Bean @Primary public Cache cache() { return new EhcacheAdapter("com.example.mapper.UserMapper"); } }

提示:@Primary是关键!MyBatis通过Cache接口类型注入,若存在多个实现,必须指定主Bean,否则启动报NoUniqueBeanDefinitionException。

Step 4:在UserMapper.xml中启用二级缓存

  • 在<mapper>根节点下添加:
<cache type="com.example.cache.EhcacheAdapter" eviction="LRU" flushInterval="600000" size="1000" readOnly="true"/>

参数详解:

  • type:指向适配器全限定名;
  • eviction:此处是MyBatis的淘汰策略(仅当未配置Ehcache时生效),我们已由Ehcache接管,可忽略;
  • flushInterval="600000":每10分钟自动清空一次缓存,这是防缓存击穿的兜底策略(即使DB数据被手动修改,10分钟内也会同步);
  • size="1000":MyBatis层面的大小限制,必须≤Ehcache配置的heap值,否则报错;
  • readOnly="true":表示缓存对象不被修改(MyBatis返回的是新对象),可提升性能;若设为false,MyBatis会克隆对象,增加GC压力。

Step 5:Mapper接口方法添加@Options注解(可选但推荐)

  • 在UserMapper.java中:
@Select("SELECT * FROM user WHERE id = #{id}") @Options(useCache = true, flushCache = false) User selectById(@Param("id") Long id);
  • useCache = true:显式开启缓存(默认true,但写明更清晰);
  • flushCache = false:查询不触发清空(默认false);
  • 对应的更新方法必须设为flushCache = true:
@Update("UPDATE user SET name = #{name} WHERE id = #{id}") @Options(flushCache = true) int updateName(@Param("name") String name, @Param("id") Long id);

这样,每次updateName执行后,MyBatis会自动调用EhcacheAdapter.clear(),保证缓存与DB一致。

Step 6:启动应用并验证JMX监控

  • 启动参数加:-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999;
  • 用JConsole连接,展开net.sf.ehcache节点,查看user_info缓存的CacheHits、CacheMisses、AverageGetTime;
  • 初始状态CacheHits=0,执行几次selectById后,CacheHits应增长,AverageGetTime稳定在100μs内。

Step 7:压测对比与参数调优

  • 工具:JMeter模拟1000并发,持续5分钟;
  • 对比指标:
    场景平均响应时间DB CPU缓存命中率
    未启用缓存128ms89%0%
    Ehcache启用14.2ms32%92.7%
  • 发现问题:sys_dict缓存命中率仅65%,查日志发现大量CacheMiss;
  • 原因:字典表有2000+条,但ehcache.xml中sys_dict的heap只设了500 entries;
  • 调整:<heap unit="entries">5000</heap>,重启后命中率升至98.3%。

4.2 关键参数调优指南:不是越大越好,而是恰到好处

缓存配置不是填数字游戏,每个参数背后都有资源代价。以下是我们在3个项目中总结的黄金比例:

参数推荐值为什么这么设调错后果
heap unit="entries"热点数据量 × 1.5留50%冗余应对突发流量,避免频繁淘汰设太小:命中率暴跌,频繁GC;设太大:内存浪费,OOM风险
offheap unit="MB"heap内存的2~3倍堆外内存不参与GC,但需堆内元数据管理,比例失衡会拖慢访问设太小:堆外空间不足,退化为纯堆内;设太大:系统内存吃紧
ttl unit="hours"数据变更周期 ÷ 3保证最终一致性,如配置表每天更新,则TTL=8小时设太短:频繁重建缓存,DB压力不减;设太长:数据陈旧
flushInterval10~30分钟折中“实时性”与“DB压力”,比TTL短,确保强兜底设太短:频繁清空,缓存形同虚设;设太长:脏数据窗口过大

特别提醒flushInterval的隐藏价值:它不仅是兜底,更是缓存预热的触发器。我们在flushInterval到期前1秒,异步加载下一轮热点数据到缓存,实现“无缝续杯”。代码片段:

// 在CacheManager初始化后启动定时任务 ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() -> { try { // 加载用户画像TOP1000 List<User> hotUsers = userMapper.selectHotUsers(1000); hotUsers.forEach(u -> cache.put(u.getId().hashCode(), u)); } catch (Exception e) { log.error("Preload failed", e); } }, 0, 10, TimeUnit.MINUTES);

4.3 生产环境必备的监控与告警配置

没有监控的缓存等于埋雷。我们强制要求以下4项必须接入:

  1. JMX基础指标采集(Prometheus + JMX Exporter)

    • 监控项:CacheHits,CacheMisses,CacheGets,AverageGetTime,OnHeapSize,OffHeapSize;
    • 告警规则:CacheHitRate < 85%(连续5分钟)→ 触发缓存失效排查;AverageGetTime > 500000(500μs)→ 触发堆外内存不足告警。
  2. MyBatis执行日志增强

    • 在logback-spring.xml中开启:
<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG"/>
  • 日志样例:Cache Hit Ratio [com.example.mapper.UserMapper]: 0.923,直接暴露命中率。
  1. 缓存穿透防护(布隆过滤器前置)

    • 对selectById这类根据ID查询的接口,在DAO层加一层布隆过滤器:
      if (!bloomFilter.mightContain(id)) { return null; // ID肯定不存在,不查缓存也不查DB }
    • 布隆过滤器用Redis实现(内存小、速度快),每日凌晨全量重建。
  2. 缓存雪崩熔断(Hystrix集成)

    • 当CacheMisses突增300%(相比昨日同期),自动切换为“直连DB”模式,持续5分钟;
    • 代码用@HystrixCommand(fallbackMethod = "fallbackSelectById")实现。

实操心得:别信“缓存命中率100%”的幻觉。我们线上系统稳定在92%~95%,低于90%才告警。因为总有新用户、新查询条件、缓存预热间隙,100%意味着配置僵化或监控失真。

5. 常见问题与排查技巧实录

5.1 典型问题速查表(附根因与解决方案)

问题现象可能根因排查命令/日志解决方案
应用启动报ClassNotFoundException: javax.cache.Cachecache-apijar未引入,或版本与Ehcache不匹配mvn dependency:tree | grep cache检查cache-api版本是否为1.1.1,Ehcache是否为3.10+
缓存始终不命中,CacheHitRatio恒为0Mapper XML中<cache>标签位置错误(不在<mapper>根节点下);或useCache=false查看MyBatis日志,搜索Cache Hit Ratio确保<cache>是<mapper>的直接子节点,且@Options(useCache=true)
更新数据后,缓存未清除,查到旧值flushCache=true未配置在update语句上;或EhcacheAdapter.clear()未调用开启LoggingCache日志,观察clear是否执行在所有@Update/@Delete方法上显式加@Options(flushCache=true)
JConsole看不到Ehcache MBeanJVM未开启JMX远程,或ehcache.xml中enable-management="false"jps -l查进程ID,jstack <pid>看JMX端口启动参数加-Dcom.sun.management.jmxremote.port=9999,XML中设enable-management="true"
堆内存飙升,Full GC频繁heap单位误设为MB(应为entries),导致存入超量对象jstat -gc <pid>看OU(老年代使用率)检查ehcache.xml,<heap unit="entries">必须是数字,不是内存大小

5.2 一次真实的线上事故复盘:缓存雪崩是如何发生的?

时间:某周五晚20:15
现象:用户中心接口P99响应时间从200ms飙升至3.2s,DB CPU 100%,告警群炸锅。

排查过程:

  1. 第一时间jstack <pid>,发现大量线程阻塞在EhcacheAdapter.getObject;
  2. 查JMX,user_info的OnHeapSize为0,OffHeapSize为128MB,但CacheMisses每秒2000+;
  3. 翻ehcache.xml,发现flushInterval="60000"(1分钟)——这是测试环境配置,误发到生产!
  4. 根本原因:每分钟强制清空缓存,而此时恰逢晚高峰,1000并发涌进来,全部打到DB,瞬间击穿。

解决方案:

  • 立即curl -X POST http://localhost:8080/actuator/refresh(Spring Boot Actuator)动态刷新配置,将flushInterval改为600000;
  • 临时扩容DB连接池,从50→200;
  • 事后在CI流程中加入ehcache.xml语法检查,禁止flushInterval < 300000的配置合并。

教训:flushInterval不是越短越安全,而是要匹配业务节奏。我们后来规定:生产环境flushInterval必须≥10分钟,且需经过压测验证。

5.3 高级避坑技巧:那些文档里不会写的细节

  • 技巧1:用CacheKey.update()代替new CacheKey()
    MyBatis的CacheKey构造耗时(需计算所有参数hash),而update()可复用对象。在自定义Executor中:

    CacheKey key = new CacheKey(); key.update(mapperId); key.update(param); // 比 new CacheKey(mapperId, param) 快40%
  • 技巧2:readOnly="true"时,返回对象必须不可变
    如果User类有setName()方法,MyBatis不会克隆它,外部修改会影响缓存。解决方案:

    • 用Lombok@Value生成不可变对象;
    • 或在EhcacheAdapter.getObject()中返回new User(user)深拷贝(牺牲性能换安全)。
  • 技巧3:集群环境下,用RmiCacheManager替代默认CacheManager
    默认CacheManager是单机的,集群需同步。Ehcache 3.x提供RmiCacheManager:

    Configuration config = ConfigurationBuilder.newConfigurationBuilder() .withClusteredStore("cluster", ClusteredStoreConfigurationBuilder .newClusteredStoreConfigurationBuilder() .withServerList("192.168.1.10:9527,192.168.1.11:9527"));
  • 技巧4:@SelectProvider动态SQL的缓存陷阱
    @SelectProvider生成的SQL字符串包含时间戳、随机数等,导致key永远不同。解决方案:

    • 在Provider方法里,用System.currentTimeMillis()/60000(分钟级)替代秒级时间戳;
    • 或用@Select("SELECT * FROM user WHERE status = #{status} /* #cacheKey# */"),在SQL注释中写死key标识。

最后分享一个小技巧:在EhcacheAdapter.clear()里加一行日志log.info("Cache cleared for {}", id),然后用ELK收集。当某天发现sys_dict每秒被清空10次,就知道是哪个定时任务在疯狂刷缓存——这比看代码快10倍。

我在实际使用中发现,缓存不是“开了就灵”的银弹,

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询