GroupCache源码分析(2):singleflight
(1)概述:
在groupcache分布式缓存中有一个问题:如果在某一瞬间,队尾节点被淘汰,然后突然涌进一大批请求原队尾节点的数据,这样就可能造成缓存击穿。
缓存雪崩:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。
缓存击穿:一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时DB请求量大、压力骤增。
缓存穿透:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB,如果瞬间流量过大,穿透到 DB,导致宕机。
因此,singleflight就是用来解决这个问题的。其核心功能是:
(2) 源码:
// call is an in-flight or completed Do call
// call 是请求的数据结构,使用WaitGroup实现等待功能
type call struct {
wg sync.WaitGroup
val interface{}
err error
}
看一下 sync.WaitGroup的使用:
等待组的方法
方法名 | 功能 |
---|---|
(wg * WaitGroup) Add(delta int) | 等待组计数器加1 |
(wg * WaitGroup) Done() | 等待组计数器减一 |
(wg * WaitGroup) Wait() | 当等待组计数器不等于 0 时阻塞直到变 0。 |
// 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)
// 使用mu可以抑制重复请求被执行
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
看一下 sync.Mutex的使用:
以下mutex介绍来源于简书作者:JunChow520
sync.Mutex是一个互斥锁,可以由不同的goroutine加锁和解锁。
Go标准库提供了sync.Mutex互斥锁类型以及两个方法分别是Lock加锁和Unlock释放锁。可以通过在代码前调用Lock方法,在代码后调用Unlock方法来保证一段代码的互斥执行,也可以使用defer语句来保证互斥锁一定会被解锁。当一个goroutine调用Lock方法获得锁后,其它请求的goroutine都会阻塞在Lock方法直到锁被释放。
一个互斥锁只能同时被一个goroutine锁定,其它goroutine将阻塞直到互斥锁被解锁,也就是重新争抢对互斥锁的锁定。需要注意的是,对一个未锁定的互斥锁解锁时将会产生运行时错误。
sync.Mutex不区分读写锁,只有Lock()和Lock()之间才会导致阻塞的情况。若在一个地方Lock(),在另一个地方不Lock()而是直接修改或访问共享数据,对于sync.Mutext类型是允许的,因为mutex不会和goroutine进行关联。若要区分读锁和写锁,可使用sync.RWMutex类型。
在Lock()和Unlock()之间的代码段成为资源临界区(critical section),在这一区间内的代码是严格被Lock()保护的,是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码。
最后则是singleflight的核心实现,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 的作用简而言之就是:使得同一时间内不管多少个请求打进来,都只会执行其中一个,然后返回相同结果
// 参数 fn 是一个函数,是用于节点挑选值时调用的
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
// 因为每个进程进来被选中概率是不一定的嘛,所以如果被挑中的那一个请求进来了,就给它上锁
// 那么同一时间内进来的另外那些相同的请求就没法再访问了
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
// 先跳过这个if,看完下面再回来看if的内容
// 第二个请求进来,这时如果 key 对应的请求正在进行中:
if c, ok := g.m[key]; ok {
// 给请求解锁
g.mu.Unlock()
// 当等待组计数器不等于 0 时阻塞直到变 0,因为这时第一个请求可能还没走到fn函数返回
// 对应的值,所以要等待它完成,然后再解锁,然后返回值
c.wg.Wait()
return c.val, c.err
}
c := new(call)
// 请求未完成计数器+1
c.wg.Add(1)
// 标记请求正在进行
g.m[key] = c
// 解锁
g.mu.Unlock()
// 调用fn函数获得val
c.val, c.err = fn()
// 呼叫请求结束,计数器减一
c.wg.Done()
// 上锁保证只对 key 删一次
g.mu.Lock()
delete(g.m, key)
g.mu.Unlock()
return c.val, c.err
}
singleflight的内容就到此。