平时我们做项目时候使用缓存的方案,一般都是在数据库中存储一份,在缓存中存储一份。当用户的请求过来时。优先从缓存中获取数据,如果缓存中有数据,直接返回缓存中的数据,如果缓存中没有数据,那么就需要从数据库中取到数据并同步更新到缓存中,返回数据。如果这个时候查不到数据,一般就是返回空数据。
可是,当项目的数据和请求量到一个量级的时候,我们会发现这并不是一个有效可行的缓存机制。(ps:常用缓存机制如下图所示)
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ffcccc','edgeLabelBackground':'#ffffee', 'tertiaryColor': '#fff0f0'}}}%%
flowchart TD
a[用户请求] --> b[项目主体] --> c{缓存系统};
c -- 有数据 --> f[返回数据体]
c -- 没数据去数据库查询 --> d[数据库] -- 查到数据返回数据 --> f
d -- 查到数据写入缓存 --> c
d -- 数据库也没数据 --> e[返回空值]
缓存雪崩
缓存雪崩,就像字面意思一样,因为缓存中的大部分数据在同一小段时间内都过期了。这个时候用户发送请求过来,而缓存中没有数据,这个时候大部分都会去请求数据库。而数据库是一个项目的瓶颈所在。数据库查询压力突然剧增。扛不住就宕机了。
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ffcccc', 'edgeLabelBackground':'#ffffee', 'tertiaryColor': '#fff0f0'}}}%%
flowchart TD
a[用户请求] --> b[项目主体] --> c{Redis};
c -- Key全部失效 --> d[数据库压力剧增]
解决方案
- 如果是抗住了高并发也就是"热点"数据,可以考虑设置永不过期
- 随机过期时间,就比如我们大部分项目考虑的都是设置缓存10或者15分钟,假设我们在基础10分钟上加一个上下浮动值,往下浮动5分钟,网上浮动10分钟,就有可能这个缓存数据5分钟过期,而那个20分钟过期。可以有效防止数据在同一个时间点大量过期。
- 使用异步缓存策略。
- 使用集群缓存,保证缓存服务的高可用。Redis主要可以考虑使用主从备份+哨兵,Redis Cluster 来避免 Redis 全盘崩溃的情况。
- 同样是Redis可以考虑开启持久化,重启后可以从磁盘自动加载数据从而快速的恢复缓存。
缓存击穿
其实从另一个方面来讲缓存击穿跟缓存雪崩挺像的。都是缓存的key失效导致的结果,只不过雪崩是大量的缓存失效,查询涌入DB导致宕机。而击穿是指什么呢,是指某一个缓存的数据集非常热点。本来就算再大的请求过来缓存都全盘接收并返回了。但是突然这个缓存失效了。所有的大量请求在一瞬间全部涌入Db。导致的宕机产生。举个栗子来讲就是,一个盾牌一直挡着所有的锋矛。突然这个盾牌消失了。然后全部扎在了身上。
解决方案
- 同样的如果是热点数据,可以考虑设置永不过期。
- 如果缓存数据一定要过期,可以考虑在获取数据查询数据库的时候设置一个互斥锁。只让一个请求去请求Db数据,而其他请求因为拿不到锁。睡眠等待重试。但是这有一个问题,无论如何请求成功一定要记得解锁。异常也需要解锁。并且拿不到锁的睡眠线程,重试超过五次后要返回空值。避免线程一直占用。白白消耗服务器资源
缓存穿透
至于什么是缓存穿透呢,缓存穿透是指缓存中没有数据库中也没有的数据。但是用户却在不停的发送请求。我们大部分数据库的数据使用的都是自增id。那么肯定是从1开始的。但是用户一直查询-1或者特别大并且数据不存在的id的时候就会一直查询数据库,而数据库也没数据,导致数据库压力非常大。严重的情况下会击垮数据库。这种通常也可能是攻击者。(小一点的单机应用,基本上用postman都能打崩)
解决方案
- 入口做一个鉴权参数校验。比如id>0才可请求。
- 缓存空值。
- 布隆过滤器是用于判断某个元素(key)是否存在于某个集合中。先把我们数据库的数据都加载到我们的过滤器中,在缓存之前在加一层BloomFilter,在查询的时候先去 BloomFilter 去查询 key 是否存在,如果不存在就直接返回,存在再走查缓存,然后查DB。
- Nginx限流
布隆过滤器逻辑流程图
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ffcccc', 'edgeLabelBackground':'#ffffee', 'tertiaryColor': '#fff0f0'}}}%%
flowchart TD
a[用户系统] --> b[BloomFilter]
b --> c[缓存]
b -- 没有数据返回空 --> a
c -- 没有数据 --> d[数据库]
c -- 有数据返回数据 --> a
d -- 有数据写入 --> c
d -- 有数据返回数据 --> a