标准结构
请求 → 本地缓存(Caffeine)
→ 有 → 直接返回
→ 无 → 查 Redis
→ 有 → 写回本地缓存 → 返回
→ 无 → 查 DB → 写 Redis + 写本地缓存
实现步骤
1. 引入本地缓存(Caffeine 最强)
<dependency>
<groupId>com.github.benmanes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.2</version>
</dependency>
2. 构建本地缓存
// 本地缓存:5分钟过期,最大10000个key
public static final LoadingCache<String, Object> LOCAL_CACHE = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES) // 本地缓存5分钟
.maximumSize(10000)
.build(key -> null);
3.实现
public Object get(String key) {
// ========== 1. 先读本地缓存 ==========
Object obj = LOCAL_CACHE.getIfPresent(key);
if (obj != null) {
return obj;
}
// ========== 2. 再读 Redis ==========
obj = redisTemplate.opsForValue().get(key);
if (obj != null) {
// 写回本地缓存
LOCAL_CACHE.put(key, obj);
return obj;
}
// ========== 3. 互斥锁,防止击穿 ==========
RLock lock = redissonClient.getLock("lock:" + key);
try {
lock.lock(3, TimeUnit.SECONDS);
// 双重检查
obj = redisTemplate.opsForValue().get(key);
if (obj != null) {
LOCAL_CACHE.put(key, obj);
return obj;
}
// ========== 4. 查询 DB ==========
obj = dbMapper.selectById(key);
// ========== 5. 防雪崩核心:TTL 加随机值 ==========
long ttl = 60 * 60 + new Random().nextInt(600); // 1小时 ±10分钟
// 热点key直接永不过期
if (isHotKey(key)) {
redisTemplate.opsForValue().set(key, obj);
} else {
redisTemplate.opsForValue().set(key, obj, ttl, TimeUnit.SECONDS);
}
// 写本地缓存
LOCAL_CACHE.put(key, obj);
} finally {
lock.unlock();
}
return obj;
}
数据库缓存(默认开启)
以 MySQL 为例,主要包括:
InnoDB Buffer Pool(缓冲池)
查询缓存(MySQL 8.0 已去掉)
磁盘数据页缓存、索引缓存
它的特点:
在 MySQL 进程内部
缓存的是:数据页、索引页
作用:减少磁盘 IO,让重复查询更快
对应用透明,你代码完全感知不到
只要你查库,它就自动生效
此处注意与常规的数据库持久化落盘区别。
此处甄别MyBatis 一级缓存,二级缓存
一级缓存 = SqlSession 级别的缓存
特点:
在 你的 Java 应用进程内
默认开启
同一个 SqlSession(同一次请求 / 同一个事务)
相同 SQL + 参数 → 直接返回缓存,不查库
事务提交 / SqlSession 关闭,缓存就清空
适用:
一次请求里多次查同一条数据
范围极小,基本不会有脏数据问题
二级缓存 = Mapper 级别的全局缓存(跨 SqlSession,整个应用共享)
特点:
默认关闭,需要手动配置开启
缓存存在 应用内存
多个请求、多个线程共用
增删改会自动清空该表缓存
问题很大:
分布式下极易脏数据
高并发非常不安全
实际项目基本不用