Redis第六章之缓存设计
文章目录
缓存收益
- 加速读写 (数据不一致、代码维护成本增加、运维成本)
- 降低后端负载
注:MongoDB的按需物化视图也可以看成是一种缓存。
缓存的使用场景
- 开销大的复杂计算
- 加速请求响应
- 数据库代理(个人看法)
缓存更新策略
LRU/LFU/FIFO算法剔除
redis使用maxmemory-policy这个配置作为内存达到最大值时对数据剔除策略,默认是不剔除。
一致性:要清理哪些数据是由具体算法决定,开发人员只能决定使用哪种算法,所以数据的一致性是最差的。
维护成本: 算法不需要开发人员自己来实现,通常只需要配置最大maxmemory和对应的策略即可。开发人员只需要知道每种算法的含义,选择适合自己的算法即可。
超时剔除
给数据设置过期时间,redis提供expire命令来设置。如果业务可以容忍一段时间内,缓存层数据和存储层数据不一致,可以使用超时剔除。
一致性: 一段时间窗口内(取决于过期时间长短)存在一致性问题,即缓存数据和真实数据源的数据不一致。
维护成本: 维护成本不是很高,只需设置expire过期时间即可,当然前提是应用方允许这段时间可能发生的数据不一致。
主动更新
使用场景应用方对于数据的一致性要求高,需要在真实数据更新后,立即更新缓存数据。
一致性:一致性最高,但如果主动更新发生了问题,那么这条数据很可能很长时间不会更新,所以建议结合超时剔除一起使用效果会更好。
维护成本:维护成本会比较高,开发者需要自己来完成更新,并保证更新操作的正确性。
最佳实践建议
低一致性的业务数据配置最大内存和淘汰策略的方式。不设置过期,定期更新。维护成本低。
给数据设置过期时间,用户如果访问一次,重置一次过期时间。当key过期的时候,访问数据库,并缓存,重置过期时间。(这样的话如果早上数据A是热点数据,数据B过期,晚上B是热点数据,第一次获取时需要去数据库获取)
高一致性业务可以结合使用超时剔除和主动更新,这样即使主动更新出了问题,也能保证数据过期时间后删除脏数据。
缓存粒度控制
通用性:缓存全部数据比部分数据更加通用,但从实际经验看,很长时间内应用只需要几个重要的属性。
空间占用:缓存全部数据要比部分数据占用更多的空间,可能存在以下问题:
- 全部数据会造成内存的浪费。
- 全部数据可能每次传输产生的网络流量会比较大,耗时相对较大,在极端情况下会阻塞网络。
- 全部数据的序列化和反序列化的CPU开销更大。
代码维护:全部数据的优势更加明显,而部分数据一旦要加新字段需要修改业务代码,而且修改后通常还需要刷新缓存数据。
穿透优化
缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中。
整个过程分为三个步骤:
- 缓存层不命中
- 存储层不命中,不将空结果写回缓存(缓存空对象就是在这一步将对应的键缓存为空,下次再访问这个键的时候redis直接告诉他不存在,不用去存储层找了)
- 返回结果结果为空
缓存空对象
导致缓存中key的数量增加,如果是恶意攻击redis将会很快爆满,所以需要针对这类数据进行过期设定,如果存储层在添加了这个数据,此时可以利用消息系统或代码清除掉缓存层中的空对象。
String get(String key) {
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
// 缓存
cache.set(key, storageValue);
// 如果存储数据为空,需要设置一个过期时间(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 返回缓存
return cacheValue;
}
}
布隆过滤器
一个推荐系统有4亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。
这种方法适用于数据命中不高、数据相对固定、实时性低
由布隆过滤器衍生的过滤方式(百万数据内可以使用)
将有存在的数据,将其ID存储在集合中,定期更新,数据访问前先查询该集合,如果存在则继续访问缓存层->存储层,如果不存在则返回空。
这样将会导致新增加的数据不能访问,需要更新后才可以查询到。
优化方法:在新增数据时将该数据的ID推送到集合中。
无底洞优化(节点太多而带来的性能下降)
更多的节点不代表更高的性能,所谓“无底洞”就是说投入越多不一定产出越多。但是分布式又是不可以避免的,因为访问量和数据量越来越大,一个节点根本抗不住,所以如何高效地在分布式缓存中批量操作是一个难点。
以Redis批量获取n个字符串为例,有三种实现方法:
- 客户端n次get:n次网络+n次get命令本身。
- 客户端1次pipeline get:1次网络+n次get命令本身。
- 客户端1次mget:1次网络+1次mget命令本身。
四种分布式批量操作:
串行命令
遍历命令,逐个执行,操作时间=n次网络时间+n次命令时间
串行IO
在客户端上计算key的散列值,对同一节点上的key进行归类,对每个节点执行mget或者pipeline操作。操作时间=node次网络时间+n次命令时间。如果节点越多,操作时间越长。
并行IO
多线程分类发送批量命令,这种方案会增加编程的复杂度。操作时间=max_slow(node网络时间)+n次命令时间
hash_tag实现
将多个key强制分配到一个节点上,操作时间 = 1次网络时间+n次命令时间。
缺点:
- 维护成本高
- 可能会出现数据倾斜,就是某个节点的数据量远远比其他节点大
没有最好的方案,只有最适合的方案
雪崩优化
雪崩是指大量热点数据同一时间过期或者缓存死机导致存储层数据库访问量暴增,造成存储层也会级联死机的情况。
预防和解决缓存雪崩问题,可以从以下几个方面进行着手。
-
保证缓存层服务高可用性。
-
数据有效期策略调整。
a.根据业务数据有效期进行分类错峰,A类90分钟,B类80分钟,C类70分钟;
b.过期时间使用固定时间+随机值的形式,稀释集中到期的key的数量;
-
超热数据使用永久key。
-
提前演练。
总结
- 缓存的使用带来的收益是能够加速读写,降低后端存储负载。
- 缓存的使用带来的成本是缓存和存储数据不一致性,代码维护成本增大,架构复杂度增大。
- 比较推荐的缓存更新策略是结合剔除、超时、主动更新三种方案共同完成。
- 穿透问题:使用缓存空对象和布隆过滤器来解决,注意它们各自的使用场景和局限性。
- 无底洞问题:分布式缓存中,有更多的机器不保证有更高的性能。有四种批量操作方式:串行命令、串行IO、并行IO、hash_tag。
一致性,代码维护成本增大,架构复杂度增大。 - 比较推荐的缓存更新策略是结合剔除、超时、主动更新三种方案共同完成。
- 穿透问题:使用缓存空对象和布隆过滤器来解决,注意它们各自的使用场景和局限性。
- 无底洞问题:分布式缓存中,有更多的机器不保证有更高的性能。有四种批量操作方式:串行命令、串行IO、并行IO、hash_tag。
- 雪崩问题:缓存层高可用、调整过期策略、超热数据永不过期、提前演练是解决雪崩问题的重要方法。