golang groupcache重复抑制(singeflight)机制,防止缓存击穿

    缓存击穿,是指一个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

你可能感兴趣的:(golang)