Golang 并发编程 SingleFlight模式


Posted in Golang onApril 26, 2022

最近接触到微服务框架go-zero,翻看了整个框架代码,发现结构清晰、代码简洁,所以决定阅读源码学习下,本次阅读的源码位于core/syncx/singleflight.go

go-zeroSingleFlight的作用是:将并发请求合并成一个请求,以减少对下层服务的压力。

应用场景

  • 查询缓存时,合并请求,提升服务性能。 假设有一个 IP 查询的服务,每次用户请求先在缓存中查询一个 IP 的归属地,如果缓存中有结果则直接返回,不存在则进行 IP 解析操作。

Golang 并发编程 SingleFlight模式

如上图所示,n 个用户请求查询同一个 IP(8.8.8.8)就会对应 n 个 Redis 的查询,在高并发场景下,如果能将 n 个 Redis 查询合并成一个 Redis 查询,那么性能肯定会提升很多,而 SingleFlight就是用来实现请求合并的,效果如下:

Golang 并发编程 SingleFlight模式

  • 防止缓存击穿。

缓存击穿问题是指:在高并发的场景中,大量的请求同时查询一个 key ,如果这个 key 正好过期失效了,就会导致大量的请求都打到数据库,导致数据库的连接增多,负载上升。

Golang 并发编程 SingleFlight模式

通过SingleFlight可以将对同一个Key的并发请求进行合并,只让其中一个请求到数据库进行查询,其他请求共享同一个结果,可以很大程度提升并发能力。

应用方式

直接上代码:

func main() {
  round := 10
  var wg sync.WaitGroup
  barrier := syncx.NewSingleFlight()
  wg.Add(round)
  for i := 0; i < round; i++ {
    go func() {
      defer wg.Done()
      // 启用10个协程模拟获取缓存操作
      val, err := barrier.Do("get_rand_int", func() (interface{}, error) {
        time.Sleep(time.Second)
        return rand.Int(), nil
      })
      if err != nil {
        fmt.Println(err)
      } else {
        fmt.Println(val)
      }
    }()
  }
  wg.Wait()
}

以上代码,模拟 10 个协程请求 Redis 获取一个 key 的内容,代码很简单,就是执行Do()方法。其中,接收两个参数,第一个参数是获取资源的标识,可以是 redis 中缓存的 key,第二个参数就是一个匿名函数,封装好要做的业务逻辑。最终获得的结果如下:

5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410

从上看出,10个协程都获得了同一个结果,也就是只有一个协程真正执行了rand.Int()获取了随机数,其他的协程都共享了这个结果。

源码解析

先看代码结构:

type (
  // 定义接口,有2个方法 Do 和 DoEx,其实逻辑是一样的,DoEx 多了一个标识,主要看Do的逻辑就够了
  SingleFlight interface {
    Do(key string, fn func() (interface{}, error)) (interface{}, error)
    DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error)
  }
  // 定义 call 的结构
  call struct {
    wg  sync.WaitGroup // 用于实现通过1个 call,其他 call 阻塞
    val interface{}    // 表示 call 操作的返回结果
    err error          // 表示 call 操作发生的错误
  }
  // 总控结构,实现 SingleFlight 接口
  flightGroup struct {
    calls map[string]*call // 不同的 call 对应不同的 key
    lock  sync.Mutex       // 利用锁控制请求
  }
)

然后看最核心的Do方法做了什么事情:

func (g *flightGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
  c, done := g.createCall(key)
  if done {
    return c.val, c.err
  }
  g.makeCall(c, key, fn)
  return c.val, c.err
}

代码很简洁,利用g.createCall(key)对 key 发起 call 请求(其实就是做一件事情),如果此时已经有其他协程已经在发起 call 请求就阻塞住(done 为 true 的情况),等待拿到结果后直接返回。如果 done 是 false,说明当前协程是第一个发起 call 的协程,那么就执行g.makeCall(c, key, fn)真正地发起 call 请求(此后的其他协程就阻塞在了g.createCall(key))。 

Golang 并发编程 SingleFlight模式

从上图可知,其实关键就两步:

  • 判断是第一个请求的协程(利用map)
  • 阻塞住其他所有协程(利用 sync.WaitGroup)

来看下g.createCall(key)如何实现的:

func (g *flightGroup) createCall(key string) (c *call, done bool) {
  g.lock.Lock()
  if c, ok := g.calls[key]; ok {
    g.lock.Unlock()
    c.wg.Wait()
    return c, true
  }
  c = new(call)
  c.wg.Add(1)
  g.calls[key] = c
  g.lock.Unlock()
  return c, false
}

先看第一步:判断是第一个请求的协程(利用map)

g.lock.Lock()
if c, ok := g.calls[key]; ok {
  g.lock.Unlock()
  c.wg.Wait()
  return c, true
}

此处判断 map 中的 key 是否存在,如果已经存在,说明已经有其他协程在请求了,当前这个协程只需要等待,等待是利用了sync.WaitGroupWait()方法实现的,此处还是很巧妙的。要注意的是,map 在 Go 中是非并发安全的,所以需要加锁。

再看第二步:阻塞住其他所有协程(利用 sync.WaitGroup)

c = new(call)
c.wg.Add(1)
g.calls[key] = c

因为是第一个发起 call 的协程,所以需要 new 这个 call,然后将wg.Add(1),这样就对应了上面的wg.Wait(),阻塞剩下的协程。随后将 new 的 call 放入 map 中,注意此时只是完成了初始化,并没有真正去执行call请求,真正的处理逻辑在 g.makeCall(c, key, fn)中。

func (g *flightGroup) makeCall(c *call, key string, fn func() (interface{}, error)) {
  defer func() {
    g.lock.Lock()
    delete(g.calls, key)
    g.lock.Unlock()
    c.wg.Done()
  }()
  c.val, c.err = fn()
}

这个方法中做的事情很简单,就是执行了传递的匿名函数fn()(也就是真正call请求要做的事情)。最后处理收尾的事情(通过defer),也是分成两步:

  • 删除 map 中的 key,使得下次发起请求可以获取新的值。
  • 调用wg.Done(),让之前阻塞的协程全部获得结果并返回。

至此,SingleFlight 的核心代码就解析完毕了,虽然代码不长,但是这个思想还是很棒的,可以在实际工作中借鉴。

总结

  • map 非并发安全,记得加锁。
  • 巧用 sync.WaitGroup 去完成需要阻塞控制协程的应用场景。
  • 通过匿名函数 fn 去封装传递具体业务逻辑,在调用 fn 的上层函数中去完成统一的逻辑处理。

项目地址

https://github.com/zeromicro/go-zero

以上就是SingleFlight模式的Go并发编程学习的详细内容!


Tags in this post...

Golang 相关文章推荐
Go语言使用select{}阻塞main函数介绍
Apr 25 Golang
彻底理解golang中什么是nil
Apr 29 Golang
使用Golang的channel交叉打印两个数组的操作
Apr 29 Golang
解决goland 导入项目后import里的包报红问题
May 06 Golang
Go 语言下基于Redis分布式锁的实现方式
Jun 28 Golang
Go 语言结构实例分析
Jul 04 Golang
golang连接MySQl使用sqlx库
Apr 14 Golang
Golang bufio详细讲解
Apr 21 Golang
Golang 实现WebSockets
Apr 24 Golang
Go微服务项目配置文件的定义和读取示例详解
Jun 21 Golang
基于Python实现西西成语接龙小助手
Aug 05 Golang
Golang 实现 WebSockets 之创建 WebSockets
Apr 24 #Golang
Golang 实现WebSockets
Golang ort 中的sortInts 方法
Apr 24 #Golang
Golang 切片(Slice)实现增删改查
Apr 22 #Golang
Golang 结构体数据集合
Apr 22 #Golang
Golang map映射的用法
Apr 22 #Golang
Golang bufio详细讲解
Apr 21 #Golang
You might like
php删除txt文件指定行及按行读取txt文档数据的方法
2017/01/30 PHP
PHP PDOStatement::getColumnMeta讲解
2019/02/01 PHP
捕获关闭窗口的脚本
2009/01/10 Javascript
js浏览器本地存储store.js介绍及应用
2014/05/13 Javascript
Node.js 去掉种子(torrent)文件里的邪恶信息
2015/03/27 Javascript
学习javascript面向对象 理解javascript原型和原型链
2016/01/04 Javascript
给angular加上动画效遇到的问题总结
2016/02/17 Javascript
Jquery修改image的src属性,图片不加载问题的解决方法
2016/05/17 Javascript
angular-ngSanitize模块-$sanitize服务详解
2017/06/13 Javascript
JS 学习总结之正则表达式的懒惰性和贪婪性
2017/07/03 Javascript
js实现多张图片延迟加载效果
2017/07/17 Javascript
angularjs 的数据绑定实现原理
2018/07/02 Javascript
解决vue keep-alive 数据更新的问题
2018/09/21 Javascript
基于Angular中ng-controller父子级嵌套的相关属性详解
2018/10/08 Javascript
JS实现的获取银行卡号归属地及银行卡类型操作示例
2019/01/08 Javascript
解决layer弹出层中表单不起作用的问题
2019/09/09 Javascript
node.js中Buffer缓冲器的原理与使用方法分析
2019/11/23 Javascript
Openlayers学习之加载鹰眼控件
2020/09/28 Javascript
原生js拖拽功能制作滑动条实例代码
2021/02/05 Javascript
[01:10:57]Liquid vs OG 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
haskell实现多线程服务器实例代码
2013/11/26 Python
python计算牛顿迭代多项式实例分析
2015/05/07 Python
利用Python中unittest实现简单的单元测试实例详解
2017/01/09 Python
Django数据库操作的实例(增删改查)
2017/09/04 Python
Python模拟脉冲星伪信号频率实例代码
2018/01/03 Python
Python实现RGB与HSI颜色空间的互换方式
2019/11/27 Python
Python 按比例获取样本数据或执行任务的实现代码
2020/12/03 Python
解析HTML5的存储功能和web SQL的相关操作方法
2016/02/19 HTML / CSS
照片礼物和装饰:MyPhoto
2019/11/02 全球购物
Oral-B荷兰:牙医最推荐的品牌
2020/02/25 全球购物
Fanatics官网:运动服装、球衣、运动装备
2020/10/12 全球购物
电气个人求职信范文
2014/02/04 职场文书
热爱祖国演讲稿
2014/05/04 职场文书
政府班子四风问题整改措施
2014/10/04 职场文书
2016国庆促销广告语
2016/01/28 职场文书
「约定的梦幻岛」作画发布诺曼生日新绘
2022/03/21 日漫