golang 缓存渗透、缓存雪崩、缓存击落图解、示例和优化
缓存穿透、缓存雪崩、缓存击穿
说明: 最后引出的singleflight方案的优化示例没有完全搞清楚,这周搞清楚会更新出来。
说明: 最后引出的singleflight方案的优化示例没有完全搞清楚,这周搞清楚会更新出来。
为了减缓数据库的压力,往往在数据库前增加一个缓存:
1. 缓存穿透
在缓存中查不到key,只能去数据库查询;当有大量请求直接穿透了缓存打到数据库,就是缓存穿透。
解决
- 系统写好参数校验
- 缓存空值,过期时间短一些
- 布隆过滤器
2. 缓存雪崩
同一时间大规模key同时失效,大量的请求直接打在数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。
原因
- Redis宕机
- 大规模key使用了相同的过期时间
解决
- 原有失效时间加随机值
- 熔断机制
- 数据库容灾,分库分表、读写分离
- 防止Redis宕机: Redis集群
3. 缓存击穿
1. 概念及解决方案
大并发集中对一个热点的Key进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。
解决
- 如果业务允许的话,对于热点的key可以设置永不过期的key
- 使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。
2. singleflight
而在golang技术中可以用singleflight
,singleflight
能够在同一时间有大量针对同一key的请求这种情况,只让一个请求执行去获取数据,而其他协程阻塞等待结果的返回 。
模拟场景,请求先走Redis,发现没有key,全部都走到了数据库:
package main
import (
"context"
"errors"
"log"
"sync"
"time"
)
var errorNotExist = errors.New("not exist")
func main() {
// 模拟透传ctx 设定超时时间
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*3)
defer cancel()
//模拟10个并发
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
data, err := fetchData(ctx, "key")
if err != nil {
log.Print(err)
return
}
log.Println(data)
}()
}
wg.Wait()
}
// 获取数据
func fetchData(ctx context.Context, key string) (string, error) {
data, err := fetchDataFromCache(key)
if err == errorNotExist {
data, err = fetchDataFromDB(key)
if err != nil {
log.Println(err)
return "", err
}
//TOOD: set cache
} else if err != nil {
return "", err
}
return data, nil
}
// 模拟从缓存中获取值,缓存中无该值
func fetchDataFromCache(key string) (string, error) {
return "", errorNotExist
}
// 模拟从数据库中获取值
func fetchDataFromDB(key string) (string, error) {
log.Printf("get %s from database", key)
return "data", nil
}
// 执行输出
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 data
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 get key from database
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
2021/10/19 14:04:36 data
从以上出书可以看出,并发请求先到缓存,发现没有值,于是都打到了数据库,假设在真实业务场景中,并发量非常大,数据库可能会瞬间宕机。因此我们需要想办法将并发的请求减少:
改动fetchData
,增加singleflight
,如果并发请求查询某个热点key,缓存库没有则首次请求打到数据库,其他请求阻塞,直接取首次请求的返回值即可。
import "golang.org/x/sync/singleflight"
var sfg singleflight.Group
// 获取数据
func fetchData(ctx context.Context, key string) (string, error) {
data, err := fetchDataFromCache(key)
if err == errorNotExist {
v, err, _ := sfg.Do(key, func() (interface{}, error) {
return fetchDataFromDB(key)
//set cache
})
if err != nil {
log.Println(err)
return "", err
}
//TOOD: set cache
data = v.(string)
} else if err != nil {
return "", err
}
return data, nil
}
// 输出
2021/10/19 14:09:25 get key from database
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
2021/10/19 14:09:25 data
可以看到此时只有一个请求进入数据库,其他的请求也正常返回了值,从而保护了后端DB。但是这样是否真正合理呢?
模拟首次请求hang住,则所有请求都会hang住,程序报错退出:
// 获取数据
func fetchData(ctx context.Context, key string) (string, error) {
data, err := fetchDataFromCache(key)
if err == errorNotExist {
v, err, _ := sfg.Do(key, func() (interface{}, error) {
select {}
return fetchDataFromDB(key)
//set cache
})
if err != nil {
log.Println(err)
return "", err
}
//TOOD: set cache
data = v.(string)
} else if err != nil {
return "", err
}
return data, nil
}
程序报错,发生死锁:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x0)
D:/Program Files/Go/src/runtime/sema.go:56 +0x25
sync.(*WaitGroup).Wait(0xa80bf0)
D:/Program Files/Go/src/sync/waitgroup.go:130 +0x71
main.main()
D:/Go/src/github.com/test/main.go:34 +0x10f
goroutine 19 [select (no cases)]:
main.fetchData.func1()
D:/Go/src/github.com/test/main.go:42 +0x17
golang.org/x/sync/singleflight.(*Group).doCall.func2(0xc00004be66, 0xc000052060, 0xa534c0)
D:/Go/pkg/mod/golang.org/x/sync@v0.0.0-20210220032951-036812b2e83c/singleflight/singleflight.go:193 +0x6f
golang.org/x/sync/singleflight.(*Group).doCall(0xa4c980, 0xc00001e030, {0xa5c137, 0x3}, 0x0)
D:/Go/pkg/mod/golang.org/x/sync@v0.0.0-20210220032951-036812b2e83c/singleflight/singleflight.go:195 +0xad
golang.org/x/sync/singleflight.(*Group).Do(0xb076f0, {0xa5c137, 0x3}, 0x0)
D:/Go/pkg/mod/golang.org/x/sync@v0.0.0-20210220032951-036812b2e83c/singleflight/singleflight.go:108 +0x154
main.fetchData({0x0, 0x0}, {0xa5c137, 0x3})
D:/Go/src/github.com/test/main.go:41 +0xb8
main.main.func1()
D:/Go/src/github.com/test/main.go:26 +0x6c
created by main.main
D:/Go/src/github.com/test/main.go:24 +0x85
此时可以使用DoChan
结合select
做超时控制:
// 获取数据
func fetchData(ctx context.Context, key string) (string, error) {
data, err := fetchDataFromCache(key)
if err == errorNotExist {
result := sfg.DoChan(key, func() (interface{}, error) {
// 模拟出现问题,hang 住
select {}
return fetchDataFromDB(key)
//set cache
})
select {
case r := <-result:
return r.Val.(string), r.Err
case <-ctx.Done():
return "", ctx.Err()
}
} else if err != nil {
return "", err
}
return data, nil
}
此时若首次请求超时则会出现超时消息:
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
2021/10/19 14:23:21 context deadline exceeded
可以看到一次超时,实际上并发请求都会报同样的超时反馈。singleflight
只是为了降低请求的数量级,为了提高程序的试错率,可以用Forget
让key适时过时,提高下游请求的并发数,多试错几次。
go func() {
log.Printf("forget key: %v\n", key)
time.Sleep(100 * time.Millisecond)
// logging
g.Forget(key)
}()
上一篇: GoLang 配置代理:系统代理和 GOPROXY
下一篇: 让我们绘制:最小示例