0%

缓存穿透、缓存雪崩、缓存击穿 解决方案分析

缓存穿透、缓存雪崩、缓存击穿 解决方案分析

1 缓存穿透

  • 正常的读取流程是,先查缓存,如果在缓存中的查不到或者数据过期,那么就到数据库中查询。如果在数据库能查到,则放入缓存,下次就能在缓存中以较快的速度拿到数据。
  • 如果用来查询的key在数据库中根本不存在,则每次查询都会直接打到数据库,因为在上面的读取流程中不对查询为空的结果进行缓存。如果当前 qps 很高,会对数据库造成压力,甚至压垮数据库。

1.1 空值法

1.1.1 基本思路

  • 当在数据库中查不到时,将查询为空的结果进行缓存,并设置过期时间,这样下次就可以直接根据在缓存中拿到的数据,判断是否存在。

1.1.2 缺点

  • 假如每次查询的key都不一样,并且在数据库中均不存在对应的记录,这样空值法就失效了,因为空值法只会对当前key查询为空的结果进行缓存。

1.2 布隆过滤器

1.2.1 原理

  • 布隆过滤器的主要功能就是用来判断某个元素(key)是否在集合中

1.2.1.1 算法

  • 首先需要k个hash函数,每个函数可以把key散列成为1个整数
  • 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
  • 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
  • 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中

1.2.1.2 缺点

  • 算法判断key在集合中时,有一定的概率key其实不在集合中(误判率可以调),但是如果算法判断key不在集合中,则一定不在
  • 无法删除,因为存在hash冲突,不同的key可能经hash运算后映射到相同的整数值

1.2.2 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main


import (
"fmt"

"github.com/bits-and-blooms/bloom/v3"
)


func main() {
size := uint(1e5)
filter := bloom.NewWithEstimates(size, 0.01)
var i uint
for i = 0; i < size; i++ {
filter.Add([]byte(fmt.Sprintf("%d", i)))
}


count := 0
for i = size; i < size+10000; i++ {
if filter.Test([]byte(fmt.Sprintf("%d", i))) {
count++
}
}
fmt.Printf("误判次数:%d\n", count)
}
$ go run ./bloom.go
误判次数:90

查询10000个不在集合中的数,误判次数为90,因此误判率约为1%,和设置值接近。

1.2.3 使用布隆过滤器解决缓存穿透问题

  • 读取时先查布隆过滤器,不存在的key可以100%地判断出来,如果出现误判,查询会被直接打到数据库,但是因为误判率通常设置得很低,所以对服务造成影响微乎其微。

2 缓存雪崩

  • 缓存雪崩,是指在某一个时间段内,缓存集中过期,集中失效(可能是物理原因)

2.1 大量缓存失效

  • 将缓存失效时间分散开,比如在原有的失效时间基础上增加一个随机值,对于热点数据,过期时间尽量设置得长一些,冷门的数据可以相对设置过期时间短一些。

2.2 缓存服务器宕机

  • 使用缓存集群,避免单机宕机造成的缓存雪崩。

3 缓存击穿

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

3.1 分布式锁

  • 如果key不在缓存中,就加锁去查数据库,然后放入缓存中。这样多个并发的查询请求,只有一个会被打到数据库,其余的请求从缓存中就可以拿到数据。

3.2 singleflight

  • 当大量请求到来时,对于相同的key,函数对象只会被调用一次。也就是说只有一次请求会被打到数据库,其余请求从singleflight内部维护的map中获取查询结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
"errors"
"log"
"sync"

"golang.org/x/sync/singleflight"
)

func main() {
var wg sync.WaitGroup
wg.Add(10)

// 模拟10个并发
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
data, err := GetData("key")
if err != nil {
log.Println(err)
return
}
log.Println(data)
}()
}

wg.Wait()
}


var g singleflight.Group


func GetData(key string) (string, error) {
// 先查缓存
data, err := GetDataFromCache(key)
if err == errNotExist {
// 缓存中找不到,查数据库
value, err, _ := g.Do(key, func() (interface{}, error) {
return GetDataFromDB(key)
})
if err != nil {
log.Println(err)
return "", err
}
data = value.(string)
} else if err != nil {
return "", err
}
return data, nil
}

var errNotExist = errors.New("not exists")

func GetDataFromCache(key string) (string, error) {
// 模拟在缓存中查不到的情况
return "", errNotExist
}

func GetDataFromDB(key string) (string, error) {
log.Printf("get %s from database", key)
return "data", nil
}
1
2
3
4
5
6
7
8
9
10
11
12
$ go run ./singleflight.go 
2021/09/30 10:57:01 get key from database
2021/09/30 10:57:01 data
2021/09/30 10:57:01 data
2021/09/30 10:57:01 data
2021/09/30 10:57:01 data
2021/09/30 10:57:01 data
2021/09/30 10:57:01 data
2021/09/30 10:57:01 data
2021/09/30 10:57:01 data
2021/09/30 10:57:01 data
2021/09/30 10:57:01 data
  • 上面的代码模拟10个并发的请求,查询相同的key对应的的value,只有一次是去访问数据库,其余都是从singleflight内部维护的map中获取的。

3.3 多级cache

localcache + 分布式缓存,本地缓存失效后优先读分布式缓存,减少回源。