一、开场白:你们要的"缓存避坑指南",我攒了三年经验
做后端开发这八年,我见过太多人栽在MyBatis缓存上------
有人一级缓存不关,导致多线程串数据;
有人二级缓存乱用,缓存穿透把DB干崩;
更离谱的是,有人连PerpetualCache
的LRU策略都没搞懂,就敢说自己"精通"MyBatis。
上周帮某电商团队排查接口QPS暴跌问题,根源就是缓存穿透:黑客批量刷不存在的商品ID,一级缓存不存null
,二级缓存默认不开,DB直接被怼到5000+ QPS。
今晚不藏着掖着,把我踩过的坑、调过的参数、踩过的雷,全掏出来------从PerpetualCache
的底层LRU逻辑,到用Redis搭分布式二级缓存防穿透,手把手教你把MyBatis缓存用成"数据库保镖"。
二、MyBatis缓存底层:一级是"本地小仓库",二级是"分布式粮仓"
先掰扯清楚两个核心概念------这不是文档复述,是我看了MyBatis源码、踩过无数坑后的总结。
1. 一级缓存(Local Cache):SqlSession的"临时抽屉"
- 默认开:每个SqlSession自带,生命周期随Session结束(比如Service方法跑完就清空);
- **底层是
PerpetualCache
**:用LinkedHashMap
实现LRU(最近最少使用)淘汰; - 坑点 :不存
null
值(怕脏数据)、多实例不共享(实例1查过的商品,实例2还得撞DB)。
源码级真相 :
PerpetualCache
的cache
字段是个LinkedHashMap
,构造时accessOrder=true
------每次get都会把Entry移到链表尾(标记"最近用过");超过容量(默认无界,但可通过size
限制)时,删链表头的"最老"Entry。
java
// MyBatis源码简化:PerpetualCache的核心存储结构
public class PerpetualCache implements Cache {
private final Map<Object, Object> cache = new LinkedHashMap<Object, Object>(128, 0.75F, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
return size() > maxSize; // 超容量,删最老的
}
};
// ... get/put方法直接操作这个map
}
2. 二级缓存(Second Level Cache):Namespace的"分布式粮仓"
- 跨实例共享 :
Mapper.xml
的namespace
级别生效(比如所有UserMapper
的查询共享缓存); - 默认关 :需手动开
<cache/>
标签; - 可扩展:能替换成Redis、Ehcache等分布式实现------这才是解决多实例缓存共享的关键。
三、缓存穿透:查不存在的数据,为什么能把DB搞崩?
回到电商团队的故障------黑客刷不存在的商品ID(如-1),这就是典型的缓存穿透:
- 定义:查询数据库中不存在的数据,缓存无记录,每次请求都打DB;
- 危害:批量攻击时,DB连接池耗尽,接口响应从200ms飙到5s;
- MyBatis的盲区 :一级缓存不存
null
,二级缓存默认也不存------等于"放任"无效查询直达数据库。
三级缓存穿透解决方案:从"堵"到"防"
方案1:缓存空对象(Null Object)------简单粗暴但有效
- 做法 :DB查不到数据时,缓存
(key, null)
,并设短过期时间(如5分钟); - 优点:挡住重复无效查询;
- 缺点:占缓存空间,数据新增后需等过期。
MyBatis配置 (Mapper.xml
中开启nullValue
):
XML
<cache type="org.mybatis.caches.redis.RedisCache" nullValue="true" expire="300"/>
方案2:布隆过滤器------100%拦截"绝对不存在"的key
- 做法:初始化时把所有存在的商品ID灌进布隆过滤器;
- 逻辑 :查询前先过布隆过滤器------
- 若返回"不存在",直接返回空,不查缓存和DB;
- 若返回"可能存在",再查二级缓存。
实战注意:布隆过滤器有误判率("可能存在"的key可能实际不存在),需结合空对象兜底。
方案3:接口层校验------拦截明显无效请求
- 做法:Controller层对参数做合法性校验(如商品ID必须是正整数);
- 优点:拦截低级错误(如ID=-1、字符串);
- 缺点:防不住"看起来合法"但不存在的ID(如ID=99999999)。
四、实战:用Redis搭MyBatis分布式二级缓存------从0到1
一级缓存解决单实例问题,二级缓存解决多实例共享------Redis是最优解,分布式、高可用、支持灵活序列化。
1. 环境准备
- 依赖(Spring Boot项目):
XML
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Redis配置 (application.yml
):
XML
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
2. 配置MyBatis使用Redis二级缓存
在Mapper.xml
中指定Redis实现,关键参数:
expire
:缓存过期时间(防脏数据);nullValue
:是否缓存null
(防穿透);readWrite
:是否读写缓存(默认true
)
XML
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<cache type="org.mybatis.caches.redis.RedisCache"
expire="300"
nullValue="true"
readWrite="true"/>
<select id="selectById" resultType="User">
select * from user where id = #{id}
</select>
</mapper>
3. 自定义Redis Cache(优化Key和序列化)
默认的RedisCache
用JDK序列化,Key是namespace::id
,不够友好。自定义Cache更灵活:
java
public class CustomRedisCache implements Cache {
private final RedisTemplate<String, Object> redisTemplate;
private final String namespace;
public CustomRedisCache(String namespace) {
this.namespace = namespace;
this.redisTemplate = SpringContextUtils.getBean("redisTemplate");
}
@Override
public String getId() {
return namespace; // 缓存唯一标识
}
@Override
public void putObject(Object key, Object value) {
// 自定义Key:namespace + 方法名 + 参数(如userMapper.selectById::1)
String cacheKey = String.format("%s:%s:%s", namespace, "selectById", key);
redisTemplate.opsForValue().set(cacheKey, value, 5, TimeUnit.MINUTES); // 设过期时间
}
@Override
public Object getObject(Object key) {
String cacheKey = String.format("%s:%s:%s", namespace, "selectById", key);
return redisTemplate.opsForValue().get(cacheKey);
}
// 其他方法(remove/clear等)按需实现...
}
Mapper.xml指定自定义Cache:
XML
<cache type="com.example.cache.CustomRedisCache"/>
. 测试验证
- 正常查询:第一次查商品1,查DB存Redis;第二次直接从Redis拿;
- 不存在的商品 :查ID=-1,DB返回空,Redis存
(-1, null)
,5分钟后过期; - 黑客攻击:刷不存在的ID,后续请求都从Redis拿空值,DB QPS稳定在50+。
五、避坑指南:Redis二级缓存的"生死线"
- 缓存一致性 :更新DB时,必须同步删Redis缓存(比如
@Update
后调用redisTemplate.delete(cacheKey)
); - 序列化:推荐Jackson/Fastjson(比JDK序列化小30%、快2倍);
- 过期时间:根据业务调整(商品详情5分钟,用户信息10分钟);
- 集群模式:所有实例连同一Redis集群,避免缓存分裂。
六、结语:缓存是工具,用对了才是"数据库保镖"
这场"缓存救火"让我彻底明白:
- 一级缓存是"本地优化",解决单实例重复查询;
- 二级缓存是"分布式扩展",解决多实例资源共享;
- 缓存穿透不是缓存的问题,是"没正确用缓存"的问题------空对象、布隆过滤器、接口校验,三管齐下才能防住。
最后送你MyBatis缓存终极Checklist
- 开二级缓存前,确认"需要跨实例共享";
- 必须缓存
null
值,避免无效查询直达DB; - Redis自定义Key和序列化,别用默认的;
- 更新数据时,同步清除缓存。
附录:资料包
- MyBatis Redis缓存源码:mybatis-redis;
- Guava布隆过滤器:BloomFilter;
- Redis序列化工具:Jackson2JsonRedisSerializer。
(全文完)
作者:踩过MyBatis所有缓存坑的后端老炮------专注用"实战经验"帮你少走弯路。
福利:关注我,私信"MyBatis缓存",送你《Redis分布式二级缓存配置手册》+《布隆过滤器实战代码》。
关键词:MyBatis一级缓存、二级缓存、LRU淘汰、缓存穿透、Redis分布式缓存