go-zero 微服务框架中提供了许多开箱即用的工具,好的工具不只能提高服务的性能并且还能提高代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等。git
本文主要讲述进程内共享调用神器 SharedCallsgithub
使用场景
并发场景下,可能会有多个线程(协程)同时请求同一份资源,若是每一个请求都要走一遍资源的请求过程,除了比较低效以外,还会对资源服务形成并发的压力。举一个具体例子,好比缓存失效,多个请求同时到达某服务请求某资源,该资源在缓存中已经失效,此时这些请求会继续访问DB作查询,会引发数据库压力瞬间增大。而使用 SharedCalls 可使得同时多个请求只须要发起一次拿结果的调用,其余请求"不劳而获",这种设计有效减小了资源服务的并发压力,能够有效防止缓存击穿。sql
高并发场景下,当某个热点 key 缓存失效后,多个请求会同时从数据库加载该资源,并保存到缓存,若是不作防范,可能会致使数据库被直接打死。针对这种场景,go-zero 框架中已经提供了实现,具体可参看 sqlc 和 mongoc 等实现代码。数据库
为了简化演示代码,咱们经过多个线程同时去获取一个 id 来模拟缓存的场景。以下:缓存
func main() { const round = 5 var wg sync.WaitGroup barrier := syncx.NewSharedCalls() wg.Add(round) for i := 0; i < round; i++ { // 多个线程同时执行 go func() { defer wg.Done() // 能够看到,多个线程在同一个 key 上去请求资源,获取资源的实际函数只会被调用一次 val, err := barrier.Do("once", func() (interface{}, error) { // sleep 1秒,为了让多个线程同时取 once 这个 key 上的数据 time.Sleep(time.Second) // 生成了一个随机的 id return stringx.RandId(), nil }) if err != nil { fmt.Println(err) } else { fmt.Println(val) } }() } wg.Wait() }
运行,打印结果为:微信
837c577b1008a0db 837c577b1008a0db 837c577b1008a0db 837c577b1008a0db 837c577b1008a0db
能够看出,只要是同一个 key 上的同时发起的请求,都会共享同一个结果,对获取 DB 数据进缓存等场景特别有用,能够有效防止缓存击穿。并发
关键源码分析
-
SharedCalls interface 提供了 Do 和 DoEx 两种方法的抽象框架
type SharedCalls interface { Do(key string, fn func() (interface{}, error)) (interface{}, error) DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error) }
-
SharedCalls interface 的具体实现 sharedGroup函数
// call 表明对指定资源的一次请求 type call struct { wg sync.WaitGroup // 用于协调各个请求 goroutine 之间的资源共享 val interface{} // 用于保存请求的返回值 err error // 用于保存请求过程当中发生的错误 } type sharedGroup struct { calls map[string]*call lock sync.Mutex }
-
sharedGroup 的 Do 方法微服务
- key 参数:能够理解为资源的惟一标识。
- fn 参数:真正获取资源的方法。
- 处理过程分析:
// 当多个请求同时使用 Do 方法请求资源时 func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) { // 先申请加锁 g.lock.Lock() // 根据 key,获取对应的 call 结果,并用变量 c 保存 if c, ok := g.calls[key]; ok { // 拿到 call 之后,释放锁,此处 call 可能尚未实际数据,只是一个空的内存占位 g.lock.Unlock() // 调用 wg.Wait,判断是否有其余 goroutine 正在申请资源,若是阻塞,说明有其余 goroutine 正在获取资源 c.wg.Wait() // 当 wg.Wait 再也不阻塞,表示资源获取已经结束,能够直接返回结果 return c.val, c.err } // 没有拿到结果,则调用 makeCall 方法去获取资源,注意此处仍然是锁住的,能够保证只有一个 goroutine 能够调用 makecall c := g.makeCall(key, fn) // 返回调用结果 return c.val, c.err }
-
sharedGroup 的 DoEx 方法
- 和 Do 方法相似,只是返回值中增长了布尔值表示值是调用 makeCall 方法直接获取的,仍是取的共享成果
func (g *sharedGroup) DoEx(key string, fn func() (interface{}, error)) (val interface{}, fresh bool, err error) { g.lock.Lock() if c, ok := g.calls[key]; ok { g.lock.Unlock() c.wg.Wait() return c.val, false, c.err } c := g.makeCall(key, fn) return c.val, true, c.err }
-
sharedGroup 的 makeCall 方法
- 该方法由 Do 和 DoEx 方法调用,是真正发起资源请求的方法。
// 进入 makeCall 的必定只有一个 goroutine,由于要拿锁锁住的 func (g *sharedGroup) makeCall(key string, fn func() (interface{}, error)) *call { // 建立 call 结构,用于保存本次请求的结果 c := new(call) // wg 加 1,用于通知其余请求资源的 goroutine 等待本次资源获取的结束 c.wg.Add(1) // 将用于保存结果的 call 放入 map 中,以供其余 goroutine 获取 g.calls[key] = c // 释放锁,这样其余请求的 goroutine 才能获取 call 的内存占位 g.lock.Unlock() defer func() { // delete key first, done later. can't reverse the order, because if reverse, // another Do call might wg.Wait() without get notified with wg.Done() g.lock.Lock() delete(g.calls, key) g.lock.Unlock() // 调用 wg.Done,通知其余 goroutine 能够返回结果,这样本批次全部请求完成结果的共享 c.wg.Done() }() // 调用 fn 方法,将结果填入变量 c 中 c.val, c.err = fn() return c }
总结
本文主要介绍了 go-zero 框架中的 SharedCalls 工具,对其应用场景和关键代码作了简单的梳理,但愿本篇文章能给你们带来一些收获。
项目地址
https://github.com/tal-tech/go-zero