什么是缓存击穿
我们常说的缓存问题:缓存雪崩,缓存击穿,缓存穿透都分别指什么呢?
简单来说:
- 缓存雪崩是指缓存在同一时间全部失效,导致压力全部转移到DB上。
- 缓存击穿指的是某个key在失效的这一刻,有大量的请求数据,这些数据压力也转移到了DB。
- 缓存穿透指的是大量数据访问一个不存在的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
}