在我们之前的实现里,向服务端发送几次请求,很可能也向数据库发送几次请求,容易造成缓存击穿和穿透。
缓存击穿:对于一个热点数据key,缓存中不再缓存时,并发的多个请求都在请求数据库,数据库瞬时请求量大,压力骤增。
缓存穿透:并发的多个请求在请求一个不存在的key,每次都绕过缓存去请求数据库也会导致数据库瞬间压力过大。
singleflight包
package singleflight
import "sync"
// call类型,表示正在进行的请求
type call struct {
wg sync.WaitGroup
val interface{}
err error
}
// 用于管理请求的结构
type Group struct {
mu sync.Mutex // protects m
m map[string]*call
}
DO方法
// 管理请求的方法,无论do调用多少次查询key,只会调用依次fn
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
// 如果请求已经存在于map中,让其等待处理后一起返回值
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)
// 添加到map里便于查找
g.m[key] = c
g.mu.Unlock()
// 只有第一个协程调用函数,其余都在map里找到后wait中
c.val, c.err = fn()
c.wg.Done()
// 对于控制请求的map处理都要加锁
g.mu.Lock()
// 删除这次请求
delete(g.m, key)
g.mu.Unlock()
return c.val, c.err
}
singleflight 的使用
缓存池中使用
// 添加了Group对象,
type Group struct {
name string
getter Getter
mainCache cache
peers PeerPicker
// 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{},
}
return g
}
//
func (g *Group) load(key string) (value ByteView, err error) {
// 对于每个key请求,调用多次do函数,但是只会调用一次,从远程获取数据的函数
viewi, err := g.loader.Do(key, func() (interface{}, error) {
if g.peers != nil {
if peer, ok := g.peers.PickPeer(key); ok {
if value, err = g.getFromPeer(peer, key); err == nil {
return value, nil
}
log.Println("[GeeCache] Failed to get from peer", err)
}
}
return g.getLocally(key)
})
if err == nil {
return viewi.(ByteView), nil
}
return
}