Redis 缓存的使用与优化
其他
2020-03-22 10:39:44
阅读次数: 0
缓存的使用与设计
- 缓存的收益与成本
- 缓存更新策略
- 缓存粒度控制
- 缓存穿透优化
- 缓存无底洞问题
- 缓存雪崩优化
- 热点key重建优化
- 缓存降级
缓存的收益与成本
收益
加速读写
降低后端负载
,例如使用 redis 降低 mysql 负载等。
成本
数据不一致
:缓存层和数据层有时间窗口不一致的情况,和更新策略有关。
代码维护成本
:多了一层缓存逻辑。
运维成本
:例如 Redis Cluster
使用场景
降低后端负载
,对高消耗的SQL比如JOIN结果集、分组统计结果缓存。
加速请求响应
,利用Redis/Memcache优化IO响应时间。
大量写合并为批量写
,如计数器现在Redis累加再批量写DB。
缓存更新策略
三种策略
- LRU/LFU/FIFO算法剔除
- 超时剔除,例如expire。
- 主动更新,代码层面控制生命周期
对比
策略 |
一致性 |
维护成本 |
LRU/LFU/FIFO剔除算法 |
最差 |
低 |
超时剔除 |
较差 |
低 |
主动更新 |
强 |
高 |
两条建议
- 对于一致性要求低的缓存:最大内存和淘汰策略,即直接往内存里扔,达到最大内存时直接淘汰
- 对于一致性要求高的缓存:超时剔除和主动更新结合,最大内存和淘汰策略兜底。
缓存粒度控制
粒度问题
- 比如要缓存一个mysql中的用户信息:
select * from user where id = {id}
。
- 设置用户缓存:
set user:{id} '(上面sql语句的结果)'
- 缓存粒度-全部属性:即缓存
select *
的结果
- 缓存粒度-部分重要属性:比如缓存
select user_name, phone
部分字段结果
三个角度
通用性
:比如加了个新的字段,全量属性最好。
占用空间
:部分属性更好。
代码维护
:表面上全量属性会更好。
缓存穿透问题
大量请求不命中
- 即查询结果一直返回 null ,对于不存在的值,缓存层也没有进行缓存
原因
- 业务代码自身问题
- 恶意攻击或者爬虫等等,比如根据一堆不存在的 id 进行查询,每次都是穿透缓存直接查询数据库。
如何发现
- 根据业务的响应时间
- 审查业务本身代码问题
- 相关指数:总调用数、缓存命中数、存储层命中数
解决方法一:缓存空对象
- 可能存在的问题
需要更多的键
,比如说恶意攻击,需要存一堆无用的 id 键,值为 null。需要使用过期时间,降低风险。
缓存层和存储层数据“短期”不一致
。比如查询mysql,这时候mysql出现问题了,导致返回的是 null,然后又把这个空数据缓存到了redis,假如有五分钟过期时间,那这五分钟内都是错误数据,这个时候就是数据不一致了。这种情况的处理可以比如订阅一些mysql服务的消息,监听到mysql服务器恢复正常,可以把缓存重新刷新一遍。
解决方法二:布隆过滤器拦截(后面文章解析)
缓存雪崩优化
问题描述
- 由于cache服务承载了大量请求,当cache服务异常/脱机,流量直接压向后端组件(例如DB),造成级联故障。
优化方案
保证缓存高可用性
,个别节点、个别机器或者个别机房出现问题时能保证高可用。
- 依赖隔离组件
为后端限流
,例如线程池、信号量隔离组件。
提前演练
,例如压力测试。
无底洞优化
问题描述
- 2010年,Facebook 有了3000个Memcache个节点。
- 发现,
加机器性能并没有提升,反而下降
。
- 问题关键点:
- 更多的机器不代表更高的性能
- 一般对于的是批量接口的需求,比如mget、mset等
- 数据增长和水平扩展需求矛盾,就是说数据增长也必须扩展节点以存储更多数据。
优化IO的几种方法
- 命令本身优化:例如慢查询keys、hgetall bigkey
- 减少网络通信的次数
- 降低接入成本:例如客户端长连接/连接池、NIO等
四种批量优化的方法
- 串行mget
- 串行IO
- 并行IO
- hash_tag
热点key重建优化
问题描述
三个目标
- 减少重建缓存的次数
- 数据尽可能一致
- 减少潜在的风险
两个解决思路
互斥锁
,当检测到缓存需要重建的时候加上一把锁,缓存成功了就把锁释放,在这个期间如果有其他线程访问缓存时会一直等待。
- 图示:
- 代码示例
String get(String key) throws InterruptedException {
String value = redis.get(key);
if (value == null) {
String mutexKey = "mutex:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
value = db.get(key);
redis.set(key, value);
redis.delete(mutexKey);
} else {
Thread.sleep(50);
get(key);
}
}
return value;
}
永远不过期
- 缓存层面:不设置过期时间
- 功能层面:为每个value添加逻辑过期时间,但发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
- 图示:
- 代码示例:
String get(final String key) throws InterruptedException {
V v = redis.get(key);
String value = v.getValue();
Long logicTimeout = v.getLogicTimeout();
if (logicTimeout >= System.currentTimeMillis()) {
String mutexKey = "mutex:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
threadPool.execute(new Runnable(){
@Override
public void run() {
String dbValue = db.get(key);
redis.set(key, (dbValue, newLogicTimeout));
redis.delete(mutexKey);
}
});
} else {
Thread.sleep(50);
get(key);
}
}
return value;
}
两种方案对比
方案 |
优点 |
缺点 |
互斥锁 |
思路简单、保证一致性 |
代码复杂度增加、存在死锁风险 |
永远不过期 |
基本杜绝热点key重建问题 |
不保证一致性、逻辑过期时间增加维护成本和内存成本 |
缓存降级
问题描述
- 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,
仍然需要保证核心服务还是可用的
,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
目的
降级的最终目的是保证核心服务可用,即使是有损的
。而且有些服务是无法降级的(如加入购物车、结算)。
如何处理
- 在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而
梳理出哪些必须誓死保护,哪些可降级
;比如可以参考日志级别设置预案:
- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级。
- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警。
- 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级。
- 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
发布了112 篇原创文章 ·
获赞 303 ·
访问量 2万+
转载自blog.csdn.net/qq_36221788/article/details/104805982