GroupCache源码分析(2):singleflight

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的内容就到此。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值