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的配置文件中定义了下面的缓存更新策略
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设置比较长的过期时间,然后设置maxmemory和maxmemory-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命令所需要的时间就更多了。