缓存的使用和设计
缓存的收益与成本
收益
加速读写
- 通过缓存加速读写:CPU L1/L2/L3 Cache,浏览器缓存,Ehcache缓存数据库结果
降低后端负载
- 后端服务器通过前端缓存降低负载:业务端使用Redis降低后端MySQL负载
成本
数据不一致
- 缓存层和数据层有时间窗口不一致,和更新策略有关
代码维护成本:多了一层缓存逻辑
运维成本:Redis Cluster
使用场景
降低后端负载
- 用于高消耗的SQL:join结果集/分组统计结果
加速请求响应
- 利用Redis/Memcache优化IO时间
大量写合并为批量写
- 计数器线Redis累加再批量更新到后端数据库
缓存更新策略
LRU/LFU/FIFO算法剔除:例如maxmemory-policy
超时剔除:例如expire
主动更新:开发控制生命周期
- 推荐结合剔除,超时,主动更新三种方案完成
三种策略比较
| 策略 | 一致性 | 维护成本 |
| —————- | —— | ——– |
| LRU/LIRS算法剔除 | 最差 | 底 |
| 超时剔除 | 较差 | 低 |
| 主动更新 | 强 | 高 |
TIPS
低一致性:最大内存和淘汰策略
高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底
缓存粒度控制
什么是缓存粒度
从MySQL获取用户信息
select * from usr where id={id}
设置用户信息缓存
- ```set usr:{id} `select * from usr where id={id}````
缓存粒度
- 部分重要属性
```set usr:{id} `select * from usr where id={id}````
- 全部属性
```set usr:{id} `select * from usr where id={id}````
缓存粒度控制
通用性:全量属性更好
占用空间:部分属性更好
代码维护:综合考虑,是否使用这么多属性
缓存穿透优化
缓存穿透:大量请求不命中
大量没有结果的请求通过cache访问到后端,后端也没有命中
原因
业务代码,没有正确从后端拿到数据
恶意攻击,爬虫{大量请求携带未知数据去访问缓存以及数据库}
及时发现
业务的相应时间
业务的本身问题
监控几个指标
总调用数
缓存层命中数
存储层命中数
解决方法
缓存空对象
如果从后端数据库中的请求结果是一个空值,我们也保存,不过设置一个过期时间(有可能后端数据库故障或者接口故障),这样减小后端数据库的压力
问题:
1). 需要更多的键(设置过期时间解决)
2). 缓存层和存储层数据“短期”不一致(订阅故障消息解决)
伪代码
public String getPassThrough(String key){
String cacheValue = cache.get(key);
if(StringUtils.isBlank(cacheValue)){
String storgeValue = storage.get(key)//如果cache中为空,就从storage中拿数据
cache.set(key,storageValue);
if(StringUtils.isBlank(storageValue)){
cache.expire(key,60*5);//如果从后端接口获取值为空,设置一个过期时间
}
return storageValue;
}else{
return cacheValue;
}
}
布隆过滤器
数据很大不能做到实时
利用算法,可以使用很小的内存判断一个值是否在一个大数据集中
在请求cache之前先通过bloom filter过滤一次,判断请求是否有效
缓存无底洞:节点增加,性能下降
原因
更多的机器!=更高的性能
批量接口需求(mget,mset)等(节点增加,io时间增加)
数据增长与水平扩展需求
优化
命令优化:例如慢查询keys,hgetall
减少网络通信次数
降低接入成本:例如客户端长连接/连接池.NIO
优化方案比较
| 方案 | 优点 | 缺点 | 网络IO |
| ——– | ———————————— | ——————————————– | —————– |
| 串行mget | 编程简单少量keys满足需求 | 大量keys请求延迟严重 | O(keys) |
| 串行IO | 编程简单少量节点满足需求 | 大量node延迟严重 | O(nodes) |
| 并行IO | 利用并行特性延迟取决于最慢的节点 | 编程复杂超时定位问题难 | O(max_slow(node)) |
| hash_tag | 性能最高 | 读写增加tag维护成本tag分布易出现数据倾斜 | O1 |
缓存雪崩
缓存集中过期或者缓存服务器宕机
缓存集中过期
在某一时间段,缓存集中过期失效,访问压力会给到后端数据库
为不同的分类设置不同的过期时间
同一分类的不同商品在设置过期时间时加一个随机因子
根据请求数量和密度设置过期时间
服务器宕机
缓存层实现高可用
客户端降级
提前演练
热点key重建优化
原因
热点key在多次访问时,线程一直在做查询数据源,重建缓存的操作
例如微博热搜
优化目标
减少重缓存的次数
数据尽可能一致
减少潜在危险
优化思路
互斥锁
在查询数据源和重建缓存这个过程中加锁,如果有线程在执行这个操作,其他线程只能等待缓存重建完毕
- 伪代码:
String get(String key){
String value = redis.get(key);
if(value == null){
String mutexKey = "mutex🔑" + key;
if(redis.set(mutexKey,"1","ex 180","nx")){
value = db.get(key);
redis.set(key,value);
redis.delete(mutexKey);
}else{
//其他线程休息50ms
Thread.sleep(50);
get(key);
}
}
return value;
}
永不过期
缓存:没有加expire
功能层面:为每个value添加逻辑过期时间,如果发现超过逻辑过期时间,使用单独的线程去构建缓存
我们的key永不过期,线程获取缓存不需要等待,如果中间发现value的过期时间到了,就新开一个线程去更新key。在更新完成前所有的请求获取得到的都是更新前的旧值,知道更新完成后,才会得到新值
- 伪代码
String get(final String key){
V v= redis.get(key);
String value = v.getValue();
long logicTimeout = v.getLogicTimeout();
if(logicTimeout >= System.currentTimeMills()){
String mutexKey = "mutex🔑" + key;
if(redis.set(mutexKey,"1","ex 180","nx")){
//异步更新
threadPool.execute(new Runnable(){
public void run(){
String dbValue = db.get(Key);
redis.set(key,(dbValue,newLogicTimeout));
redis.delete(muteKey);
}
});
}
}
return value;
}
两种方案对比
| 方案 | y优点 | 缺点 |
| ———- | ————————- | ————————————————– |
| 永远不过期 | 基本杜绝热点key重建按问题 | 不保证一致性逻辑过期时间增加维护成本和内存成本 |
| 互斥锁 | 思路简单保证一致性 | 代码复杂度增加存在死锁的风险 |
Comments