redis学习记录(5)redis缓存
缓存的使用与设计
一、缓存的收益与成本
1、收益
①加速读写
②降低后端负载
2、成本
① 数据不一致:缓存层和数据层有时间窗口不一致,和更新策略有关。
②代码维护成本:多了一层缓存逻辑。
二、缓存更新策略
1、LRU/LFU/ FIFO 删除
2、超时剔除:例如expire
3、主动更新:开发控制生命周期
三、缓存粒度控制
一、什么是缓存粒度
下面这个图是很多项目关于缓存使用最常用的一个抽象,那么我们假设storage层为mysql, cache层为redis。
假如我现在需要对视频的信息做一个缓存,也就是需要对select * from video where id=?的每个id在redis里做一份缓存,这样cache层就可以帮助我抗住很多的访问量。
我们假设视频表有100个属性,那么问题来了,需要缓存什么维度呢,也就是有两种选择吧:
部分数据和全部数据
1. 两者的特点是显而易见的:
数据类型
通用性
空间占用(内存空间 + 网络码率)
代码维护
全部数据 高 大 简单 部分数据 低 小 较为复杂 2. 通用性
如果单从通用性上看、全部数据是最优秀的,但是有个问题就是是否有必要缓存全部数据,任务以后会有这样的需求,但是从经验上看除了非常重要的信息,那些不重要的字段基本不会再出现,也就是说着通用性,通常都是想象出来的。而且全部数据会占用大量空间。
3. 空间占用:部分属性更好。
4. 代码维护:
代码维护性,全部数据的优势更加明显,而部分数据一旦要加新字段就会修改代码,而且还需要对原来的数据进行刷新。
四、缓存穿透优化
缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,并且出于容错考虑, 如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
原因:
- 业务代码自身问题
- 恶意攻击。爬虫等等
如何发现:
- 业务的响应时间
- 业务本身问题
- 相关指标:总调用数、缓存层命中数、存储层命中数。
解决:解决思路大致有两个
①、缓存空对象
(1). 定义:如上图所示,当第②步MISS后,仍然将空对象保留到Cache中(可能是保留几分钟或者一段时间,具体问题具体分析),下次新的Request(同一个key)将会从Cache中获取到数据,保护了后端的Storage。
(2) 适用场景:数据命中不高,数据频繁变化实时性高
(3) 维护成本:代码比较简单,但是有两个问题:
第一是空值做了缓存,意味着缓存系统中存了更多的key-value,也就是需要更多空间,解决方法是我们可以设置一个较短的过期时间。
第二是数据会有一段时间窗口的不一致,假如,Cache设置了5分钟过期,此时Storage确实有了这个数据的值,那此段时间就会出现数据不一致,解决方法是我们可以利用消息或者其他方式,清除掉Cache中的数据。
②布隆过滤器拦截
如上图所示,在访问所有资源(cache, storage)之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,
五、无底洞问题优化
问题描述:2010年,Facebook有了3000个memcached节点,发现加 机器,性能并没有提升,反而下降。
当节点从一个增加到三个,执行mget命令需要三次网络时间,并且命令执行时间受最慢的那个节点的影响。
更多的机器!=更高的性能,所谓“无底洞”就是说投入越多不一定产出越多。
优化IO的几种方法
- 命令本身优化:例如慢查询keys、hgetall bigkey
- 减少网络通信次数
- 降低接入成本:例如客户端长连接/连接池等。
四种解决方案:
(1).串行mget
将Mget操作(n个key)拆分为逐次执行N次get操作, 很明显这种操作时间复杂度较高,它的操作时间=n次网络时间+n次命令时间,网络次数是n,很显然这种方案不是最优的,但是足够简单。
(2). 串行IO
将Mget操作(n个key),利用已知的hash函数算出key对应的节点,这样就可以得到一个这样的关系:Map<node, somekeys>,也就是每个节点对应的一些keys
它的操作时间=node次网络时间+n次命令时间,网络次数是node的个数,很明显这种方案比第一种要好很多,但是如果节点数足够多,还是有一定的性能问题。
(3). 并行IO
此方案是将方案(2)中的最后一步,改为多线程执行,网络次数虽然还是nodes.size(),但网络时间变为o(1),但是这种方案会增加编程的复杂度。
它的操作时间=1次网络时间+n次命令时间
(4). hash-tag实现。将所有的keys强制分配到一个节点。
四种方案比较:
六、缓存雪崩优化
1. 由于Cache层承载着大量请求,有效的保护了Storage层(通常认为此层抗压能力稍弱),所以Storage的调用量实际很低,所以它很爽。
2. 但是,如果Cache层由于某些原因(宕机、cache服务挂了或者不响应了)整体cache掉了,也就意味着所有的请求都会达到Storage层,所有Storage的调用量会暴增,所以它有点扛不住了,甚至会挂掉 。
优化:
- 保证Cache服务高可用性
例如redis sentinel、redis cluster。- 依赖隔离组件为后端限流
其实无论是cache或者是mysql, hbase, 甚至别人的API,都会出现问题,我们可以将这些视同为资源,作为并发量较大的系统,假如有一个资源不可访问了,即使设置了超时时间,依然会hang住所有线程,造成其他资源和接口也不可以访问。
例如推荐系统中,个性化推荐不能提供服务了,就降级补充热点数据。- 提前演练:例如压力测试。
七、热点key重建优化
我们通常使用 缓存 + 过期时间的策略来帮助我们加速接口的访问速度,减少了后端负载,同时保证功能的更新,一般情况下这种模式已经基本满足要求了。
但是有两个问题如果同时出现,可能就会对系统造成致命的危害:
(1) 这个key是一个热点key(例如一个重要的新闻,一个热门的八卦新闻等等),所以这种key访问量可能非常大。
(2) 缓存的构建是需要一定时间的。(可能是一个复杂计算,例如复杂的sql、多次IO、多个依赖(各种接口)等等)
因为缓存的构建是需要时间的,所以会出现一个问题:在缓存失效后,同时有大量线程重建缓存。
三个目标和两个解决:
- 减少重建缓存的次数
- 数据尽可能一致
- 减少潜在危险
两个解决:
- 互斥锁
就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了。
- 永不过期
(1)从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。
两种方案对比: