文章目录

  • 概述
  • 缓存收益和成本
  • 收益:
  • 成本:
  • 缓存更新策略的选择和使用场景
  • LRU/LFU/FIFO算法剔除:
  • 超时剔除:
  • 主动更新:
  • 缓存粒度控制方法
  • 通用性:
  • 空间占用:
  • 代码维护
  • 穿透问题优化
  • 缓存空对象
  • 布隆过滤器拦截
  • 两种方案对比:
  • 无底洞问题优化
  • 血崩问题优化
  • 保证缓存层服务高可用性
  • 依赖隔离组件为后端限流并降级
  • 提前演练
  • 热点Key重建优化
  • 互斥锁(mutex key)
  • 永远不过期
  • 参考:《redis开发与运维》


概述

我们大家都知道在我们的应用中加入缓存可以有效的加速运用的读写速度,同时还可以降低DB的负载,对于我们的日常应用的开发至关重要。其实任何一个东西都具有它的两面性,也就是说我们加了缓存以后也会给我们的应用带来一个的问题,具体问题如下:

缓存收益和成本

收益:

  • 加速读写

因为缓存通常都是全内存的(例如:redis、memcache),而存储层一般都是物理磁盘,通过缓存可以有效的加速读写,优化用户体验

  • 降低后端的负载

帮助后端减少访问量和复杂计算,很大程度上减少了后端的负载

成本:

  • 数据不一致

缓存层和数据层的数据存在着不一定时间窗口的不一致行,时间窗口跟更新策略有关系

  • 代码维护成本增加

加入了缓存后,需要同时处理缓存层和存储层的逻辑,增加了开发者维护代码的成本。

  • 运维成本增加

以Redis Cluster为例,加入后无行中增加了运维的成本。

缓存更新策略的选择和使用场景

LRU/LFU/FIFO算法剔除:

使用场景:
剔除算法通常用于缓存使用过量超过了预设的最大值的时候,如何对现有的数据进行剔除。例如:Redis使用了maxmemory-policy 这个配置作为内存最大值后对于数据的剔除策略
一致性:
要清理哪些数据是由具体的算法决定的,开发人员只能决定使用哪种算法,所以数据一致性是最差的。
维护成本:
算法不需要开发人员自己来实现,通常只需要配置最大maxmemory 和对应策略即可。开发人员只需要知道每种算法的含义,选择自己合适的就可了。

超时剔除:

使用场景:
超时剔除通过给缓存数据设置过期时间,让其在过期时间后自动剔除,例如Redisexpire命令。如果业务可以容忍一段时间内,缓存层数据和存储层数据不一致,那么可以设置过期时间。在数据过期后,再从真实数据源获取数据,重新放到缓存并设置过期时间。例如:一个视频的描述信息,可以容忍几分钟内数据不一致,但是涉及交易方面的业务后果就不堪设想了。
一致性:
一段时间窗口内(取决于过期时间长短)存在不一致问题,也就是缓存数据和真是数据源的数据不一致
维护成本:
维护成本不是很高,只需要设置expire过期时间即可,当然前提是应用方允许这段时间可能发生的数据不一致现象。

主动更新:

使用场景:
应用方对于数据的一致性要求高,需要在真实的数据更新后,立即更新缓存数据。例如:可以利用消息系统或者其他方式通知缓存更新。
一致性:
一致性最高,但如果主动更新发生了问题,那么这条数据很可能很长时间不会更新,所以建议结合超时剔除一起使用效果会更好
维护成本:
维护成本比较高,开发者需要自己来完成更新,并保证更新操作的正确性。

缓存粒度控制方法

缓存策略 redis redis缓存设计思路_缓存策略 redis


示例:
现在需要将Mysql中的用户信息使用redis进行缓存,可以执行如下操作:
①从Mysql获取用户信息:

select * from user where id = {id};

②将用户信息缓存到redis中:(缓存的是全部列)

set user:{id} 'select * from user where id={id}'

③缓存部分重要列:

set user:{id} 'select {importantColumn1},{importantColumn2},{importantColumn3} from user where id={id}'

上述的这些问题就是缓存的粒度问题,那么我们究竟应该选择缓存全部还是部分呢?下面我们通过这三方面综合来考虑:通用性、空间占用、代码维护

通用性:

缓存全部数据比部分数据更加通用,但从实际经验来看,很长时间内应用只需要几个重要的属性。

空间占用:

缓存全部数据比部分数据要占用更多的内存空间:
①全部数据会造成内存的浪费
②全部数据可能每次传输产生的网络流量会比较大,耗时相对较大,在极端情况下还有可能造成网络阻塞

代码维护

全部数据的优化更加明显,而部分数据一旦要加新字段需要修改业务代码,而且修改后还要刷新缓存数据。

缓存策略 redis redis缓存设计思路_数据_02

穿透问题优化

定义:
缓存穿透是指查询了一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层中查不到的数据则不写入缓存层;而缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
危害:
①使得后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕机。通常可以在程序中分别统计总调用次数、缓存命中数、存储层命中数,如果发现大量存储层命中,可能就是出现了缓存穿透的问题。
造成穿透的原因:
①自身业务代码或者数据出现问题
②一些恶意攻击、爬虫等造成大量空命中

缓存空对象

当存储层中没有命中后,仍然将空对象保留到缓存层中,之后再访问这个数据就从缓存中获取,这样一来就保护了后端数据源
**保存空对象存在的问题:
①空值做了缓存,意味着缓存层中存在了更多的键,需要更多的内存空间(如果是攻击,问题将会更严重),有效的方法是:针对这类数据设置一个较短时间窗口的过期时间,让其自动剔除。 ②缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定的影响。例如:过期时间设置为5分钟,如果此时存储层添加了这个数据,那么在这段时间里就会出现缓存层和存储层数据的不一致,此时我们可以利用消息系统或其他方式清除掉缓存中的空对象。

布隆过滤器拦截

缓存策略 redis redis缓存设计思路_数据_03


缓存策略 redis redis缓存设计思路_使用场景_04

两种方案对比:

缓存策略 redis redis缓存设计思路_使用场景_05

无底洞问题优化

缓存策略 redis redis缓存设计思路_使用场景_06

血崩问题优化

**定义:
由于缓存层承载着大量的请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务,或者是缓存中的数据大面积同时过期【将过期时间都错开,避免设置相同的过期时间】,于是所有的请求都会到达存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。
**

保证缓存层服务高可用性

**Redis Sentinel 和 Redis Cluster **

依赖隔离组件为后端限流并降级

缓存策略 redis redis缓存设计思路_使用场景_07

Hystrix:是解决依赖隔离的利器

提前演练

项目上线前,演练缓存层宕机后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定。

热点Key重建优化

互斥锁(mutex key)

此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。

永远不过期

**这里的不过期有两层含义:
①从缓存层面来看:确实没有设置过期时间,所以不会出现热点key过期的问题,也就是物理不过期。
②从功能层面来看:为每个value设置一个逻辑过期时间,当发现查过逻辑过期时间后,会使用单独的线程去构建缓存。

缓存策略 redis redis缓存设计思路_使用场景_08

参考:《redis开发与运维》