Redis 缓存穿透、击穿、雪崩

缓存穿透(Cache Penetration)

Untitled

  • 现象

    访问一个非法的数据(数据库和缓存中都不存在)出现这种情况,每次必然是要去数据库请求一次不存在的数据,这时候因为没有数据,所以也不会写入缓存,下一次同样的请求还是会重蹈覆辙。

  • 解决方案

    1. 前端校验: 例如根据用户id查询数据,针对id如负数等可以直接拦截
    2. 后端校验: 在接口的开始处,校验一些常规的正负数,比如负数的user_id直接返回报错。
    3. 空值缓存: 有时候我们也对于数据库查不到的数据,也做个缓存,这个缓存的时间可以短一些如60秒、5分钟等需要结合业务权衡。
    4. hash 拦截: hash 校验使用一些数据量不多的场景,比如店铺的商品信息,上架一个商品的时候,我们商品做下hash标记(map[“商品ID”]=1),这样如果请求的商品 id 都不在 hash 表里,直接返回了。
    5. 位图标记: 类似 hash,但是使用比特位来标记
    6. 布隆过滤器: 当我们关心的数据量非常大的时候 hash和位图那得多大,不现实,这时可以用布隆过滤器,布隆过滤器不像hash和位图那样可以做到百分百的拦截,但是可以做到绝大部分的非法的拦截。布隆过滤器的思想就是在有限的空间里,通过多个hash函数来定位一条数据,当只要有一个hash没中,那么一定是不存在的,但是当多个hash全中的话,也不一定是存在的,这一点是需要注意的。

缓存击穿(Cache Breakdown)

Untitled

  • 现象:

    热点数据在某一时刻缓存过期,然后突然大量请求打到 db 中,这时如果 db 扛不住,可能就挂了,引起线上连锁反应。

  • 解决方案

    1. 分布式锁:分布式系统中,并发请求的问题,第一时间想到的就是分布式锁,只放一个请求进去(可以用redis setnx、zookeeper等等)
    2. 单机锁:也并不一定非得需要分布式锁,单机锁在集群节点不多的情况下也是ok的(golang 可以用 synx.mutex、java 可以用 JVM 锁),保证一台机器上的所有请求中只有一个能进去。假设你有 10 台机器,那么最多也就同时 10 个并发打到db,对数据库来说影响也不大。相比分布式锁来说开销要小点,但是如果你的机器多达上千,还是慎重考虑。
    3. 二级缓存:当我们的第一级缓存失效后,也可以设置一个二级缓存,二级缓存也可以拦截下,二级缓存可以是内存缓存也可以是其他缓存数据库。针对本地缓存的不一致问题(集群某个节点数据修改只影响了本地的,导致和其他节点的数据不一样,可以通过gossip协议通知其他节点)
    4. 热点数据不过期:某些时候,热点数据就不要过期。或者通过定时任务去刷新
    5. Golang可以使用 singleflight 防止击穿

      Untitled

缓存雪崩(Cache Avalanche)

Untitled

  • 现象:

    缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,所有的请求都打到了 db,与缓存击穿不同的是,雪崩是大量的 key,击穿是一个 key,这时 db 的压力也不言而喻。

  • 解决方案

    1. Redis 高可用方案(如Redis Cluster + 主从),避免缓存单点造成全面崩溃。同时监控缓存,弹性扩容。
    2. 采用多级缓存方案,本地缓存(Ehcache/Caffine/Guava Cache) + 分布式缓存(Redis/ Memcached)
    3. 配置限流 + 熔断 + 降级(Hystrix),避免极端情况下,数据库被打死。
    4. Redis 持久化(RDB+AOF),一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
    5. 缓存的过期时间可以取个随机值。这么做是为避免缓存同时失效,使得数据库 IO 骤升。比如:以前是设置 10 分钟的超时时间,那每个 Key 都可以随机 8-13 分钟过期,尽量让不同 Key 的过期时间不同
    6. 根据场景上锁,保护 DB,同缓存击穿
    7. 热点数据不过期,同缓存击穿

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

Scroll to Top