缓存穿透

缓存不一致的问题

使用缓存时会出现缓存中的数据和数据库中的数据不一致的情况。

使用缓存的两种模式

  1. 只读模式

当更新或删除数据时不更新缓存而是直接删除缓存

  1. 读写模式

当插入、更新或删除数据时同时更新缓存和数据库

只读模式下出现缓存不一致的情况:

情况一:缓存删除失败

更新/删除数据时
先删除缓存后更新数据库,如果缓存删除成功数据库更新失败,缓存会被重新创建此时没有影响。

先更新数据库,后删除缓存,如果缓存删除失败则会出现缓存不一致。

解决方案: 重试机制

可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用 Kafka 消息队列)。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。

情况二: 并发请求
  1. 先删除缓存再更新数据库
时间 线程A 线程B
t1 删除数据X缓存
t2 查询X缓存中未命中,从数据库中查询数据并更新缓存
t3 更新数据库

如上表所示,当出现并发操作时,最终会导致缓存中的数据依旧是旧值

  1. 先更新数据库再删除缓存

当数据库已经更新,缓存还未来得及删除时,其他请求仍然可能从缓存中读取到旧数据,但是时间很短对业务影响较小。

解决方案:
  1. 当采用先删除缓存再更新数据库时可以使用延迟双删来保证一致性,但延迟时间不好把握且不够优雅,伪代码:
1
2
3
4
redis.delKey(X)
db.update(X)
Thread.sleep(N)
redis.delKey(X)
  1. 使用分布式锁,将请求串行化

缓存雪崩

缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。

导致原因有:

  1. 缓存中大量数据同时过期,
  2. Redis服务不可用

避免方法:

  1. 这些数据的过期时间增加一个较小的随机数(例如,随机增加 1~3 分钟)
  2. 构建 Redis 缓存高可靠集群

缓存击穿

缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,大量请求发送到了后端数据库,导致了数据库压力激增。缓存击穿的情况,经常发生在热点数据过期失效时,

解决方案:

  1. 热点数据不设置过期时间
  2. 使用互斥锁, 代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public String get(key) {
    String value = redis.get(key);
    if (value == null) { //代表缓存值过期
    //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
    if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
    value = db.get(key);
    redis.set(key, value, expire_secs);
    redis.del(key_mutex);
    } else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
    sleep(50);
    get(key); //重试
    }
    } else {
    return value;
    }

缓存穿透

缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。如果应用持续有大量请求,就会同时给缓存和数据库带来巨大压力。

一般来说,有两种情况:

  1. 业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;
  2. 恶意攻击:专门访问数据库中没有的数据。

解决方案:

  1. 一旦发生缓存穿透,可以针对查询的数据,在 Redis 中缓存一个空值或是和业务层协商确定的缺省值(例如,库存的缺省值可以设为 0)
  2. 使用布隆过滤器快速判断数据是否存在
  3. 过滤不合法的请求
关于布隆过滤器

布隆过滤器由一个初值都为 0 的 bit 数组和 N 个哈希函数组成,可以用来快速判断某个数据是否存在。当我们想标记某个数据存在时(例如,数据已被写入数据库),布隆过滤器会通过三个操作完成标记:

  1. 首先,使用 N 个哈希函数,分别计算这个数据的哈希值,得到 N 个哈希值。
  2. 然后,我们把这 N 个哈希值对 bit 数组的长度取模,得到每个哈希值在数组中的对应位置。
  3. 最后,我们把对应位置的 bit 位设置为 1,这就完成了在布隆过滤器中标记数据的操作。

当需要查询某个数据时,我们就执行刚刚说的计算过程,先得到这个数据在 bit 数组中对应的 N 个位置。紧接着,我们查看 bit 数组中这 N 个位置上的 bit 值。只要这 N 个 bit 值有一个不为 1,这就表明布隆过滤器没有对该数据做过标记,所以,查询的数据一定没有在数据库中保存。

image

注意布隆过滤器会有误判,由于数组长度有限, 不同的值可能映射到同一组bit上。它可以确定数据一定不存在,但不能确定一个数据是否一定存在。