缓存三剑客解决方案
1.缓存雪崩
定义:大量缓存数据在同一时间点集体失效,导致所有请求直接穿透到数据库,引发数据库瞬时高负载甚至崩溃。
解决方案:设置过期随机值,避免大量缓存同时失效
// 缓存雪崩防护:随机过期时间 + 双层缓存
// 设置随机过期时间(基础时间 + 随机偏移)
Random random = new Random();
long expire = baseExpire + random.nextInt(5 * 60 * 1000); // 基础5分钟 + 随机5分钟内
data = loader.load();
setCache(key, data, expire);
setCache(backupKey, data, expire * 2); // 备份缓存过期时间更长
return data;
}
2. 缓存击穿解决方案
定义:某个热点Key突然失效(如过期或被删除),同时有大量并发请求访问该Key,导致请求全部穿透到数据库。
方案1:互斥锁(分布式锁)
@Nullable
// todo 3、缓存击穿 -> 互斥锁:只能由一个线程进行缓存构建,其他线程等待,吞吐量较低
private Shop huchi(Long id) {
String shopJsonStr = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
if (StrUtil.isNotBlank(shopJsonStr)) {
return JSONUtil.toBean(shopJsonStr, Shop.class);
}
// 未命中获取锁
String tryLockKey = "cache:shop:lock:" + id;
Shop shop = null;
try {
boolean tryLock = getLock(tryLockKey);
// 未命中:不断休眠直至获取成功
while (!tryLock) {
Thread.sleep(50);
tryLock = getLock(tryLockKey);
}
// 获取互斥锁,进行缓存的构建
shop = getById(id);
if (shop == null) {
// 数据库中也不存在时候,进行空字符串缓存
stringRedisTemplate.opsForValue().set("cache:shop:" + id, "", 2, TimeUnit.MINUTES);
return null;
}
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop), 2, TimeUnit.MINUTES);
} catch (Exception e) {
e.getStackTrace();
} finally {
unLock(tryLockKey);
}
return shop;
}
方案2:逻辑过期(适合高并发读场景)
@Nullable
// todo 3、缓存击穿 -> 逻辑过期:通过设置逻辑过期时间,然后判断是否过期来确定是否进行缓存更新
private Shop exLogical(Long id) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
String shopJsonStr = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
// 如果不存在那就是一定不存在
if (StrUtil.isBlank(shopJsonStr)) {
return null;
}
//
RedisDate redisDate = JSONUtil.toBean(shopJsonStr, RedisDate.class);
Shop shop = JSONUtil.toBean((JSONObject) redisDate.getObject(), Shop.class);
// 未逻辑过期
if (redisDate.getEx().isAfter(LocalDateTime.now())) {
return shop;
}
// 逻辑过期
// 缓存重建
String tryLockKey = "cache:shop:lock:" + id;
boolean tryLock = getLock(tryLockKey);
if (tryLock) {
// 开启独立的线程去独立的进行缓存
executorService.submit(() -> {
try {
this.saveShopRedis(id, 20L);
} finally {
unLock(tryLockKey);
}
});
}
return shop;
}
// 手动设置逻辑过期时间
private void saveShopRedis(Long id, Long ex) {
Shop shop = getById(id);
RedisDate redisDate = new RedisDate();
redisDate.setEx(LocalDateTime.now().plusSeconds(ex));
redisDate.setObject(shop);
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(redisDate));
}
3. 缓存穿透解决方案
定义:查询数据库中根本不存在的数据(如非法ID或恶意攻击),导致请求绕过缓存直接访问数据库。
方案1:缓存空对象
@Nullable
// todo 1、解决缓存穿透问题
private Shop chaungtou(Long id) {
// 缓存穿透解决方案 -> 缓存""空字符冲
String shopJsonStr = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
if (StrUtil.isNotBlank(shopJsonStr)) {
return JSONUtil.toBean(shopJsonStr, Shop.class);
}
// shopJsonStr == "":代表用户访问的是一个数据库中不存在的数据
if (shopJsonStr != null) {
// 店铺不存在
return null;
}
Shop shop = getById(id);
if (shop == null) {
// 数据库中也不存在时候,进行空字符串缓存
stringRedisTemplate.opsForValue().set("cache:shop:" + id, "", 2, TimeUnit.MINUTES);
return null;
}
stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop), 2, TimeUnit.MINUTES);
return shop;
}