一、缓存特征

  一)命中率

  命中数/(命中数+没有命中数)

  二)最大元素(空间)

  代表缓存中可以存放的最大元素的数量,一旦缓存中元素的数量超过这个值,或者缓存数据所占的空间超过了最大支持的空间,将会触发缓存清空策略。根据不同的场景,合理设置最大元素(空间)的值,在一定程度上可以提高缓存的命中率,从而更有效的使用缓存。

  三)清空策略

  FINO(先进先出)、LFU(最少使用)、LRU(最近最少使用)、过期时间、随机等

  • FINO(先进先出):最先进入缓存的数据,在缓存空间不够或超出最大元素限制的情况下,会有限被清除掉,以腾出新的空间来接收新的数据。这种策略的算法主要是比较缓存元素的创建时间,在数据实时性较高的场景下,可以选择这种策略,优先保证最新策略可用。
  • LFU(最少使用):无论元素是否过期,根据元素的被使用次数来判断,清除使用次数最少的元素来释放空间。算法主要是比较元素的命中次数,在保证高频数据有效的场景下,可以选择这种策略。
  • LRU(最近最少使用):无论元素是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素,释放空间。算法主要是比较元素最近一次被获取的时间,在热点数据场景下,可以选择这种策略。
  • 过期时间:根据过期时间判断,清理过期时间最长的元素,或清理最近要过期的元素。

二、缓存命中率影响因素

 

  一)业务场景和业务需求

  缓存王往往适合读多写少的场景。业务需求对实时性的要求,直接回影响到缓存的过期时间和更新策略。实时性要求越低,就越适合缓存。在相同key和相同请求数的情况下,缓存的时间越长,命中率越高。

  二)缓存的设计(粒度和策略)

  通常情况下,缓存的粒度越小,命中率越高。缓存的更新和命中策略也会影响缓存的命中率,当数据发生变化时,直接更新缓存的值会比移除缓存或使缓存过期的命中率更高。

  三)缓存的容量和基础设施

  缓存的容量有限,则容易引起缓存失效和被淘汰(目前多数的缓存框架或中间件都采用了LRU算法)。同时,缓存的技术选型也是至关重要的,比如采用应用内置的本地缓存就比较容易出现单机瓶颈,而采用分布式缓存则容易扩展。所以需要做好系统容量规划,并考虑是否可扩展。此外,不同的缓存框架或中间,其效率和稳定性也是存在差异的。

  四)其他因素

  当缓存节点发生故障时,需要避免缓存失效并最大程度降低影响,这种特殊情况也是架构师需要考虑的。业内比较典型的做法是通过一致性hash算法,或者通过节点冗余的方式。

  误区:既然业务需求对数据时效性要求很高,而缓存时间又会影响到缓存命中率,那么系统就别用缓存了。其实这忽略了一个重要因素——并发。通常来讲,在相同缓存时间和key的情况下,并发越高,缓存的收益越高,即便缓存时间很短。

  提高缓存命中率的方法

  从架构师的角度,需要应用尽可能的通过缓存直接获取数据,并避免缓存失效。这也是比较考验架构师能力的,需要在业务需求、缓存力度、缓存策略、技术选型等各个方面去通盘考虑并做权衡。尽量的聚焦在高频访问且时效性要求不高的热点业务上,通过缓存预加热、增加存储容量、调整存储容量、调整缓存力度、更细缓存等手段来提高命中率。

三、缓存的使用与设计

  参考:​​http://carlosfu.iteye.com/blog/2269678​

  一)缓存的受益与成本

   1、受益

    1、加速读写
  • 通过缓存加速读写速度:CPU L1/L2/L3 Cache、Linux page Cache加速硬盘读写、浏览器缓存、Ehcache缓存数据库结果
    2、降低后端负载
  • 后端服务器通过前端缓存降低负载:业务端使用Redis降低后端MySQL负载等

  2、成本

  1. 数据不一致:缓存层与数据层有时间窗口不一致,和更新策略有关
  2. 代码维护成本:多了一层缓存逻辑
  3. 运维成本:例如Redis cluster

  3、使用场景

    1、降低后端负载
  • 对高消耗的SQL:join结果集/分组统计过缓存
    2、加速请求相应
  • 利用Redis/memcache优化IO响应时间
    3、大量写合并为批量写
  • 计数器先Redis累加再批量写DB

  二)缓存的更新策略

  1、缓存的更新策略主要有三种:

  1. LRU/LFU/FIFO算法剔除:例如maxmemory-plicy
  2. 超时剔除:例如expire(金融性信息不能使用)
  3. 主动更新:开发控制生命周期

  2、三种缓存的更新策略对比

  

缓存服务cache_缓存

  3、策略选择

  • 低一致性:最大内存和淘汰策略
  • 高一致性:超时剔除和主动更新相结合,最大内存和淘汰策略兜底。

  三)缓存粒度控制

  1、缓存粒度控制介绍

  

缓存服务cache_redis_02

    1、从MySQL获取用户信息
select * from user where id={id}
    2、设置用户信息缓存
set user:{id} `select * from user where id={id}`
    3、缓存粒度

  全部属性

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

  部分重要属性

set user:{id} `select importantColumn1,..importantColumnK from user where id={id}`

  2、缓存粒度控制-三个角度

  • 通用性:全量属性更好
  • 占用空间:部分属性更好
  • 代码维护:表面上全量属性更好

  综合考虑后,选择:大多数场景使用部分属性就行了。

  四)缓存穿透优化

  1、缓存穿透问题

  大量请求不命中,请求访问缓存层没有数据,请求直接打到存储层,这样就失去缓存的意义(保护存储层),会加大存储层的压力

  

缓存服务cache_缓存_03

  2、产生穿透的原因

  • 业务带包自身问题:业务逻辑有问题
  • 恶意攻击、爬虫等等:通过一些渠道知道业务url的规则或者不知道,强制访问触发到代码写的接口,接口触发到缓存,给的ID根本不在缓存范围内,必然出产生穿透问题

  3、如何发现

  • 业务的响应时间(时间更长(监控系统统计指标))
  • 业务本身问题
  • 相关指标:总调用数、缓存层命中数、存储层命中数

  4、解决方法

    1、缓存空对象

  

缓存服务cache_redis_04

  问题:

  1. 需要更多的键
  2. 缓存层和存储层数据“短期”不一致
    2、布隆过滤器拦截(适用于更新频繁的数据)

  

缓存服务cache_redis_05

 

  五)无底洞问题优化

  1、问题描述

  Facebook的工作人员反应2010年已达到3000个memcached节点,储存数千G的缓存。
  他们发现一个问题--memcached的连接效率下降了,于是添加memcached节点,添加完之后,并没有好转。称为“无底洞”现象

  2、问题关键点

  由于节点扩容,网络IO从o(1)变成了O(Node)

  具体解释:键值数据库或者缓存系统,由于通常采用hash函数将key映射到对应的实例,造成key的分布与业务无关,但是由于数据量、访问量的需求,需要使用分布式后(无论是客户端一致性哈性、redis-cluster、codis),批量操作比如批量获取多个key(例如redis的mget操作),通常需要从不同实例获取key值,相比于单机批量操作只涉及到一次网络操作,分布式批量操作会涉及到多次网络io。

  

缓存服务cache_缓存_06

  

缓存服务cache_redis_07

   

缓存服务cache_Redis_08

    1、问题关键点
  • 更多的机器!=更高的性能
  • 批量接口需求(mget,mset等操作)
  • 数据增长与水平扩展需求
    2、无底洞带来的危害
  • 客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着实例的增多,耗时会不断增大。
  • 服务端网络连接次数变多,对实例的性能也有一定影响。
     3、优化IO的几种方法
  1. 命令本身优化:例如慢查询keys、hgetall bigkey
  2. 减少网络通信次数:mget n次网络变成node次
  3. 降低接入成本:例如客户端长连接:连接池、NIO等
    4、总结
  • 用一句通俗的话总结:更多的机器不代表更多的性能,所谓“无底洞”就是说投入越多不一定产出越多。
  • 分布式又是不可以避免的,因为我们的网站访问量和数据量越来越大,一个实例根本坑不住,所以如何高效的在分布式缓存和存储批量获取数据是一个难点。

  六)缓存雪崩优化

  参考链接:​​http://carlosfu.iteye.com/blog/2249316​

  七)热点key重建优化

   1、问题描述

  热点key + 较长的重建时间

  

缓存服务cache_redis_09

  这个过程会有大量的线程做缓存重建,查询数据源

  2、三个目标

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

  3、两个解决

    1、互斥锁

  

缓存服务cache_Redis_10

  已解决问题:不需要大量重建缓存

  产生问题:有等待的过程,大量的线程夯住

    2、永远不过期
  1. 缓存层面:没有设置过期时间(没有expire)
  2. 功能层面:为每个value添加逻辑过期时间,但发现超过逻辑过期时间后,会使用单独的线程去构建缓存

  

缓存服务cache_缓存_11

   问题:会出现数据不一致的情况(没等待数据缓存完,就来取数据,会取到老的数据)

     3、两种方案对比

  

缓存服务cache_数据_12