缓存能够有效地加速应用的读写速度,同时也可以降低后端负载,对日常用用的开发至关重要

缓存的收益和成本

image.png

缓存的收益

  • 加速度写
    因为缓存通常是全内存的(Redis、Memcache),存储层通常读写性能不够强悍(MySQL),通过缓存的使用可以有效地加速度写,优化用户体验
  • 降低后端负载
    帮助后端减少访问量和复杂计算(复杂SQL语句),在很大程度降低了后端的负载

缓存的成本

  • 数据不一致性
    缓存和存储层的数据存在着一定时间窗口的不一致性,时间窗口跟跟新策略有关
  • 代码维护成本
    加入缓存后,需要同时处理缓存层和存储层的逻辑,增大了开发者维护代码的成本
  • 运维成本
    以Redis Cluster为例,加入后无形中增加了运维成本

缓存的使用场景

  • 开销大的复杂运算
    以MySQL为例子,一些复杂的操作或者计算(列入大量联表操作、一些分组计算),如果不加缓存,不但无法满足高并发量,不但无法满足高并发量,同时也会给MySQL带来巨大的负担
  • 加速请求响应
    即时查询存储层数据足够快也可以使用缓存,Redis每秒可以完成数万次读写,并且提供的批量操作可以优化整个IO链的响应时间

缓存更新策略

  • 缓存通常是有生命周期的,在指定时间被删除或更新,保存缓存空间在一个可控的范围内
  • 缓存数据和数据源中的真实数据有一段时间窗口的不一致,需要进行更新

LRU/LFU/FIFO算法删除

使用场景

常用于缓存使用了超过了预设的最大值的时候,如何对现有的数据进行剔除

一致性

最差,因为要清理哪些数据是由具体算法决定,开发人员只能决定使用哪种算法

维护成本

不需要开发人员自己实现算法,只需要配置最大内存和对应策略即可

超时剔除

使用场景

业务可以容忍一段时间内,缓存层数据和存储层数据不一致,可以为其设置过期时间,在过期时间后会删除,再从帧数数据源获取数据,重新放到缓存并设置过期时间。但需要实时数据的交易方面等业务不适用

一致性

一段时间窗口内(取决于过期时间长短)存在一致性问题,即缓存数据和真实数据也的数据不一致

维护成本

维护成本不高,只设置expire过期时间即可,当然前提是应用放允许这段时间发生的数据不一致性

主动跟新

适用场景

对于数据一致性要求高,在真实数据跟新后立即缓存数据(可利用消息系统或者其他方式通知缓存更新)。

一致性

一致性最高,但如果主动更新发生问题,那么这条数据可能很长时间不会更新,所以和超时剔除一起使用

维护成本呢

维护成本比较高,需要开发者自己来完成判断,并保证跟新操作的正确性

最佳实践

低一致性业务

配置最大内存和淘汰策略的方式使用

高一致性业务

结合使用超时剔除和主动更新

缓存粒度控制

缓存层选用Redis,存储层选用MySQL。

缓存信息的粒度,是缓存全部属性还是只缓存部分重要属性

通用性

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

空间占用

缓存全部比部分占用更多的空间,且会存在以下问题

  • 造成内存浪费
  • 传输网络流量大,消耗相对大,极端情况下回阻塞网络
  • 数据的序列化和反序列化的CPU开销开销更大

代码维护

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

穿透优化

缓存穿透

定义

查询一个根本不存在的数据,缓存层和存储层都不会命中,通常处于容错的考虑,如果从存储层查不到数据则不写入缓存层

  • 过程:
    1.缓存层不明中
    2.存储层不命中,不将空结果写回缓存
    3.返回空结果

发现

缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储层的意义,由于后端存储不具备高并发性,甚至可能造成后端存储宕掉。可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就出现了缓存穿透问题

造成原因

  • 自身业务代码或者数据出现问题
  • 恶意攻击、爬虫等造成大量空命中

解决缓存穿透

缓存空对象

image.png
当存储层不命中后,仍然将空对象保留到缓存层,之后再访问这个数据将会从缓存中获取,即保护了后端数据源
1.空值做了缓存,缓存中有更多的键,需要更多的内存空间(如果是共攻击,问题更严重),可以针对这类数据设置一个较短的过期时间,让其自动剔除
2.缓存层和存储层的数据会有一段时间的窗口不一致,可能会对业务有一定影响,可以利用消息系统或者其他方式清除掉缓存层中的空对象

布隆过滤器

image.png
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。
可以将所有推荐的数据做成过滤器,如果过滤器确认所需要查询的数据主键ID不存在,则不访问存储层,在一定程度上保护了存储层
适用于数据命中不高、数据相对固定、实时性低(数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少

无底洞优化

无底洞现象

Facebook的Memcache节点为了满足业务要求,添加了大量新节点,但是发现性能不但没有好转反而下降了,这种现象称为缓存的“无底洞”现象
无论是Memcache还是Redis分布式,批量操作通常需要从不同节点上获取,相比于单机批量操作只涉及一次网络操作,分布式批量操作回设计多次网络时间

原因分析

分布式Redis

  • 客户端一次批量操作会涉及多次网络操作,批量操作会随着节点的增多,耗时会不断增大
  • 网络连接数变多,对节点的性能也有一定影响

总结

更多的节点不代表更高的性能,“无底洞”即投入越高不一定产出越多。但是分布式又是不可避免地,因为访问类和数据量越来越大,一个节点根本扛不住,所以如何高效地在分布式缓存中批量操作是一个难点

分布式优化批量操作

  • IO优化思路:
    1.命令本身的优化,例如优化SQL语句等
    2.减少网络通信次数
    3.降低接入成本,例如客户端使用长连接/连接池、NIO等

雪崩优化

雪崩现象

缓存层由于某些原因不能提供服务,所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况
image.png

预防和解决缓存雪崩问题

保证缓存层服务高可用性

通过Redis Sentinel和Redis Cluster把缓存设计成高可用,即使个别节点、机器甚至机房宕掉,依然可以提供服务

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

需要对重要的资源(Redis、MySQL、HBase、外部接口)都进行隔离,让每种资源都单独运行在自己的线程池中,即使个别资源出现问题,对其他服务没有影响。Hystrix是解决依赖隔离的利器,但只适用于Java应用

提前演练

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

热点key重建优化

“缓存+过期时间”基本可以满足绝大部分需求

发生原因

  • 当前key是一个热点key(热门的娱乐新闻),并发量大
  • 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂SQL、多次IO、多个依赖等。缓存失效的瞬间有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用奔溃

解决办法

  • 减少重建缓存的次数
  • 数据尽可能一致
  • 较少的潜在危险

互斥锁(mutex key)

只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新获取缓存数据即可
使用Redis的setnx命令实现

永远不过期

  • 缓存层面:不设置过期时间,不会出现热点key过期后产生的问题
  • 功能层面:为每个value设置一个逻辑过期时间(并没有正真设置过期时间),当超过该时间会使用当度的线程去构建缓存

这个家伙很懒,啥也没有留下😋