6. 防止缓存击穿
...大约 3 分钟
https://github.com/dreamjz/golang-notes/tree/main/books/7-days-golang/GeeCache/day6-single-flight
DAY6-SINGLE-FLIGHT
│ go.mod
│ go.work
│ main.go
│ run.sh
│
└─geecache
│ byteview.go
│ cache.go
│ consistenthash.go
│ consistenthash_test.go
│ geecache.go
│ geecache_test.go
│ go.mod
│ http.go
│ peers.go
│
├─consistenthash
│ consistenthash.go
│ consistenthash_test.go
│
├─lru
│ lru.go
│ lru_test.go
│
└─singleflight
singleflight.go
1. 缓存雪崩、缓存击穿与缓存穿透
缓存雪崩:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。
缓存击穿:一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时DB请求量大、压力骤增。
缓存穿透:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB,如果瞬间流量过大,穿透到 DB,导致宕机。
2. Single Flight
在之前的测试中,若同时发起大量的请求,会可能导致两种情况:
- 若需本地载入 value,会同时发起大量的数据库调用,导致缓存击穿
- 若需从其他节点载入,则会同时发起大量的 HTTP 请求,并且可能导致目标节点出现缓存击穿
实际上,发起的多次请求,因为需要的结果相同,只需要一个请求成功执行并返回结果即可。
2.1 实现
package singleflight
import "sync"
type call struct {
wg sync.WaitGroup
val any
err error
}
type Group struct {
mu sync.Mutex
m map[string]*call
}
call
:表示请求wg
:并发计数器,用于确保请求只会被调用一次val
:请求的结果err
:请求的 error
Group
:mu
:互斥锁,保护m
的读写m
:存储不同的 key 对应的请求
func (g *Group) Do(key string, fn func() (any, error)) (any, error) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call) // 延迟加载
}
// 调用已存在
if c, ok := g.m[key]; ok {
g.mu.Unlock()
// 等待调用结束
c.wg.Wait()
// 直接返回
return c.val, c.err
}
// 调用不存在,创建新调用
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
// 发起调用
c.val, c.err = fn()
c.wg.Done()
// 调用结束,删除调用
g.mu.Lock()
delete(g.m, key)
g.mu.Unlock()
return c.val, c.err
}
- 若调用表不存在,则创建(延迟加载)
- 若key对应的调用已存在,则等待调用完成并返回
- 若不存在,则创建新的调用
- 执行调用函数
fn
- 调用结束,从调用表中删除
- 返回结果
Do
保证了任意多次相同的 key 对应的调用函数只会被执行一次,并返回相同的结果。
2.2 使用
type Group struct {
...
// use singleflight.Group to make sure that
// each key is only fetched once
loader *singleflight.Group
}
func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
...
g := &Group{
...
loader: &singleflight.Group{},
}
...
}
func (g *Group) load(key string) (ByteView, error) {
view, err := g.loader.Do(key, func() (any, error) {
if g.peers != nil {
if peer, ok := g.peers.PickPeer(key); ok {
val, err := g.getFromPeer(peer, key)
if err == nil {
return val, nil
}
log.Println("[GeeCache] Failed to get from peer", err)
}
}
return g.getLocally(key)
})
if err != nil {
return ByteView{}, err
}
return view.(ByteView), nil
}
load
方法中,将原有的执行逻辑放入g.loader.Do
中执行,保证每个请求只会被执行一次。
3. Demo 测试
发起 10 请求,可以看到请求实际只执行了 3 次。
2023/10/11 07:00:28 [Server http://localhost:8003] Pick peer http://localhost:8001
2023/10/11 07:00:28 [Server http://localhost:8001] GET /geecache/scores/Tom
2023/10/11 07:00:28 [SlowDB] search key Tom
0023/10/11 07:00:28 [Server http://localhost:8003] Pick peer http://localhost:8001
2023/10/11 07:00:28 [Server http://localhost:8001] GET /geecache/scores/Tom
2023/10/11 07:00:28 [GeeCache] hit
2023/10/11 07:00:28 [Server http://localhost:8003] Pick peer http://localhost:8001
2023/10/11 07:00:28 [Server http://localhost:8001] GET /geecache/scores/Tom
2023/10/11 07:00:28 [GeeCache] hit
...
Reference
Powered by Waline v2.15.2