防止缓存击穿之进程内共享调用 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
kevinwan

防止缓存击穿之进程内共享调用

  •  
  •   kevinwan Sep 19, 2020 2414 views
    This topic created in 2049 days ago, the information mentioned may be changed or developed.

    go-zero 微服务框架中提供了许多开箱即用的工具,好的工具不仅能提升服务的性能而且还能提升代码的鲁棒性避免出错,实现代码风格的统一方便他人阅读等等。

    本文主要讲述进程内共享调用神器 SharedCalls

    使用场景

    并发场景下,可能会有多个线程(协程)同时请求同一份资源,如果每个请求都要走一遍资源的请求过程,除了比较低效之外,还会对资源服务造成并发的压力。举一个具体例子,比如缓存失效,多个请求同时到达某服务请求某资源,该资源在缓存中已经失效,此时这些请求会继续访问 DB 做查询,会引起数据库压力瞬间增大。而使用 SharedCalls 可以使得同时多个请求只需要发起一次拿结果的调用,其他请求"坐享其成",这种设计有效减少了资源服务的并发压力,可以有效防止缓存击穿。

    高并发场景下,当某个热点 key 缓存失效后,多个请求会同时从数据库加载该资源,并保存到缓存,如果不做防范,可能会导致数据库被直接打死。针对这种场景,go-zero 框架中已经提供了实现,具体可参看 sqlcmongoc 等实现代码。

    为了简化演示代码,我们通过多个线程同时去获取一个 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 sharedGrop 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

    微信交流群

    1 replies    2020-09-21 00:31:17 +08:00
    About     Help     Advertise     Blog     API     FAQ     Solana     971 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 32ms UTC 23:23 PVG 07:23 LAX 16:23 JFK 19:23
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86