Redis 做缓存可能面临哪些问题?
Redis 做缓存是提升系统性能的利器,但同时也会面临一些问题。本文总结了使用缓存常见的几种问题,以及应对这些问题的方法。
缓存穿透
对数据库做缓存时,大部分流量从缓存中返回,少部分未命中缓存的流量才会请求到数据库中,数据库的压力就会比较低。但如果数据在数据库中不存在,自然也不会在缓存中存在,这类流量每次都无法命中缓存,都会请求到数据库中,这样缓存就无法起到降低数据库压力的作用。这种查询不存在数据的现象叫做缓存穿透。
缓存穿透可能是业务逻辑固有的问题,也可能是恶意攻击导致的。针对这两种情况,可以采取的应对措施也不同。
业务逻辑导致的缓存穿透
对返回空的 key 值也进行缓存,注意这里是正常返回但是结果为空的情况,发生异常时不能当作空数据进行缓存。恶意攻击导致的缓存穿透
如果是恶意攻击刻意构造不存在的 key 发起请求,缓存空数据的方案就不可行了,这种情况可以借助布隆过滤器来对请求进行一层过滤。
布隆过滤器是用最小代价来判断元素是否存在于某个集合中的方法。虽然维护布隆过滤器需要一定的成本,但是相比攻击导致的资源损耗还是值得的。
缓存击穿
如果缓存中的某些热点数据因为某种原因突然失效,比如典型地由于超期而失效,同时又有大量针对该数据的请求发送进来,那么这些请求因为无法命中缓存,都会到达数据库中,导致数据库压力突增,这种现象叫做缓存击穿。
针对缓存击穿,可以采取两种方案:
- 加锁同步
当缓存数据失效时,请求会查询数据库,然后再将查询到的数据写会回缓存。如果将这部逻辑通过互斥锁保护起来,这样最多只有一个请求能够达到数据库中,其它请求可以采取阻塞或者重试的策略。注意 redis 是分布式缓存,这里通常需要采用分布式锁来进行保护。 - 热点数据手动管理
缓存击穿是由于热点数据失效导致的问题,对于这类数据,我们可以通过代码进行有计划的更新,避免自缓存自动失效。
缓存雪崩
大批量不同的数据在短时间内一起失效的话,针对这些数据的请求都会击穿缓存,到达数据库,导致数据库压力剧增,这种现象叫过缓存雪崩。
缓存雪崩可能是由于服务有专门的缓存预热功能,或者大量数据都是由某一次冷操作加载的,这样导致由此载入缓存的数据具有相同的过期时间,在同一时刻一起失效;还可能是缓存服务崩溃重启,造成大量数据同时失效。
针对缓存雪崩,我们可以采取以下三种方案:
提升缓存系统的可用性,比如集群部署,支持故障检测、主备切换等;
使用本地缓存,各个服务节点的本地缓存通常具有不同的加载时间,从而过期时间就会比较分散
将缓存的过期时间从固定时间改为一个时间段内的随机事件,比如原来是两个小时过期,现在设置为 110 分钟到 130 分钟之间的某个随机值
缓存污染
缓存污染是指缓存和数据库数据不一致的现象。通常使用缓存时,不会追求强一致性,但是最终一致性还是需要保证的。
缓存污染问题通常是由于代码开发不规范导致的,比如更新缓存数据后,由于某些原因,比如业务发生异常回滚,导致数据没有写入数据库中,这时缓存中数据是新的,但是数据库还是就数据。
为了尽可能保证缓存数据的一致性,业内总结了很多更新缓存的设计模式,包括 Cache Aside、Read/Write Through、Write Behind Caching 等等,其中最常用的是 Cache Aside 模式,因为它最简单、成本最低。主要内容可以概括为以下两点:
读数据时,首先读缓存,没有缓存,再度数据源,然后将数据写入缓存,再响应请求
写数据时,先写数据源,然后失效缓存
写数据时,这里要注意两点。
一个是先后顺序一定是先数据源,再缓存。如果先失效缓存,再更新数据源,一定存在一段时间内缓存已经删除完毕,数据源还未更新。此时如果有读请求进来,无法命中缓存,就会到达数据源中。
此时读到的还是旧数据,随后又会写到缓存中。等数据更新完成后,就出现了缓存中是旧数据,数据源是新数据,两者数据不一致的情况。
二是应当失效缓存,而不是更新缓存。因为如果是更新缓存,更新过程中数据源又被其它请求修改,缓存要面临多次赋值的复杂时序问题。如果是失效缓存,则无论这个过程中数据源更新了多少次,都不会产生影响。
当然,有一种情况下 Cache Aside 模式也会导致数据不一致,就是如果某个数据是从未被缓存过的,或是恰好超期失效,或是恰好因为更新被失效,读请求就会达到数据源中。如果对数据源的又一个写操作正好发生在查询请求之后,结果回填到缓存前,也会出现缓存中数据和数据源不一致的情况。
相对而言,这种缓存不一致出现的条件更为苛刻一点。通过设置合理的过期时间,可以控制这种情况下的最长影响时间。 通常情况下,Cache Aside 模式依然是一种低成本更新缓存,且能够获得相对可靠结果的解决方案。
BigKey
string 类型超过 10KB,hash、list、set、zset 元素个数超过 5000,可以认为是 big key,可能导致 Redis 性能下降。
BigKey 的产生可能有下面这些原因:
未正确使用 Redis,如使用 String 类型的 key 存放大体积二进制文件型数据
业务规划不足,没有对 key 中的成员进行合理的拆分,造成个别 key 中的成员数量过多
未定期清理无效数据,造成如 HASH 类型 key 中的成员持续不断地增加
服务发生异常,如使用 LIST 类型 key 的业务消费侧发生代码故障,造成对应key 的成员只增不减
BigKey 可能导致以下问题:
大量占用内存,引发操作阻塞或重要的 key被逐出,甚至引发内存溢出
集群架构下,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡
命令执行效率下降,如 lrange、hgetall 等时间复杂度为 O(n) 的命令
对 BigKey 执行读请求,会使 Redis 实例的带宽被占满,影响服务性能
对 BigKey 执行删除操作,易造成主库较长时间的阻塞,进而可能引发同步中断或主从切换
如何定位 BigKey:
通过 redis-cli 的 bigkeys 参数查找 BigKey
通过 Redis 内置命令 DEBUG OBJECT、MEMORY USAGESTRLEN、LLEN 等对目标 Key 进行分析
使用第三方工具redis-rdb-tools,使用过程中会先使用 bgsave 命令dump一个rdb 镜像,然后对这个镜像进行分析
我们可以参考以下几种方案来解决 BigKey 的问题。
尝试压缩 value。
将 BigKey 拆分成多个小 key
对 BigKey 进行清理,迁移到其它更适合的存储中
对集合中的过期数据进行定期清理
删除 BigKey 时,使用 redis4.0 新特性,非阻塞删除
HotKey
HotKey 是指 Redis 中访问频率特别高的 key。在秒杀、爆款商品、爆款新闻等场景中经常会出现 HotKey,可能导致以下问题:
占用 Redis server 端大量的 CPU 资源,导致整体性能下降
如果是集群架构,会产生访问倾斜,某个内存分片被大量访问,可能导致该分片出现连接数耗尽、性能下降等问题
增加网络流量,可能导致网路阻塞
出现缓存击穿现象,导致数据库压力突增,影响其它业务
访问量超出 Redis server 上限,导致服务崩溃,进一步导致缓存雪崩
如何定位 HotKey:
通过 redis-cli 的 hotkeys 参数查找 HotKey
在业务层增加相应的代码对 Redis 的访问进行监控分析
通过 MONITOR 命令找出 HotKey
我们可以参考以下几种方案来解决 HotKey 的问题:
可以增加本地缓存,来降低 HotKey 的访问频率。不过这种方案会面临缓存不一致、消耗本地内存的问题。
实现读写分离以及读流量在多个从节点间的负载均衡。在请求量极大的场景下,读写分离架构会产生不可避免的延迟,此时会有读取到脏数据的问题
对 HotKey 进行复制,比如将 HotKey foo 复制出 3 个内容完全一样的 key 并名为 foo2、foo3、foo4。该方案的缺点在于需要联动修改代码,同时也有数据一致性的问题(由原来更新一个 key 演变为需要更新多个 key)
接入 proxy,在 proxy 层实现缓存或读写分离的功能。类似方案 1 和方案 2,改进的地方是对业务层是透明的
以上主要是针对读请求热点 key,如果是写请求过多,则考虑一下方案:
通过将一个 key 改成逻辑上的多个 key 来实现
在内存中操作,按一定周期写回 redis
在写 redis 时进行限流
总结
本文总结了 Redis 作为缓存的常见问题及解决方案,不同的业务场景下可能会面临不同的问题,可以采取的方案也不尽相同。可以遵循一个原则进行选择,“能满足需求的前提下,最简单的系统就是最好的系统”。