深入解析go 缓存击穿方案-singleflight

1.问题

描述:某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。

曾经有面试官问我这个问题,我将解决方法说了,然后提到singleflight ,问我源码,一时语塞(▼皿▼#),只能说曾经看过,所以写下这篇文章,记录一下。

2.解决方法

  • 给key设置随机过期时间

  • 让多个请求请求数据库只有一个连接成功(singleflight 实现思路)

3.singleflight源码解析

github地址

golang.org/x/sync/singleflight

使用示例

func TestDoDupSuppress(t *testing.T) {
    var g Group
    c := make(chan string)
    var calls int32
    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)
            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
    c <- "bar"
    wg.Wait()
    if got := atomic.LoadInt32(&calls); got != 1 {
        t.Errorf("number of calls = %d; want 1", got)
    }
}
​
  • 从上面官方测试例子可以看出,10个并发请求执行将变量calls+1,最后只有1个请求,执行成功,所有calls 依然为1

3.1new

从例子看,只需要直接new就行了,或者直接声明,没有提供相关构造函数

var g singleflight.Group

group 结构,m代表每个key保存call信息

// 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  //赖初始化,意思是使用时初始化
}

call 结构

// call is an in-flight or completed Do call
type call struct {
    wg  sync.WaitGroup
    val interface{} //存储fn返回值
    err error
}

3.2do方法

// 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 { //如果该key能找到,那么该key之前请求过了,拿出来阻塞等待完成就行了
        g.mu.Unlock()
        c.wg.Wait()
        return c.val, c.err
    }
    c := new(call) //如果没有,wg添加1,然后给g.m赋值
    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) //将执行完的key进行删除
    g.mu.Unlock()
​
    return c.val, c.err
}
  • 每次调用do方法执行函数,要求添加一个key

  • 当key存在时,说明,之前已经有协程在执行了,等待之前的协程执行完,然后返回

  • key不存在时生成call添加

  • 最后执行完将key删除

  • 避免缓存击穿的话,给请求数据库的连接设置相同key,当多个连接到达时,只有一个执行成功,其他连接等待执行成功,然后返回,那么下一次执行时,其他连接直接从缓存中获取。所以可以有效避免缓存击穿问题。

你可能感兴趣的:(go,golang,缓存)