什么是缓存:
缓存就是数据交换的缓冲区(Cache),是存储数据的临时地方,一般读写性能较高。
缓存的作用:降低后端负载、提高读写效率,降低响应时间
缓存的成本:数据一致性成本、代码维护成本、运维成本
添加Redis缓存:


给商铺类型查询业务添加缓存

缓存更新策略:




给查询商铺的缓存添加超时剔除和主动更新的策略

写入redis时,设置超时时间

根据ID修改店铺时,先修改数据库,在删除缓存
缓存穿透:
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。常见的解决方案有两种:
- 缓存空对象 优点:实现简单,维护方便 缺点:额外的内存消耗 可能造成短期的不一致
- 布隆过滤 优点:内存占用少,没有多余的key 缺点:实现复杂 存在误判可能
解决商铺查询的缓存穿透问题


缓存雪崩:
缓存雪崩是指在同一时间大量缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的key的TTL添加随机值(key失效)
- 利用redis集群增加服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿:
缓存击穿也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会瞬间给服务器带来巨大的冲击。
解决方案:
- 互斥锁
- 逻辑过期


基于互斥锁方式解决缓存击穿问题
修改根据ID查询商铺的业务

先定义获取锁和释放锁的两个方法

按流程图来实现

基于逻辑过期方式解决缓存击穿问题

java
public Shop queryWithLogicalExpire(Long id){
//1.从redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if(StrUtil.isBlank(shopJson)){
//3.不存在,返回
return null;
}
//4.存在,需要先把json反序列化成对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, 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);
if(isLock){
//6.2.获取锁成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//缓存重建
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(lockKey);
}
});
}
//6.3.返回过期的商铺信息
return shop;
}
缓存重建

缓存工具封装

java
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
// 构造函数注入
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
public void setWithLogical(String key, Object value, Long time, TimeUnit unit) {
//设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds( time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
//1.从redis中查询商铺缓存
String Json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
//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){
//5.1.不存在,将缓存设为空,过期时间为2分钟
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.存在,写入redis
this.set(keyPrefix + id, r, time, unit);
//7.返回
return r;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public <R,ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){
//1.从redis中查询商铺缓存
String Json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
//2.判断是否存在
if(StrUtil.isBlank(Json)){
//3.不存在,返回
return null;
}
//4.存在,需要先把json反序列化成对象
RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
R r = JSONUtil.toBean(data, 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);
if(isLock){
//6.2.获取锁成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//缓存重建
R r1 = dbFallback.apply(id);
//写入redis
this.setWithLogical(keyPrefix, r1, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(lockKey);
}
});
}
//6.3.返回过期的信息
return r;
}
//尝试获取锁
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);
}
}