缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
groupcache是golang实现的分布式缓存,和memcache同一作者出品,groupcache使用重复抑制机制(singeflight)用了很少了代码提供了缓存击穿的解决方式。代码github地址:https://github.com/golang/groupcache/blob/master/singleflight/singleflight.go
// Group represents a class of work and forms a namespace in which
// units of work can be executed with duplicate suppression.
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
Group表示一个工作类,并形成一个命名空间,在该命名空间中,可以使用重复抑制来执行工作单元。
Group中的map是执行的命名空间,mu则保证map的安全性。
// call is an in-flight or completed Do call
type call struct {
wg sync.WaitGroup
val interface{}
err error
}
call结构体可以保存执行的结果,并使用WaitGroup让并发获取结果的其他goroutine可以等待执行的携程执行完并获取结果。
val和err则保存了执行结果和错误。
// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
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)
}
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的函数。如果出现重复,则重复调用方将等待正在执行的相同key函数完成并接收相同的结果。key用来标识操作,fn则执行操作,如果key当前没在执行的话,封装成call加入命名空间标识该key在执行,WaitGroup加1,然后开始执行,执行完后WaitGroup放开。
如果命名空间中已经存在key了说明该key已经在执行,那么就是使用WaitGroup等待执行结果即可。
代码相当简单通用。
测试用例分别测试了Do执行功能,Do执行返回错误情形,重复抑制功能。直接在原始代码中加注释来展示。
import (
"errors"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
)
//测试Do功能,传入一个固定函数,比较返回与预期
func TestDo(t *testing.T) {
var g Group
v, err := g.Do("key", func() (interface{}, error) {
return "bar", nil
})
if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
t.Errorf("Do = %v; want %v", got, want)
}
if err != nil {
t.Errorf("Do error = %v", err)
}
}
//测试Do返回错误功能,传入一个固定函数,比较错误返回与预期
func TestDoErr(t *testing.T) {
var g Group
someErr := errors.New("Some error")
v, err := g.Do("key", func() (interface{}, error) {
return nil, someErr
})
if err != someErr {
t.Errorf("Do error = %v; want someErr", err)
}
if v != nil {
t.Errorf("unexpected non-nil value %#v", v)
}
}
//重复抑制测试
//
func TestDoDupSuppress(t *testing.T) {
var g Group
c := make(chan string)
//记录调用次数
var calls int32
//每调用一次calls加一
fn := func() (interface{}, error) {
atomic.AddInt32(&calls, 1)
return <-c, nil
}
const n = 10
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
v, err := g.Do("key", fn)
//每一个goroutine判断结果是否符合预期
if err != nil {
t.Errorf("Do error: %v", err)
}
if v.(string) != "bar" {
t.Errorf("got %q; want %q", v, "bar")
}
wg.Done()
}()
}
time.Sleep(100 * time.Millisecond) // let goroutines above block
//睡眠让所有goroutine执行到等待结果
//传入值,函数执行结束
c <- "bar"
//等待全部执行结束
wg.Wait()
//判断是否执行一次
if got := atomic.LoadInt32(&calls); got != 1 {
t.Errorf("number of calls = %d; want 1", got)
}
}
另外,golang还提供了一个更通用单飞版本,地址:https://github.com/golang/sync/tree/master/singleflight