防止缓存击穿
1. 缓存事故
缓存雪崩、缓存击穿和缓存穿透是缓存系统中常见的比较严重的问题。
缓存雪崩: 缓存在同一时刻全部失效,造成瞬间
DB
请求量过大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的key
设置了相同的过期时间等引起。
缓存击穿:一个存在的热点key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到
DB
,造成瞬间DB
请求量过大、压力骤增。
缓存穿透:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求
DB
,如果瞬间流量过大,穿透到DB
,导致宕机。
2. singleflight
的实现
当并发地向同一个节点发起N
个请求时,如果对数据库的访问没有做任何限制,很可能向数据库也会发起N
次请求,容易造成缓存击穿和穿透。即使对数据库做了防护,HTTP
请求也是十分消耗资源的操作,针对相同的key
,向同一个节点发起多次请求是完全没有必要的。
singleflight
GoCache
中设计了名为singleflight
的package
来解决这个问题。
// call 表示正在进行中或者已结束的请求
type call struct {
// 使用WaitGroup锁避免重入
wg sync.WaitGroup
val interface{}
err error
}
// Group 管理不同key的请求
type Group struct {
mu sync.Mutex //保护requests不被并发读写
requests map[string]*call //requests中存放正在进行中的请求
}
实现Do
方法,对相同key
发起的多次请求,只会调用一次请求函数。
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.mu.Lock()
if g.requests == nil {
g.requests = make(map[string]*call)
}
// 存在进行中的请求,c等待
if c, ok := g.requests[key]; ok {
g.mu.Unlock()
c.wg.Wait()
return c.val, c.err //请求结束,返回结果
}
// 没有进行中的请求
c := new(call)
c.wg.Add(1) //发起请求前加锁
g.requests[key] = c //添加到requests表中
g.mu.Unlock()
c.val, c.err = fn()
c.wg.Done() //请求结束
g.mu.Lock()
delete(g.requests, key) //删除表中请求,更新requests
g.mu.Unlock()
return c.val, c.err
}