一、什么是缓存
(一)概念:缓存就是数据交换的缓冲区(称为Cache),是存储数据的临时区域,一般读写性能较高。
(二)常见缓存: 浏览器缓存,服务器缓存,数据库缓存,CPU缓存,磁盘缓存。
(三)缓存的作用:
(四)缓存的成本:
二、缓存更新策略
(一)三种更新策略的对比
内存淘汰 | 超时剔除 | 主动更新 | |
---|---|---|---|
说明 | Redis提供的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存 | 给缓存数据添加TTL,到期后自动删除缓存,下次查询时更新缓存 | 编写业务逻辑,在修改数据库的同时更新缓存 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
(二)业务场景
(三)主动更新策略
三、缓存穿透、缓存雪崩、缓存击穿
(一)缓存穿透
1、缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
2、缓存穿透过程: 客户端发送不存在的id 》 Redis按照 id 查询(未查询到) 》 数据库根据 id 查询(未查询到) 》 返回客户端 》 客户端发送不存在的id。(这个过程若采用多线程循环攻击数据库,那么就会造成数据库带宽爆满,请求阻塞,系统资源耗尽等等问题)
3、缓存穿透常用解决方案:
4、布隆过滤器: 一种算法,通过将数据库的数据进行某一种hash值计算,并将这些hash值转化为二进制位存储到布隆过滤器中,客户端发送的请求中的数据,通过hash计算转化为二进制位在布隆过滤器中查找,若能找到则说明存在该数据。
(二)缓存雪崩
1、缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库。
2、缓存雪崩的过程: 客户端请求数据 》 Redis查询(Redis部分缓存缺失或Redis宕机) 》 查询数据库(大量请求到达,把数据库也给宕机了)
3、解决方案:
(三)缓存击穿
1、缓存击穿是指高并发且缓存重建业务复杂的key突然失效,无数的请求访问在瞬间给数据库带来巨大的冲击。
2、常见的解决方案:
3、两种解决方案的区别
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁 |
|
|
逻辑过期 |
|
|
四、缓存工具封装
基于StringRedisTemplate封装一个缓存工具类。
(一)工具类
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate redisTemplate;
public CacheClient(StringRedisTemplate redisTempalte) {
this.redisTemplate = redisTempalte;
}
public String toJsonStr(Object value) {
return JSONObject.toJSONString(value)
}
public <T> String toJsonObj(String json, Class<T> type) {
return JSONObject.parseObject(json, type)
}
}
(二)实现缓存工具方法
方法1:序列化并设置TTL
public void set(String key, Object data, Long time, TimeUnit unit) {
redisTemplate.opsForValue().set(key, this.toJsonStr(data), time, unit);
}
方法2:序列化并设置逻辑过期
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
//1、设置逻辑过期,RedisData -> (R Data, LocalDateTime dateTime)
RedisData redisData = new RedisData(value, LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//2、RedisData 写入Redis
redisTemplate.opsForValue().set(key, this.toJsonStr(redisData));
}
方法3:缓存空值解决缓存穿透
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
//1、查看Cache是否命中
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
if(!json.isEmpty()){
//命中,判断为有值,直接返回
return this.toJsonObj(json, type);
}
if(Objects.isNull(json)){
//命中,判断为空白,返回错误信息
return null;
}
//未命中,查看数据库
R r = dbFallback.apply(id);
//若不存在对应数据,则打入Redis一个空值,并返回错误信息
if (r == null) {
redisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
return null;
}
//若存在,则返回并设置TTL
this.set(key, r, time, unit);
//返回该值
return r;
}
方法4: 逻辑过期解决缓存击穿
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
//1、查看Cache是否命中
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
if(Objects.isEmpty(json)){
//命中,判断为空白,返回错误信息
return null;
}
RedisData redisData = this.toJsonObject(json, Redis.Class);
R r = type.cast(redisData.getData());
LocalDateTime time = redisData.getDateTime();
//若逻辑时间小于现实时间则返回当前对象
if(time.isBefore(LocalDateTime.now())){
redisData = JSON.parseObject(json, RedisData.class);
if(redisData.getDateTime < LocalDateTime.now()){
return type.cast(redisData.getData());
}
}
//过期了,获取互斥锁
String lockKey = "lock:shop:"+id;
boolean isLock = tryLock(lockKey);
//尝试获取互斥锁,如果事变,则睡眠重试
if(!isLock){
//失败则休眠后重试
Thread.sleep(50);
return queryWithLogicalExpire(keyPrefix, id, type, dbFallback, time, unit);
}
// 重建数据
Executors.newFixedThreadPool(10).submit(() -> {
try{
// 查询数据库
R r1 = dbFallback.apply(id);
this.setWithLogicalExpire(id, r1, time, unit);
}catch(Exception e){
throw new RuntimeException(e);
}finally{
unlock(lockKey);
}
});
return r;
}
private boolean tryLock(String key) {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "", 10, TimeUnit.SECONDS);
return Boolean.getBoolean(flag);
}
private void unlock(String key) {
redisTemplate.delete(key);
}