介绍旁路缓存和缓存异常的场景

旁路缓存(Cache-Aside)

Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准

  • 读策略: 从缓存中读取数据;如果缓存命中,则直接返回数据;如果缓存不命中,则从数据库中查询数据;查询到数据后,将数据写入到缓存中,并且返回给用户
  • 写策略: 更新数据库Mysql中的记录,然后删除Redis中的缓存记录

场景:

举个例子,假设某个用户的年龄是 20,请求 A 要更新用户年龄为 21,所以它会删除缓存中的内容。这时,另一个请求 B 要读取这个用户的年龄,它查询缓存发现未命中后,会从数据库中读取到年龄为 20,并且写入到缓存中,然后请求 A 继续更改数据库,将用户的年龄更新为 21。最终,该用户年龄在缓存中是 20(旧值),在数据库中是 21(新值),缓存和数据库的数据不一致

为什么「先更新数据库再删除缓存」不会有数据不一致的问题?

  • 假如某个用户数据在缓存中不存在,请求 A 读取数据时从数据库中查询到年龄为 20,在未写入缓存中时另一个请求 B 更新数据。它更新数据库中的年龄为 21,并且清空缓存。这时请求 A 把从数据库中读到的年龄为 20 的数据写入到缓存中
  • 最终,该用户年龄在缓存中是 20(旧值),在数据库中是 21(新值),缓存和数据库数据不一致。 从上面的理论上分析,先更新数据库,再删除缓存也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高
  • 因为缓存的写入通常要远远快于数据库的写入
  • Cache Aside 策略适合读多写少的场景,不适合写多的场景

缓存穿透

用户访问的数据既不在缓存中,也不在数据库中

解决:

  • 回写特殊值:缓存未命中且数据库也没有,在Redis缓存中设置一个特殊值表示数据不存在。会消耗内存
  • 布隆过滤器:由初始值都为0的位图数组和N个哈希函数组成
    流程:
    1. 采用N个哈希函数运算得到N个哈希值
    2. 将这N个哈希值对位图数组长度取模,得到每个哈希值在数组中的位置
    3. 在N个对应位置设置成1
    4. 查询的时候,只要对于位置的值全是1就表示存在。因此过滤器查询到这个数据,数据库不一定存在。如果查询不到数据库一定不存在
  • 限流策略:采取令牌桶算法或者漏桶算法,对这些数据进行限流

缓存击穿

缓存中的某个热点数据过期

解决:

  • 设置热点数据的热度时间窗口,在这个时间内如果数据被频繁访问,将缓存时间延长
  • 使用互斥锁或分布式锁,只允许一个线程去查询数据,避免多个线程同时查询数据库压力过大
  • 热点数据缓存永不过期
  • 异步更新缓存,减少对数据库的直接访问,并且不会阻塞请求的响应

缓存雪崩

大量缓存数据在同一时间过期或者Redis宕机

大量缓存数据在同一时间过期的解决:

  • 设置缓存过期的随机过期时间
  • 使用互斥锁或分布式锁,只允许一个线程去查询数据,避免多个线程同时查询数据库压力过大
  • 数据预热,提前将热点数据加载到缓存
  • 后台更新缓存,业务不再复制更新缓存,也不设置有效期,而让缓存“永久有效”,将更新缓存的工作交给后台线程定时更新
  • 数据库优化,提升数据库的性能,增加数据库的容量,以应对大量请求导致的数据库压力

Redis宕机的解决:

  • 服务熔断或者限流策略:暂停业务对缓存的访问,如果Redis宕机则直接返回错误不访问数据库。但是这样做会导致业务无法正常工作。也可以启动限流策略,只接受少量部分的请求
  • 提供缓存本身的可用性:通过主从节点的构建Redis缓存高可靠集群