高可用Redis(十三):Redis缓存的使用和设计

1.缓存的受益和成本

1.1 受益

1.能够加速读写:Redis是基于内存的数据源,经过缓存加速数据读取速度
2.下降后端负载:后端服务器经过前端缓存下降负载,业务端使用Redis下降后端数据源的负载等

1.2 成本

1.数据不一致:后端数据源中的数据缓存到Redis,若是后端数据库中的数据被更新时,根据更新策略不一样,Redis缓存层中的数据和数据源的数据有时间窗口不一致
2.代码维护成本:多了一层缓存逻辑,之前只须要读取后端数据库,如今还须要维护缓存的读写以及Redis与数据库的链接等
3.运维成本:例如Redis Cluster

1.3 使用场景

1.下降后端负载:对高消耗的SQL,例如作排行榜的计算涉及到不少张数据表上数据的很复杂的实时计算,这种计算实际上没有任何意义,
    若是使用Redis缓存,只须要第一次把计算结果写入到Redis缓存中,后续的计算直接在Redis中就能够了,join结果集/分组统计结果进行缓存
2.加速请求响应:因为Redis中的数据是保存在内存中的,利用Redis能够显著的提升IO响应时间
3.大量写请求合并为批量写:如计数器先使用Redis进行累加,最后把结果批量写入到后端数据库中,而不用每次都更新到后端数据库,有效下降后端数据库的负载

2.缓存的更新策略

缓存中的数据有生命周期,须要按期更新和删除,保证内存空间的合理使用以及缓存数据的一致,缓存数据须要根据合理的数据更新策略更新缓存中的数据前端

  • LRU/LFU/FIFO算法剔除:Redis使用maxmemory-policy,即Redis中的数据占用的内存超过设定的最大内存时的操做策略
  • 超时剔除:对缓存的数据设置过时时间,超过过时时间自动删除缓存数据,而后再次进行缓存,保证与数据库中的数据一致
  • 主动更新:开发者控制key的更新周期,当key在后端数据库中发生更新时,向Redis主动发送消息,Redis接收到消息对key进行更新或删除

Redis的配置文件中定义了下面的缓存更新策略node

volatile-lru -> remove the key with an expire set using an LRU algorithm        # 根据LRU算法删除过时的key 
allkeys-lru -> remove any key according to the LRU algorithm                    # 根据LRU算法删除一些key
volatile-random -> remove a random key with an expire set                       # 随机删除一些设置了过时时间的key
allkeys-random -> remove a random key, any key                              # 从全部的key中随机删除一些key
volatile-ttl -> remove the key with the nearest expire time (minor TTL)     # 删除一些快过时的key
noeviction -> don't expire at all, just return an error on write operations # 不删除任何key,在向Redis写入key时返回一个错误,这将会占用更多的内存

须要注意的是:with any of the above policies, Redis will return an error on write operations, when there are no suitable keys for eviction。即在上面的六种策略中,若是没有key能够被删除时,向Redis中写入数据会返回一个error异常算法

LRU和最小TTL算法并非精确的算法,而是近似的算法(为了节省内存)。所以能够根据速度或准确性对其进行优化设置,使用maxmemory-samples选项来设置这个值数据库

默认状况下,maxmemory-samples的值设置为5,即Redis将检查5个键并选择使用最少的一个key,若是设置为10,很是接近真实的LRU算法,可是另外消耗一些的CPU。若是设置为3则会加快Redis,但执行结果不够准确。后端

缓存更新策略对比缓存

2.1 对于缓存的建议

  • 对数据一致性要求不高,即真实数据和缓存数据差异较大对业务影响不大状况下,能够采用最大内存和淘汰策略,内存使用量超过maxmemory-policy时,自动删除数据,而不会影响业务
  • 对数据一致性要求较高,即真实数据和缓存数据差异较大会影响业务状况下,能够采用超时剔除和主动更新结合策略,由最大内存和淘汰策略兜底。若是主动更新的功能出现问题失效,没有把一些没必要要的数据删除时,Redis占用的内存会愈来愈多,此时能够给一些有生命周期的key设置比较长的过时时间,而后设置maxmemorymaxmemory-policy,来保证Redis占用的内存超过设置的最大内存时删除一些过时的key,来保证Redis的高可用
  • 3.缓存粒度控制

上图中,使用Redis来作缓存,底层使用MySQL来作数据存储源,这种架构下大部分请求由Redis处理,少部分请求到达MySQL。服务器

从MySQL中获取一个用户的全部信息,而后缓存到Redis的数据结构中。网络

此时须要面对一个问题:缓存这个用户的全部数据信息,仍是缓存用户须要的用户信息字段。数据结构

能够从三个角度来考虑:多线程

3.1 通用性

从通用性角度考虑,缓存全量属性更好。

当用户数据表字段发生改变时,不须要修改程序就能够直接同步修改以后的用户信息到Redis缓存中供用户使用,可是用占用更多的内存空间

3.2 占用空间

从占用空间的角度考虑,缓存部分属性更好.

一样当用户数据表字段发生改变时而用户须要这个字段信息时,就须要修改程序源代码来把修改以后的用户信息同步缓存到Redis中,这种状况下占用的内存空间比全量属性占用的内存空间要少

3.3 代码维护

从代码维护角度考虑,表面上全量属性更好。

无论数据源中的数据表结构如何改变,都会把全部的数据同步到Redis缓存中,而不须要修改程序源代码,可是在大多数状况下,不会使用到全量数据,只须要缓存须要的数据就能够了,从内存空间消耗及性能方面考虑,使用部分属性更好

3.4 总结

选择缓存属性时,须要综合考虑缓存全量属性仍是部分属性

4.缓存穿透优化

4.1 什么叫缓存穿透

正常状况下,客户端从缓存中获取数据,若是缓存中没有用户请求须要的数据,就会读取数据源中的数据返回给客户端,同时把数据回写到缓存中。这样当下次客户端再请求这个数据时,就能够直接从缓存中获取数据而不须要通过数据库了。

若是客户端获取一个数据源中没有的key时,先从缓存中获取,获取结果为null,而后到数据源中获取,一样获取结果为null,这样全部的请求都会到达数据源,这就是缓存穿透的基本过程

缓存的存在就是为了保护数据源,缓存穿透以后会对数据源形成巨大的负载和压力,这就失去了缓存的意义。

4.2 缓存穿透的缘由

业务程序自身的问题:如没法对缓存进行回写等逻辑bug
恶意攻击,爬虫等

4.3 缓存穿透的发现

根据业务的响应时间来进行判断,当业务的响应时间远远过正常状况下的响应时间时,颇有可能就是缓存穿透形成的

能够经过监控一些指标:总调用数,缓存层命中数,存储层命中数等发现缓存穿透

4.4 缓存穿透解决办法

4.4.1 缓存空对象

缓存空对象是一种简单粗暴的解决方法

当数据源中没有用户请求须要的数据时,会请求数据源,以前的作法是数据源返回一个null,而缓存中并不作回写,缓存空对象的作法就是把null回写到缓存中,暂时解决缓存穿透带来的压力

缓存空对象会形成两个问题

1.若是是恶意攻击和爬虫等,若是每次请求的数据都不一致,缓存空对象时会在缓存中设置不少的key,即便这些key的值都为空值,也会占用不少的内存空间,此时能够为这个key设置过时时间来下降这样的风险

2.缓存空对象并设置过时时间,在这个时间内即便数据源恢复正常,请求获得的结果仍然是null,形成缓存层和存储层数据短时间不一致。这种状况下,能够经过订阅发布消息来解决,当数据源恢复正常时,会发布消息,而后把正常数据缓存到Redis中

4.4.2 布隆过滤器拦截

使用布隆过滤器能够经过占用很小的内存来对数据进行过滤

布隆过滤器拦截是把全部的key或者离散数据保存到布隆过滤器中,而后使用布隆过滤器在缓存层以前再作一层拦截。

若是请求没有被布隆过滤器拦截,则会到达缓存层获取须要的数据并返回,以达到实际效果

布隆过滤器对于固定的数据能够起到很好的效果,可是对于频繁更新的数据,布隆过滤器的构建会面临不少问题

4.4.3 缓存穿透解决办法对比

1.缓存空对象代码层面比较简单,可是须要一些额外的内存空间来保存空对象,并且会有短期内的数据不一致性
2.布隆过滤器须要特殊的使用场景,布隆过滤器须要维护一些单独的代码,并且布隆过滤器也会占用额外的不多的内存空间来实现数据的过滤

5.无底洞问题优化

5.1 无底洞问题描述

2010年,Facebook已经有了3000个Memcache节点,Facebook发现问题:"加"Memcache节点,客户端批量操做的效率不只没有提高,反而降低,这就是一个无底洞问题

5.2 无底洞问题关键点

当只有一个节点时,执行一次mget只产生一次网络IO;而当节点增长到3个时,使用顺序IO方式执行一次mget就会产生三次网络IO

同理,当节点愈来愈多,执行一次mget所须要的网络时间也愈来愈多,会对客户端的执行效率带来很大的降低

实际上网络IO因为扩容已经由原来的O(1)变成O(node)了,节点越多,并行执行一次mget命令所须要的时间就越长,若是串行执行mget命令所须要的时间就更多了。

无底洞问题关键点即:

  • 更多的机器 != 更多的性能
  • 批量接口需求(mget和mset等):在执行mget和mset等命令时会面对的问题
  • 数据增加与水平扩展需求等:随着业务量愈来愈大,对于缓存和数据源存储的需求也是愈来愈大,就须要对缓存和数据源进行扩容,即增长缓存节点和数据源节点,可是节点数量增多并不能带来性能的提高,这是一个矛盾的问题

    5.3 优化IO的方法

  • 优化命令自己:例如执行慢查询keys,hgetall bigkey等命令时,尽可能选择在缓存节点压力不大时执行
  • 减小网络通讯次数,例如执行mget命令由原来的O(n)次网络时间缩减为O(node)次网络时间,
  • 下降接入成本:例如客户端长链接/链接池,NIO等

    5.4 四种批量优化方法:

    5.4.1 串行mget

    串行mget须要n次网络时间

5.4.2 串行IO

因为客户端对key进行从新组装,因此把网络通讯时间下降到节点次O(node)

5.4.3 并行IO

并行IO也会在客户端对key进行从新组装,而后执行并行操做,所须要的网络时间为O(1)

5.4.4 hash_tag

hash_tag会把全部的key都分配到一个节点,可是使用这种方法会遇到各类问题

5.5 四种优化方案的优缺点分析

6.执行key重建优化

6.1 缓存重建过程描述

在正常状况下,客户端发送请求,会先到缓存,从缓存中获取须要的数据,若是缓存中并无须要数据,才会继续向数据源请求,从数据源中获取数据返回给客户端并回写到缓存中,这就是缓存的重建过程

6.2 缓存重建问题描述

若是重建的是一个热点key,用户访问量很是大。不少用户发送请求获取数据,执行线程从缓存中获取数据,可是此时缓存中并有这些数据,就会从数据源中获取数据,而后重建缓存。

当缓存重建完成,后续的访问才会直接读取缓存数制并返回

在这个过程当中,会有不少线程同时查询并重建缓存key,一方面会对数据源形成很大压力,另外一方面也会加大响应的时间

6.3 解决缓存重建的目标

减小缓存重建次数:不要屡次重建缓存
数据尽量一致:缓存中的数据要尽量与数据源中的数据保持一致
减小潜在风险:可能形成死锁或者线程池大量被夯住等状况

6.4 缓存重建解决方法

6.4.1 互斥锁(mutex key)

互斥锁是一种比较直观和简单的解决思路

第一个用户从缓存中获取数据,此时缓存中并无用户须要的数据,会从数据源中重建缓存,

用户在从数据源查询获取数据和重建缓存的过程当中加上一把锁,当重建缓存完成之后再把锁解开,并返回

当第二个用户也想从缓存中获取数据时,若是第一个用户重建缓存的过程尚未结束,即锁尚未被解开时,就会等待,一样后续访问的用户也通过这样一个过程

当缓存重建完成,锁被解开,全部的用户请求都从缓存中获取数据并输出

互斥锁解决了缓存大量重建的过程,可是在缓存重建的过程当中会有一个等待时间,大量线程被夯住,有可能形成死锁的状况

6.4.2 数据永不过时

在缓存层面,每个key都不设置过时时间(没有设置expire)
在功能层面中,为每一个value添加逻辑过时时间,一旦发现超过逻辑过时时间后,会使用单独的线程去构建缓存

须要注意的是

数据永不过时是一个异步的过程,即便缓存重建失败,也不会形成线程夯住的问题
数据永不过时基本杜绝了热点key的重建问题。
数据永不过时好处是:相比于使用互斥锁的方案,不会使用户产生一个等待的时间,并且能够保证只有一个线程来完成数据源的查询和缓存的重建
数据永不过时的缺点:在缓存重建完成以前,用户从缓存中获得的原来的数据有可能与从数据源中的新数据不一致的状况
数据永不过时中设置逻辑过时时间,会为每个key设置过时时间,会增长维护成本,占用更多的内存空间。

6.4 缓存重建解决方法对比

相关文章
相关标签/搜索