文章目录
一、缓存穿透
1. 什么是缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,给数据库带来压力
2. 解决方案
2.1 缓存空对象
思路:客户端请求Redis,请求未命中,请求数据库也未名中,这时直接将空对象缓存在Redis中
优点:实现简单,维护方便
缺点:额外的内存消耗(可以为其增加一个短期的TTL来解决)、可能造成短期的不一致(新增数据时,主动将数据插入缓存中,覆盖之前的null)
//缓存穿透
public Shop queryWithPassThrough(Long id){
//1.从redis查询商铺缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是空值
if (shopJson != null) {
//返回错误信息
return null;
}
//4.不存在,根据id查询数据库,
Shop shop = getById(id);
//5.不存在,返回错误
if (shop == null) {
//将空值写入Redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7.返回
return shop;
}
2.2 布隆过滤
思路:在客户端和Redis之间添加布隆过滤器。当用户请求时,首先到布隆过滤器,如果数据不存在,直接拒绝。如果存在,则放行。
优点:内存占用较少,没有多余的key
缺点:实现复杂、存在误判可能
2.3 增加id的复杂度,避免被猜测id规律
2.4 做好数据的基础格式校验
2.5 加强用户权限校验
2.6 做好热点参数的限流
二、缓存雪崩
1. 什么是缓存雪崩
缓存雪崩是指在同一时间段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到数据库,带来巨大压力
2. 解决方案
- 给不同的key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
三、缓存击穿
1. 什么是缓存击穿
也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
2. 解决方案
-
互斥锁:当一个请求查询缓存,未命中时,获取互斥锁成功,查询数据库重建缓存数据,写入缓存,释放锁。其他的线程查询缓存时,未命中,获取互斥锁失败,休眠一会重试,失败再重试,直到获取到互斥锁,查询到缓存。
//缓存击穿:互斥锁解决 public Shop queryWithMutex(Long id){ //1.从redis查询商铺缓存 String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); //2.判断是否存在 if (StrUtil.isNotBlank(shopJson)) { //3.存在,直接返回 return JSONUtil.toBean(shopJson, Shop.class); } //判断命中的是否是空值 if (shopJson != null) { //返回错误信息 return null; } //4.实现缓存重建 //4.1 获取互斥锁 String lockKey = LOCK_SHOP_KEY + id; Shop shop = null; try { boolean isLock = tryLock(lockKey); //4.2 判断是否获取成功 if (!isLock) { //4.3 失败,则休眠并重试 Thread.sleep(50); return queryWithMutex(id); } //4.4 成功,根据id查询数据库 shop = getById(id); //5.不存在,返回错误 if (shop == null) { //将空值写入Redis stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } //6.存在,写入redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { //7.试放互斥锁 unlock(lockKey); } //8.返回 return shop; } private boolean tryLock(String key){ Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } private void unlock(String key){ stringRedisTemplate.delete(key); }
-
逻辑过期:在添加缓存时,为缓存添加一个逻辑时间,当有请求查询缓存时,发现逻辑时间已过期,便获取互斥锁,开启子线程,查询数据库重建缓存数据,写入缓存数据并重新设置逻辑过期时间,释放锁。在子线程查询数据库写入缓存时,主线程返回过期数据
@Data public class RedisData { private LocalDateTime expireTime; //逻辑过期时间 private Object data; //要存入的数据 }
public void saveShop2Redis(Long id, Long expireSeconds){ //1.查询店铺数据 Shop shop = getById(id); //2.封装逻辑过期时间 RedisData redisData = new RedisData(); redisData.setData(shop); redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); //3.写入Redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData)); } public static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); //缓存击穿:逻辑过期解决 public Shop queryWithLogicExpire(Long id){ //1.从redis查询商铺缓存 String key = CACHE_SHOP_KEY + id; String shopJson = stringRedisTemplate.opsForValue().get(key); //2.判断是否存在 if (StrUtil.isBlank(shopJson)) { //3.存在,直接返回 return null; } //4.命中,需要先把json反序列化为对象 RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class); Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class); LocalDateTime expireTime = redisData.getExpireTime(); //5.判断是否过期 if (expireTime.isAfter(LocalDateTime.now())){ //5.1 未过期,直接返回店铺信息 return shop; } //5.2已过期,需要缓存重建 //6.缓存重建 //6.1 获取互斥锁 String lockKey = LOCK_SHOP_KEY + id; boolean isLock = tryLock(lockKey); //6.2 判断是否获取锁 if (isLock){ //6.3 成功,开启独立线程,实现缓存重建 CACHE_REBUILD_EXECUTOR.submit(() -> { try { //重建缓存 this.saveShop2Redis(id, 20L); } catch (Exception e) { throw new RuntimeException(e); } finally { //释放锁 unlock(lockKey); } }); } //6.4 返回过期的商铺信息 return shop; }
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 没有额外的内存消耗 保证一致性 实现简单 | 线程需要等待,性能受影响 可能有死锁风险 |
逻辑过期 | 线程无需等待,性能较好 | 不保证一致性 有额外内存消耗 实现复杂 |
四、缓存工具封装
基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
-
将任意Java对象序列化为json并存储在string类型的key中,并可以设置TTL过期时间
public void set(String key, Object value, Long time, TimeUnit unit){ stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit); }
-
将任意Java对象序列化为json并存储在string类型的key中。并且可以设置逻辑过期时间,用于处理缓存击穿问题
public void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit){ //设置逻辑过期 RedisData redisData = new RedisData(); redisData.setData(value); redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))); //写入redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); }
-
根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){ //1.从redis查询商铺缓存 String key = keyPrefix + id; String json = stringRedisTemplate.opsForValue().get(key); //2.判断是否存在 if (StrUtil.isNotBlank(json)) { //3.存在,直接返回 return JSONUtil.toBean(json, type); } //判断命中的是否是空值 if (json != null) { //返回错误信息 return null; } //4.不存在,根据id查询数据库, R r = dbFallback.apply(id); //5.不存在,返回错误 if (r == null) { //将空值写入Redis stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } //6.存在,写入redis this.set(key, r, time, unit); //7.返回 return r; }
-
根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
private boolean tryLock(String key){ Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } private void unlock(String key){ stringRedisTemplate.delete(key); } public static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); //缓存击穿:逻辑过期解决 public <R,ID> R queryWithLogicExpire(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){ //1.从redis查询商铺缓存 String key = keyPrefix + id; String json = stringRedisTemplate.opsForValue().get(key); //2.判断是否存在 if (StrUtil.isBlank(json)) { //3.存在,直接返回 return null; } //4.命中,需要先把json反序列化为对象 RedisData redisData = JSONUtil.toBean(json, RedisData.class); R r = JSONUtil.toBean((JSONObject) redisData.getData(), type); LocalDateTime expireTime = redisData.getExpireTime(); //5.判断是否过期 if (expireTime.isAfter(LocalDateTime.now())){ //5.1 未过期,直接返回店铺信息 return r; } //5.2已过期,需要缓存重建 //6.缓存重建 //6.1 获取互斥锁 String lockKey = LOCK_SHOP_KEY + id; boolean isLock = tryLock(lockKey); //6.2 判断是否获取锁 if (isLock){ //6.3 成功,开启独立线程,实现缓存重建 CACHE_REBUILD_EXECUTOR.submit(() -> { try { //查询数据库 R r1 = dbFallback.apply(id); //写入redis this.setWithLogicExpire(key, r1, time, unit); } catch (Exception e) { throw new RuntimeException(e); } finally { //释放锁 unlock(lockKey); } }); } //6.4 返回过期的商铺信息 return r; }