缓存穿透、缓存雪崩、缓存击穿 解决方案分析 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 mainimport ( "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 mainimport ( "errors" "log" "sync" "golang.org/x/sync/singleflight" ) func main () { var wg sync.WaitGroup wg.Add(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.Groupfunc 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 + 分布式缓存,本地缓存失效后优先读分布式缓存,减少回源。