single flight 防止缓存击穿

什么是缓存击穿

我们常说的缓存问题:缓存雪崩,缓存击穿,缓存穿透都分别指什么呢?
简单来说:

  1. 缓存雪崩是指缓存在同一时间全部失效,导致压力全部转移到DB上。
  2. 缓存击穿指的是某个key在失效的这一刻,有大量的请求数据,这些数据压力也转移到了DB。
  3. 缓存穿透指的是大量数据访问一个不存在的key,导致每次都必须请求DB,DB压力过大。

如何减少瞬间压力

像缓存雪崩这种现象,一般是由于缓存服务器宕机(key哈希失效),大量的key设置了相同失效时间导致。可以通过哈希环或者人为控制key的失效时间来避免。

对于击穿和穿透来说,他们的共同点是同一时间涌入大量相同请求,这些相同请求没必要每次都重新处理一遍,如果我们能将这些请求绑定在一起,返回同一个结果,就能减少大量的并发压力。

single flight机制

single flight就是上述思想的实现,groupcache(https://github.com/golang/groupcache/blob/master/singleflight/singleflight.go)
和官方(https://pkg.go.dev/golang.org/x/sync/singleflight)都有各自的实现。

这里说一下groupcache中的实现:整体上使用mutex+waitgroup控制多个线程的同步,第一个到来的请求必然需要进行处理,其他到来的请求如果发现该key正在被处理,就被waitgroup阻塞,直到第一个请求处理结束,将结果赋给类似全局变量的call,剩余请求共享这个call,将结果直接返回。

package singleflight

import "sync"

type Call struct {
	wg  sync.WaitGroup
	val interface{}
	err error
}

type Group struct {
	mu    sync.Mutex
	calls map[string]*Call
}

func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
	g.mu.Lock()

	if g.calls == nil {
		g.calls = make(map[string]*Call)
	}

	// 其他线程访问,同时等待相同请求的结果
	if c, ok := g.calls[key]; ok {
		g.mu.Unlock()
		c.wg.Wait()
		return c.val, c.err
	}

	c := new(Call)
	c.wg.Add(1)
	g.calls[key] = c
	g.mu.Unlock()

	// 处理请求
	c.val, c.err = fn()
	c.wg.Done()

	g.mu.Lock()
	delete(g.calls, key) // 无用的key要回收,避免map一直增大
	g.mu.Unlock()

	return c.val, c.err
}

posted @ 2021-12-07 09:52  moon_orange  阅读(472)  评论(0编辑  收藏  举报