本节实现实现了防止缓存击穿的措施,通过多个并发请求映射为一个请求来实现。最终代码结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| lru/ |--lru.go |--lru_test.go singleflight/ |--singleflight.go byteview.go cache.go consistenthash.go dawncache.go dawncache_test.go go.mod http.go peers.go
|
概念
- 缓存雪崩:缓存在同一时刻全部失效,造成瞬时 DB 请求量过大、压力骤增。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。
- 缓存击穿:一个存在的 key,在缓存过期的一瞬间,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时DB请求量大、压力骤增。
- 缓存穿透:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB。
singleflight 实现
singleflight/singleflight.go
dawncache 通过 singleflight 来实现防止缓存击穿。
定义结构体
首先定义 call,代表一次请求:
1 2 3 4 5 6
| type call struct { wg sync.WaitGroup val interface{} err error }
|
Group 记录了待查询的 key 和一次请求之间的映射关系。
当 key 还在 hashMap 中时,视为一次请求即可。
1 2 3 4
| type Group struct { mu sync.Mutex hashMap map[string]*call }
|
实现 Do 方法
Do 方法实现了多次相同的查询,到一次请求的映射操作:
- 查询 hashMap 中是否已经记录了 key 对应的 call 操作,如果有,则等待这一次请求得到数据并返回结果。
- 如果不在 hashMap 中,则新建一个查询请求 call 并记录在 hashMap 中,待执行过查询操作之后再返回数据。
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
| func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() if g.hashMap == nil { g.hashMap = make(map[string]*call) } if c, ok := g.hashMap[key]; ok { g.mu.Unlock() c.wg.Wait() return c.val, c.err }
c := new(call) c.wg.Add(1) g.hashMap[key] = c g.mu.Unlock()
c.val, c.err = fn() c.wg.Done()
g.mu.Lock() delete(g.hashMap, key) g.mu.Unlock()
return c.val, c.err }
|
修改主流程
dawncache.go
需要修改 Group 结构体,使之能够防止缓存穿透:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| type Group struct { loader *singleflight.Group }
func NewGroup(name string, cacheBytes int64, getter Getter) *Group { g := &Group{ loader: new(singleflight.Group), } }
|
修改 load 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| func (g *Group) load(key string) (ByteView, error) { view, err := g.loader.Do(key, func() (interface{}, error) { if g.peers != nil { if peer, ok := g.peers.PickPeer(key); ok { view, err := g.getFromPeer(peer, key) if err != nil { log.Println("[GeeCache] Failed to get from peer", err) return ByteView{}, err } return view, nil } } return g.getLocally(key) }) if err != nil { return ByteView{}, err } return view.(ByteView), nil }
|