主题
《面渣逆袭》Redis 篇 · 第 4/8 章。原版 PDF(下载 / 打印)
26.什么是缓存击穿、缓存穿透、缓存雪崩?
PS: 这是多年黄历的老八股了,一定要理解清楚。
缓存击穿
一个并发访问量比较大的k ey 在某个时间过期,导致所有的请求直接打在DB上。

解决方案:
- 加锁更新,比如请求查询A ,发现缓存中没有,对A 这个k ey 加锁,同时去数据库查询数据,写入
缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。

- 将过期时间组合写在value 中,通过异步的方式不断的刷新过期时间,防止此类现象。
缓存穿透
缓存穿透指的查询缓存和数据库中都不存在的数据,这样每次请求直接打到数据库,就好像缓存不存在一样。

缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
缓存穿透可能会使后端存储负载加大,如果发现大量存储层空命中,可能就是出现了缓存穿透问题。
缓存穿透可能有两种原因:
自身业务代码问题
恶意攻击,爬虫造成空命中
它主要有两种解决办法:
缓存空值/ 默认值
一种方式是在数据库不命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库。

缓存空值有两大问题:
- 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严
重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
- 缓存层和存储层的数据会有一段时间窗又的不一致,可能会对业务有一定影响。
例如过期时间设置为5 分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致。
这时候可以利用消息队列或者其它异步方式清理缓存中的空对象。
布隆过滤器
除了缓存空对象,我们还可以在存储和缓存之前,加一个布隆过滤器,做一层过滤。
布隆过滤器里会保存数据是否存在,如果判断数据不不能再,就不会访问存储。

两种解决方案的对比:

缓存雪崩
某一时刻发生大规模的缓存失效的情况,例如缓存服务宕机、大量k ey 在同一时间过期,这样的后果就是大量的请求进来直接打到DB上,可能导致整个系统的崩溃,称为雪崩。

缓存雪崩是三大缓存问题里最严重的一种,我们来看看怎么预防和处理。
提高缓存可用性
- 集群部署:通过集群来提升缓存的可用性,可以利用Redis 本身的RedisCluster 或者第三方集群
方案如Codis 等。
- 多级缓存:设置多级缓存,第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都
不同。
过期时间
- 均匀过期:为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过
期时间太过集中。
- 热点数据永不过期。
熔断降级
- 服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访
问缓存系统。
- 服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对
一些非核心的交叉和数据的请求,而直接返回一个提前准备好的 fallback (退路)错误处理信息。
27.能说说布隆过滤器吗?
布隆过滤器,它是一个连续的数据结构,每个存储位存储都是一个bit,即0或者1, 来标识数据是否存在。
存储数据的时时候,使用K 个不同的哈希函数将这个变量映射为b it 列表的的K 个点,把它们置为1 。

我们判断缓存k ey 是否存在,同样,K 个哈希函数,映射到b it 列表上的K 个点,判断是不是1 :
如果全不是1 ,那么k ey 不存在;
如果都是1 ,也只是表示k ey 可能存在。
布隆过滤器也有一些缺点:
它在判断元素是否在集合中时是有一定错误几率,因为哈希算法有一定的碰撞的概率。
不支持删除元素。
28.如何保证缓存和数据库数据的一致性?
根据CAP 理论,在保证可用性和分区容错性的前提下,无法保证一致性,所以缓存和数据库的绝对一致是不可能实现的,只能尽可能保存缓存和数据库的最终一致性。
选择合适的缓存更新策略
1. 删除缓存而不是更新缓存
当一个线程对缓存的k ey 进行写操作的时候,如果其它线程进来读数据库的时候,读到的就是脏数据,产生了数据不一致问题。
相比较而言,删除缓存的速度比更新缓存的速度快很多,所用时间相对也少很多,读脏数据的概率也小很多。

- 先更数据,后删缓存
先更数据库还是先删缓存?这是一个问题。
更新数据,耗时可能在删除缓存的百倍以上。在缓存中不存在对应的k ey ,数据库又没有完成更新的时候,如果有线程进来读取数据,并写入到缓存,那么在更新成功之后,这个k ey 就是一个脏数据。
毫无疑问,先删缓存,再更数据库,缓存中k ey 不存在的时间的时间更长,有更大的概率会产生脏数据。

目前最流行的缓存读写策略cache-aside-pattern 就是采用先更数据库,再删缓存的方式。
缓存不一致处理
如果不是并发特别高,对缓存依赖性很强,其实一定程序的不一致是可以接受的。
但是如果对一致性要求比较高,那就得想办法保证缓存和数据库中数据一致。
缓存和数据库数据不一致常见的两种原因:
缓存k ey 删除失败并发导致写入了脏数据

消息队列保证k ey 被删除
可以引入消息队列,把要删除的k ey 或者删除失败的k ey 丢尽消息队列,利用消息队列的重试机制,重试删除对应的k ey 。

这种方案看起来不错,缺点是对业务代码有一定的侵入性。
数据库订阅+ 消息队列保证k ey 被删除
可以用一个服务(比如阿里的 canal )去监听数据库的binlog ,获取需要操作的数据。
然后用一个公共的服务获取订阅程序传来的信息,进行缓存删除操作。

这种方式降低了对业务的侵入,但其实整个系统的复杂度是提升的,适合基建完善的大厂。
延时双删防止脏数据
还有一种情况,是在缓存不存在的时候,写入了脏数据,这种情况在先删缓存,再更数据库的缓存更新策略下发生的比较多,解决方案是延时双删。
简单说,就是在第一次删除缓存之后,过了一段时间之后,再次删除缓存。

这种方式的延时时间设置需要仔细考量和测试。
设置缓存过期时间兜底
这是一个朴素但是有用的办法,给缓存设置一个合理的过期时间,即使发生了缓存数据不一致的问题,它也不会永远不一致下去,缓存过期的时候,自然又会恢复一致。
29.如何保证本地缓存和分布式缓存的一致?
PS: 这道题面试很少问,但实际工作中很常见。
在日常的开发中,我们常常采用两级缓存:本地缓存+ 分布式缓存。
所谓本地缓存,就是对应服务器的内存缓存,比如Caffeine ,分布式缓存基本就是采用Redis 。
那么问题来了,本地缓存和分布式缓存怎么保持数据一致?

Redis 缓存,数据库发生更新,直接删除缓存的k ey 即可,因为对于应用系统而言,它是一种中心化的缓存。
但是本地缓存,它是非中心化的,散落在分布式服务的各个节点上,没法通过客户端的请求删除本地缓存的k ey ,所以得想办法通知集群所有节点,删除对应的本地缓存k ey 。

可以采用消息队列的方式:
- 采用Redis 本身的P ub/Sub 机制,分布式集群的所有节点订阅删除本地缓存频道,删除Redis 缓存
的节点,同事发布删除本地缓存消息,订阅者们订阅到消息后,删除对应的本地k ey 。
但是Redis 的发布订阅不是可靠的,不能保证一定删除成功。
引入专业的消息队列,比如RocketMQ ,保证消息的可靠性,但是增加了系统的复杂度。
设置适当的过期时间兜底,本地缓存可以设置相对短一些的过期时间。
30.怎么处理热key?
什么是热K ey ?
所谓的热k ey ,就是访问频率比较的k ey 。
比如,热门新闻事件或商品,这类k ey 通常有大流量的访问,对存储这类信息的 Redis 来说,是不小的压力。
假如Redis 集群部署,热k ey 可能会造成整体流量的不均衡,个别节点出现OPS 过大的情况,极端情况下热点k ey 甚至会超过 Redis 本身能够承受的OPS 。
怎么处理热k ey ?

对热k ey 的处理,最关键的是对热点k ey 的监控,可以从这些端来监控热点k ey:
- 客户端
客户端其实是距离k ey “ 最近” 的地方,因为Redis 命令就是从客户端发出的,例如在客户端设置全局字典(k ey 和调用次数),每次调用Redis 命令时,使用这个字典进行记录。
- 代理端
像Tw emproxy 、Codis 这些基于代理的Redis 分布式架构,所有客户端的请求都是通过代理端完成的,可以在代理端进行收集统计。
- Redis 服务端
使用monitor 命令统计热点k ey 是很多开发和运维人员首先想到,monitor 命令可以监控到Redis执行的所有命令。
只要监控到了热k ey ,对热k ey 的处理就简单了:
把热k ey 打散到不同的服务器,降低压力
加入二级缓存,提前加载热k ey 数据到内存中,如果redis 宕机,走内存查询
31.缓存预热怎么做呢?
所谓缓存预热,就是提前把数据库里的数据刷到缓存里,通常有这些方法:
直接写个缓存刷新页面或者交叉,上线时手动操作
数据量不大,可以在项目启动的时候自动进行加载
定时任务刷新缓存.
32.热点key重建?问题?解决?
开发的时候一般使用“ 缓存+ 过期时间” 的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。
但是有两个问题如果同时出现,可能就会出现比较大的问题:
当前k ey 是一个热点k ey (例如一个热门的娱乐新闻),并发量非常大。
重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL 、多次I O 、多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
怎么处理呢?
要解决这个问题也不是很复杂,解决问题的要点在于:
减少重建缓存的次数。
数据尽可能一致。
较少的潜在危险。
所以一般采用如下方式:
- 互斥锁(mutexkey )
这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
- 永远不过期
“ 永远不过期” 包含两层意思:
从缓存层面来看,确实没有设置过期时间,所以不会出现热点k ey 过期后产生的问题,也就是“ 物理” 不过期。
从功能层面来看,为每个value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
33.无底洞问题吗?如何解决?
什么是无底洞问题?
2010 年,Facebook 的Memcache 节点已经达到了3 000 个,承载着TB级别的缓存数据。但开发和运维人员发现了一个问题,为了满足业务要求添加了大量新Memcache 节点,但是发现性能不但没有好转反而下降了,当时将这 种现象称为缓存的“无底洞” 现象。
那么为什么会产生这种现象呢?
通常来说添加节点使得Memcache 集群 性能应该更强了,但事实并非如此。键值数据库由于通常采用哈希函数将 key 映射到各个节点上,造成k ey 的分布与业务无关,但是由于数据量和访问量的持续增长,造成需要添加大量节点做水平扩容,导致键值分布到更多的 节点上,所以无论是Memcache 还是
Redis 的分布式,批量操作通常需要从不同节点上获取,相比于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络时间。
无底洞问题如何优化呢?
先分析一下无底洞问题:
客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着节点的增多,耗时会不断增大。
网络连接数变多,对节点的性能也有一定影响。
常见的优化思路如下:
命令本身的优化,例如优化操作语句等。
减少网络通信次数。
降低接入成本,例如客户端使用长连/ 连接池、NIO 等。