Redis 缓存雪崩、击穿、穿透

雪崩

什么是redis缓存雪崩现象呢?

Redis在某一时间段内,出现大量的key失效了,所以导致请求全部涌入到了数据库当中,此时数据库压力巨大,可能会导致崩溃。

雪崩现象的本质就是 某一时间,大量的key过期了。

我们要解决这个问题就需要合理的管理每一个keyredis缓存中的生命周期,避免大量的key同一时间过期。

产生的原因

  • 给大量的key设置相同的过期时间,所以导致会在同一时间缓存过期。
  • Redis服务宕机了,所有请求都会绕过缓存直接到数据库中。
graph TD User1[用户请求 A] --> Cache User2[用户请求 B] --> Cache User3[用户请求 C] --> Cache UserN[用户请求 ...] --> Cache subgraph Redis ["Redis 缓存层"] K1[Key 1: 过期] K2[Key 2: 过期] K3[Key 3: 过期] KN[Key N: 过期] end Cache -- 全部失效/宕机 --> DB[(数据库 DB)] DB -- "!!! 压力过载 (冒烟) !!!" --> Panic[系统瘫痪/宕机] style K1 fill:#f96,stroke:#333 style K2 fill:#f96,stroke:#333 style K3 fill:#f96,stroke:#333 style KN fill:#f96,stroke:#333 style DB fill:#ff4d4d,stroke:#333,stroke-width:4px

解决方案

针对相同时间过期

首先问题出现在了大量的key过期时间一致,我们一般设置key过期时间时,一般会指定它生存多久,而不会指定在什么时刻失效,所以我们可以在指定过期时间时不要设置N时间过期,而是在这个时间上加一个随机值,这个随机值可以根据业务需求来定,比如1-5分钟。

针对Redis宕机了

Redis宕机了,也就是说当前Redis服务器部署,不满足高可用性。那么我们可以通过Redis高可用组件来避免一台服务器宕机了,就导致业务崩溃。比如可以采用Redis的哨兵模式或者Redis集群模式。

当主库挂了,可以第一时间让从库升级为主库,继续为业务提供缓存服务。


当然为了最大化提升缓存的高可用性,可以在业务服务器当中,再添加一层缓存,这里的缓存由于内存硬件限制,不能存储大量的数据,但是可以存储一些热点key。使这些热点key长时间驻留在缓存中,不会因为Redis服务器宕机了,导致请求全部涌向了数据库中。


还有一种策略,它属于兜底的策略。使用限流和熔断。

限流:可以在网关层对请求进行限流(令牌桶等方法),这样不会导致大量的请求同一时间涌入数据库中。

熔断机制:当下游服务出现故障时,为了它不影响当前服务,可以直接放弃访问下游服务,直接返回错误给用户。避免长时间等待下游服务的恢复,导致上游服务崩溃。

击穿

我在最初学习Redis缓存的时候,分不清楚击穿和穿透,有时会会记混。

在这里我尽可能把这两个概念,写清楚,让之后的自己或者读者能够一眼分清楚什么是击穿什么是穿透。

穿透可以联想渗透测试(一种提前防止黑客攻击的安全测试-找漏洞),它是大量请求访问不存在的数据。一般是由黑客进行攻击时造成的。

缓存击穿主要问题是针对热点key突然过期了,大量访问该热点key的请求只能涌向数据库,导致数据库压力上升。

解决方案

  • 最简单的方式是针对热点key设置长时间过期时间或者设置为永不自动过期,可以手动删除key

  • 在业务服务器中,添加一个缓存,里面存储着所有的热点key,然后设置为永不过期,虽然redis中的过期了,不影响业务服务器里的缓存获取。

  • 加互斥锁,当一个请求访问热点key发现它过期了,然后加锁,从数据库获取数据,并且放入Redis缓存中,之后释放锁。这样其它请求就可以直接从Redis中访问热点key了。但是该方法,会有加锁-释放锁操作。会导致性能在某一时刻(等待锁)的时候会下降很多。

graph LR Users[海量并发请求 - 针对某热点 Key] --> Cache{缓存是否存在?} subgraph Redis ["Redis"] HotKey[热点 Key: 刚好过期] end Cache -- "不存在 (击穿)" --> Lock{是否获得互斥锁?} Lock -- "是 (仅一个请求)" --> DB[(数据库 DB)] Lock -- "否 (其他请求等待)" --> Wait[重试或阻塞] DB -- 返回数据并写回 --> Redis HotKey -.-> |原本的屏障破了| DB style HotKey fill:#f96,stroke-dasharray: 5 5 style DB fill:#fff,stroke:#ff4d4d,stroke-width:2px
  • 逻辑过期:在数据字段,加一个逻辑过期时间,如果请求访问热点key发现,逻辑过期了,可以另起一个协程去更新这个key,然后返回旧的数据,可能会导致数据短暂的不一致,但是保证了可用性。

穿透

缓存穿透主要问题是大量请求访问不存在的数据,然后这些请求涌入数据库中,破坏数据库的可用性。

一般是有黑客攻击,所造成的正常用户请求无法及时响应,因为服务器资源被黑客的大量求请所占用。

解决方案

  • 我第一个想法是在风控系统中,直接判断一些用户短时间内大量请求不存在的数据,直接拒绝该用户访问下游服务。

  • 也可以采用布隆过滤器,将大量的数据映射是否存在映射到小内存中。布隆过滤器就类似于一个漏斗,将请求过滤成更少的请求。将访问不存在的数据在网管层,就直接返回数据不存,不需要将该请求传递给具体的服务。但是布隆过滤器有误判的可能。

graph TD Attacker[恶意攻击/非法请求: ID=-1] --> Filter{布隆过滤器/参数校验} Filter -- "非法/不存在" --> Block[直接拦截返回] Filter -- "可能存在" --> Cache{Redis 缓存} Cache -- "未命中 (数据本就不存在)" --> DB[(数据库 DB)] DB -- "查询为空" --> Return[返回空结果] subgraph Layers ["层层穿透"] Cache DB end %% 视觉表达穿过感 Attacker -.-> |直接穿透| DB style Attacker fill:#00ffaa style DB fill:#fff,stroke:#333,stroke-width:2px style Layers fill:#e1f5fe,stroke:#01579b
  • 缓存一个空的对象(cache null)。如果数据库查不到该数据,就直接在缓存中缓存一个null对象,也就是说第一次访问数据库不存在的数据,那么就在缓存中缓存一个key的null值,然后后续的访问就直接在缓存层直接获取null了不需要将请求传递到数据库中。

  • 在接口前端,进行参数校验,比如ID >0等措施。