Redis 缓存穿透及解决方案

缓存访问流程

正常的缓存访问过程通常包含以下三种情况:

  1. 缓存命中:应用访问缓存,假如数据存在,则直接返回数据。
    缓存命中流程
  2. 缓存未命中(回源):数据在 Redis 不存在,则去访问数据库。数据库查询到了直接返回应用,同时把结果写回 Redis。
    缓存未命中回源流程
  3. 缓存穿透:数据在 Redis 不存在,数据库也不存在,返回空。一般来说空值是不会写入 Redis 的,如果反复请求同一条数据,那么则会发生缓存穿透。
    缓存穿透流程

针对上述第三种情况,一种简单的解决方案是为这个 Key 设置一个空值同时写入 Redis。这样下次请求的时候就不会访问数据库。但是,如果每次请求的是不同的 Key,同时这些 Key 在数据库中也是不存在的,那么依然会发生缓存穿透。

布隆过滤器解决方案

我们可以这样考虑:在访问 Redis 之前,先判断 Key 值是否存在。如果不存在,则不访问 Redis,这样就可以拦截大量的无效请求。布隆过滤器(Bloom Filter) 恰好可以实现这样的需求。

原理

布隆过滤器本质上是一个二进制向量。初始化的时候每一个位置都是 0。如下图所示,比如说元素 a 经过 Hash 算法后得到一个下标位置,接下来就会把该下标的值改为 1。图中所示的是每一个元素经过三次 Hash 运算,每一个红线代表一次 Hash 算法。

为什么要运算三次呢?这是为了减少 Hash 冲突。当然 Hash 算法不一定是三次,经过多次不同维度的哈希算法后,就把 a 值映射到了二进制向量里面。这样的好处很多,可以节省空间。假如说 a 值是一串很长的字符串,那么经过映射后就可以只占三位长度,并且查找速度很快。

布隆过滤器原理

核心特性

如果布隆过滤器判断元素存在,则不一定存在;如果不存在,则一定不存在。

如何理解这句话?因为有可能一个元素运算得到的下标恰好是别的元素的下标(即 Hash 冲突)。如果经过运算后布隆过滤器判断不存在,也就是说至少有一个下标是为 0 的,那肯定是不存在的。

布隆过滤器的使用

Google 的 Guava 包已经有了布隆过滤器算法的实现。注意的是布隆过滤器有一定的误判率,不可能达到 100% 的精准。首先初始化项目的时候从数据库查询出来所有的 Key 值,然后放到布隆过滤器中,Guava 包都实现了相应的 put 方法和 Hash 算法。

加了布隆过滤器的过程如下:

加入布隆过滤器的流程

  1. 当应用访问的时候,先去布隆过滤器中判断 Key 值。如果发觉 Key 值不存在,直接返回。
  2. 如果 Key 值在布隆过滤器存在,则去访问 Redis。由于是有误判率的,所以 Redis 也有可能不存在。
  3. 那么这时候就去访问数据库。数据库不存在,那就直接返回空就行。

并发处理

如果误判率为 3%,当有 100 万个请求同时过来的时候,布隆过滤器已经挡住了 97 万个请求。剩下 3 万个请求假如是误判的,这时候再访问数据库可以通过加锁的方式实现。只有竞争到锁了就去访问数据库,这样就完全可以解决缓存穿透问题。

布隆过滤器的应用场景

  • 输入用户名的时候,可以马上检测出该用户名是否存在。
  • 黑名单机制。
  • 单词错误检测等。
说明:本文基于传统架构下的缓存穿透解决方案整理,涉及图片及部分内容参考自 2019 年左右的技术实践。在实际生产中,也可结合 Redis 原生模块(如 RedisBloom)或网关层拦截等多种方式综合防护。