mybatis自定义cache不生效,主因是未正确注册(缺@cachenamespace或)、被代理导致spring bean注入失败、getid()返回固定值引发key冲突、序列化异常静默失败、事务回滚后clear()未触发及缓存穿透/击穿/雪崩未防护。

MyBatis Cache 接口实现类不生效?先看是否被代理绕过
MyBatis 二级缓存默认走 PerpetualCache,一旦你写了自定义 Cache 实现(比如用 Redis),却没生效,大概率是缓存未被真正注册进 MappedStatement。常见原因是:Mapper 接口上漏了 @CacheNamespace 注解,或 XML 中没配 <cache></cache> 标签。
更隐蔽的坑是:MyBatis 会为每个 Cache 实例自动套一层 SynchronizedCache 或 LoggingCache 代理——这意味着你不能在 Cache 实现里直接依赖 Spring Bean(比如 RedisTemplate),因为代理对象不是 Spring 管理的 Bean。
- 必须在
Cache构造时传入RedisTemplate,而不是靠@Autowired - XML 方式启用缓存时,
<cache type="com.example.RedisCache"></cache>的type必须是全限定名,且该类要有 public 无参构造器(否则 MyBatis 反射失败) - 如果用了
@Select+@Options(useCache = true),但 Mapper 类没加@CacheNamespace,缓存照样不走你的实现
Redis key 冲突或序列化失败?检查 Cache 的 getId() 和序列化逻辑
MyBatis 每个 MappedStatement 会生成一个唯一的 Cache 实例,其 getId() 返回值就是 Redis 中的 key 前缀。如果你所有 Mapper 共享同一个 RedisCache 实例(比如单例模式),getId() 返回固定值,会导致不同 SQL 的结果互相覆盖。
另一个高频问题是 value 序列化:MyBatis 把整个查询结果(List/Map/POJO)塞进 Redis,若没指定序列化器,JdkSerializationRedisSerializer 会默默失败(尤其遇到 Lambda 表达式、非 Serializable 字段时),日志里可能只报 Cannot serialize 而不抛异常。
-
getId()必须返回MappedStatement的 namespace + id,例如"com.example.UserMapper.selectById" - RedisTemplate 的
valueSerializer建议设为GenericJackson2JsonRedisSerializer,避免 JDK 序列化兼容性问题 - 不要在
putObject()里对 value 做额外包装(比如套CacheValueWrapper),MyBatis 本身不处理嵌套结构,反序列化时容易ClassCastException
缓存穿透/击穿/雪崩没防?别只盯着 Cache 接口写法
自定义 Cache 接口只是把数据存到 Redis,但 MyBatis 不管缓存策略。比如空结果不缓存,下次请求直接打到 DB;高并发查同一条不存在的 ID,大量请求穿透;热点 key 过期瞬间流量全涌向 DB——这些问题不会因为你实现了 Cache 就自动消失。
最简落地方式是在 getObject() 和 putObject() 里加控制,而不是依赖外部组件拦截。
-
getObject()返回 null 时,可主动写入一个空对象(如new NullValueHolder())并设短过期时间(2~5 分钟),防止穿透 -
putObject()写入前,用RedisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS)防击穿(注意:需确保 key 有唯一性,不能只靠getId()) - 避免所有缓存用同一过期时间,可在
putObject()里对不同 namespace 设置差异化 TTL,比如用户信息 30 分钟,配置表 2 小时
事务中缓存不一致?MyBatis 的 clear() 不一定触发
MyBatis 在执行 INSERT/UPDATE/DELETE 语句后,会调用当前 namespace 对应 Cache 的 clear() 方法。但这个动作只发生在 SQL 执行成功且事务提交后——如果用了 Spring 声明式事务,而方法抛异常回滚,clear() 就不会调用,Redis 里还是旧数据。
更麻烦的是:跨 namespace 更新(比如 UserMapper.update() 应该清掉 OrderMapper 缓存)MyBatis 根本不管,得你自己协调。
- 不要依赖 MyBatis 自动
clear()做强一致性,关键业务建议在 service 层显式调用RedisTemplate.delete(pattern)清相关 key - 如果必须用 MyBatis 自动清理,确保
clear()方法里用RedisTemplate.delete(getId() + ":*")批量删,而不是只删getId()本身(它只是前缀) - 开启
cacheEnabled=true且useCache=true的同时,确认没有在select标签里写flushCache="true"——这会导致每次查询都先清再查,完全废掉缓存价值
真正难的不是写个 RedisCache 类,而是想清楚哪些数据允许脏读、哪些更新必须强一致、Redis key 的粒度和生命周期怎么跟业务语义对齐。很多人卡在“缓存没生效”,其实问题早在线上压测时就暴露了:key 设计不合理、序列化失败静默吞异常、事务回滚后缓存滞留——这些细节不盯住,代码写得再工整也没用。










