Skip to main content
  1. internet/

Redis缓存设计中的问题

·1365 words·3 mins·

Redis最常用的场景就是作为缓存。

缓存带来的收益就是加速读写,降低下游存储的压力

但引入缓存的同时也增加了一些成本与潜在问题。

缓存穿透 #

当客户端访问一个即不存在于缓存层,又不存在于存储层的数据时,为了保持“数据一致性”,服务端会直接将空结果返回。但是缓存这样就失去了保护存储层负载的意义——如果有大量这样的恶意攻击,存储层会由于请求太多导致响应慢,牵一发而动全身,整体系统都会受影响甚至崩溃。

缓存穿透有两种解决办法:缓存空对象和使用布隆过滤器。

缓存空对象 #

通过在缓存层保存一个NULL值就能保护存储层免于袭击。

但这同样有缺点:

  1. 缓存中可能存在大量的NULL数据,会占用大量宝贵的内存
  2. 缓存层和存储层数据不一致:存储层可能会写入在缓存中为NULL的数据。

可通过对NULL数据设置较短的缓存时间使用合理的缓存清理方案来缓解上面两个问题。

使用布隆过滤器拦截 #

可以使用布隆过滤器来拦截掉不存在的数据请求。这种方案的缺点就是代码复杂度会更高

缓存击穿 #

“击穿”和“穿透”两个词的相似性太高,往往使人迷惑。所以我们往往使用热点key问题来描述。

一个热点数据往往有着大量的并发请求,我们要小心处理这些热点数据,否则一旦缓存失效,巨量请求会直接使存储层响应变慢甚至崩溃。

缓存击穿的场景往往是缓存失效导致的,解决方案有:通过加锁限制存储层的访问数量、设置“随机”的过期时间避免大量数据一起失效、设置缓存永不过期等

通过加锁限制存储层的访问数量 #

当缓存失效后,使用全局锁来实现只允许一个线程请求存储层,其他线程等待这个请求的结果。

这种方案的缺点是代码实现更复杂,并且如果获取到锁的线程访问有异常,会导致大量的请求超时。此外,还会有死锁这种潜在问题。

设置“随机”的过期时间 #

设置“随机”的过期时间是为了避免大量数据一起失效,这样能够分批请求存储层,减少存储层压力。

但是如果有一个超热数据,仍会对存储层造成压力。

设置缓存永不过期 #

设置缓存永不过期能够避免缓存失效问题。但是需要在代码上增加复杂度——判断何时对缓存进行更新、删除。

缓存雪崩 #

缓存雪崩是指缓存服务器异常,缓存全部失效,导致存储层压力骤增。

这种时候可以先提高缓存层的可用性,如使用哨兵模式或者集群模式。然后再进行其他优化,如:限制请求频率

除了对缓存层进行优化外,还要从整体角度来考虑,比如增加降级机制来避免整体系统崩溃。

无底洞问题 #

无底洞问题是指在一个分布式缓存集群中,添加节点并没有加快请求,反而使请求更慢。这一问题往往是发生在批量获取数据时产生的。

由于数据分布在多个节点中,因此一个批量操作会涉及到多次网络操作,另外,网络连接数变多也会影响服务器性能。

我们假设需要执行mget命令批量读取多个数据,有以下几种方案:

串行命令 #

最简单的方式就是有几个key就进行多个次get请求,但是这种方式无疑也是性能最差的。

客户端聚合key #

客户端能够提前在本地缓存key->槽->节点的映射关系,因此可以先遍历key,将在同一节点的key执行批量操作,这样能够减少网络请求。

并行IO #

在上一步的基础上,通过异步请求将串行IO变为多线程,能够进一步加速请求。

hash tag #

Redis集群提供了hash_tag功能,将多个key强制分配到一个节点上。但是这种方式需要更高的维护成本,还容易形成数据倾斜。