groupcache源码阅读(三)——防止缓存惊群效应

本文最后更新于:2024年7月6日 早上

缓存惊群效应

惊群效应问题有时被称为缓存击穿,穿透或者雪崩效果。从本质上讲,就像是使系统不堪重负的大量请求。

缓存雪崩:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。

缓存击穿:一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时DB请求量大、压力骤增。

缓存穿透:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB,如果瞬间流量过大,穿透到 DB,导致宕机。

singleflight解析

groupcache 中的singleflight模块就是用来专门解决缓存惊群效应的,话不多说我们来看看源代码的实现逻辑。

Group

1
2
3
4
5
6
7
8
// Group represents a class of work and forms a namespace in which
// units of work can be executed with duplicate suppression.
// Group 是 singleflight 的最主要的数据结构,管理不同 key 的请求(call),
// 保证在上一次缓存结果没有关系前,本地不会发送更多的请求
type Group struct {
mu sync.Mutex // protects m 一个互斥锁,用来保护m
m map[string]*call // lazily initialized 一个懒加载的字典,存储需要被访问的key与其对应的单个访问器
}

Groupsingleflight的最主要的数据结构,管理不同 key 的请求(call),保证在上一次缓存结果没有关系前,本地不会发送更多的请求。当然也可以理解为,是本地的访问任务调度中心。

call

1
2
3
4
5
6
7
// call is an in-flight or completed Do call
// 正在进行中,或已经结束的请求
type call struct {
wg sync.WaitGroup // 等待多个协程完成,避免重入
val interface{} // 请求得到的正常结果
err error // 请求得到的异常结果
}

call是每个key从远端节点获取数据的正在进行中,或已经结束的请求。使用 sync.WaitGroup 锁避免重入。

Do

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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.
// Do 方法,接收 2 个参数,第一个参数是 key,第二个参数是一个函数 fn。
// Do 的作用就是,针对相同的 key,无论 Do 被调用多少次,函数 fn 都只会被调用一次,
// 等待 fn 调用结束了,返回返回值或错误
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.mu.Lock() // 上锁,防止其他协程进来修改m,干扰接下来的工作
if g.m == nil {
// 就是所谓的懒加载,
// 第一次先创建一个创建一个保存key请就任务的字典
g.m = make(map[string]*call)
}
// 获取到key的执行实例
if c, ok := g.m[key]; ok { //如果存在说明这个key有正在请求的call
// key的执行实例已经拿到了,先把整个Group的锁解开,
// 这里没有IO,预计所不会阻塞其他协程操作其他key太久
g.mu.Unlock()
c.wg.Wait() // 如果之前已经有人发起了这个缓存的请求正在进行中,则等待
return c.val, c.err // 等待完毕就返回别人请求到的缓存结果
}
// 代码走到这里,说明目前当前没有其他协程,在请求这个缓存
c := new(call) // 发起一个请求
c.wg.Add(1) // 准备开始开始工作,这个Group中的其他协程将等待我的请求结果
g.m[key] = c // 注册一下这个key的请求任务
g.mu.Unlock() // m 缓存的请求注册中心,操作完毕,交出锁

c.val, c.err = fn() // 执行key的远端请求任务,io部分
c.wg.Done() // 请求完毕,通知其他协程,可以那我的结果了

g.mu.Lock() // 给注册中心上个锁,准备删除掉本次请求
delete(g.m, key) //删删删
g.mu.Unlock() // m 缓存的请求注册中心,操作完毕,交出锁

return c.val, c.err // 返回结果
}

通过Do方法的调用,就是实现group内多个协程并发的请求的限制,有效的防止了高并发情况下,本出现内多个协程,同时对同一个key想远端节点发起大量不必要的请求。

singleflight总结

现在读完一遍源码,我们再来审视singleflight这个命名的含义——singleflight(单次航班)——言下之意,针对相同的货物(key缓存数据)运输请求,我们将只发起一次飞行计划(HTTP的远端节点访问)。


groupcache源码阅读(三)——防止缓存惊群效应
https://yance.wiki/缓存惊群效应/
作者
Yance Huang
发布于
2020年9月12日
许可协议