GEE 组件篇 – singleflight
详细可查看https://github.com/OddSpilit/gee/blob/master/gee/geecache/singleflight/singleflight.go 源码
singleflight
我相信大家应该都挺熟悉的,主要的功能就是对相同key
重复的取数操作简化为一次操作。这次的流程主要有以下几点:- 代码分析
- 代码测试
- 现有
singleflight
工具比较
代码分析
-
两个结构体:
call
&&Group
, 一个方法Do
-
call
结构体主要是用来保存数据的值,以及通过sync.WaitGroup
包来控制并发解锁后让重复获取相同key
的请求处于等待状态。 -
Group
:锁负责控制并发,m是一个map类型,负责存储键值对。-
Do
方法:我们贴上代码解析一下这块流程:/** 每次大量请求, 控制fn函数执行次数 */ func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() // 1. 请求进来即锁住 if g.m[key] == nil { g.m = make(map[string]*call) } // 2. 如果是第二次以及之后的请求进来,就直接通过Wait操作等待Done操作信息(获取相同key) if c, ok := g.m[key]; ok { g.mu.Unlock() c.wg.Wait() return c.val, c.err } // 3. 首次进来逻辑: c := new(call) c.wg.Add(1) g.m[key] = c g.mu.Unlock() // 已经设置了key => c, 没必要锁, 让继续进来的请求去到步骤2 c.val, c.err = fn() // 执行方法, 获取数据 c.wg.Done() // 告知已完成值跟错误信息的设置 // 貌似这块可以不加锁操作 g.mu.Lock() delete(g.m, key) // 删除,保证每次第一次进来都不会走到步骤2 g.mu.Unlock() return c.val, c.err }
-
代码测试
现有singleflight
工具比较
-
结构体对比:
call
结构体增加了dups int
成员,因为调用Do
方法的时候会返回是否是数据共享(即是否重复获取了一个key
值),如果dups > 0
即为true
,还有chans []chan<- Result
保存异步数据结果集- 新增了
panicError
结构体,自定义错误信息 - 新增了
Result
结构体,主要是服务于DoChan
方法,通过返回只读通道支持异步操作过程,里面的操作跟Do
差不多,只是把fn
结果集通过chan
收集起来了
-
方法对比:
-
Do
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) { g.mu.Lock() if g.m == nil { g.m = make(map[string]*call) } // 主要是这里有点区别 if c, ok := g.m[key]; ok { c.dups++ g.mu.Unlock() c.wg.Wait() // 判断是否出现异常错误或者自定义的错误,这些错误会在`doCall`方法体现出来 if e, ok := c.err.(*panicError); ok { panic(e) } else if c.err == errGoexit { runtime.Goexit() } return c.val, c.err, true } c := new(call) c.wg.Add(1) g.m[key] = c g.mu.Unlock() g.doCall(c, key, fn) return c.val, c.err, c.dups > 0 }
-
DoChan
: 这一块代码比较简单,读者可以自己分析一下,重点讲一下doChan
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result { ch := make(chan Result, 1) g.mu.Lock() if g.m == nil { g.m = make(map[string]*call) } if c, ok := g.m[key]; ok { c.dups++ c.chans = append(c.chans, ch) g.mu.Unlock() return ch } c := &call{chans: []chan<- Result{ch}} c.wg.Add(1) g.m[key] = c g.mu.Unlock() go g.doCall(c, key, fn) return ch }
-
doCall
: 执行了两个defer
操作func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) { // 通过两个 defer 巧妙的区分了到底是发生了 panic 还是用户主动调用了 runtime.Goexit normalReturn := false recovered := false // 最后执行的逻辑,先看下面的操作 defer func() { // 正常时这里的normalReturn跟recovered为true if !normalReturn && !recovered { c.err = errGoexit } g.mu.Lock() defer g.mu.Unlock() c.wg.Done() if g.m[key] == c { delete(g.m, key) } // fn 执行出错 if e, ok := c.err.(*panicError); ok { // Dochan操作,因为DoChan是异步操作,用select{}做阻塞操作,其实这里就是会让panic信息输出出来,不会因为recover操作而不输出 if len(c.chans) > 0 { go panic(e) select {} } else { panic(e) } } else if c.err == errGoexit { // 执行了runtime.Goexit退出 } else { // 收集数据 for _, ch := range c.chans { ch <- Result{c.val, c.err, c.dups > 0} } } }() func() { defer func() { // 如果fn方法异常,实例化panic if !normalReturn { if r := recover(); r != nil { c.err = newPanicError(r) } } }() c.val, c.err = fn() // 获取数据来到了这里 // 定义为正常退出 normalReturn = true }() // 如果不是正常退出,进来了就是recover错误类型 if !normalReturn { recovered = true } }
-
Forget
:手动移除某个key的数据func (g *Group) Forget(key string) { g.mu.Lock() delete(g.m, key) g.mu.Unlock() }
-
总结
- 之前在写这块代码的时候就觉得这个操作很轻巧,作为缓存,他不会说需要等待结果收集完成之后再把运行权交出去,而是会让重复的请求先做一个等待操作,并且第一次获取到的结果又是可以共享的。
singleflight
是一个阅读难度不大,但是能学到很多东西的一个防缓存穿透利器。- 其实我是先打算讲
lru
但是我觉得singleflight
的难度相比lru跟一致性hash
还是简单的,也先让读者对缓存这块内容有个简单的了解。 - 后面还是着重讲缓存这一方面的内容为后面重头戏
geecache
做一个铺垫。👻👻