缓存惊群效应

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

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

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

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

singleflight解析

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

Group

// 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

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

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

Do

// 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的远端节点访问)。


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

K3S本地测试环境搭建 上一篇
Yance的技能树(2020) 下一篇