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语言map与string的相互转换的实现
Apr 07 Golang
Golang二维切片初始化的实现
Apr 08 Golang
Goland使用Go Modules创建/管理项目的操作
May 06 Golang
golang 实现时间戳和时间的转化
May 07 Golang
go语言中http超时引发的事故解决
Jun 02 Golang
Golang 语言控制并发 Goroutine的方法
Jun 30 Golang
Golang并发操作中常见的读写锁详析
Aug 30 Golang
Go 语言中 20 个占位符的整理
Oct 16 Golang
GO语言字符串处理函数之处理Strings包
Apr 14 Golang
GO语言异常处理分析 err接口及defer延迟
Apr 14 Golang
Golang获取List列表元素的四种方式
Apr 20 Golang
Go中使用gjson来操作JSON数据的实现
Aug 14 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 截取字符串 分别适合GB2312和UTF8编码情况
2009/02/12 PHP
用PHP获取Google AJAX Search API 数据的代码
2010/03/12 PHP
PHP模板引擎smarty详细介绍
2015/05/26 PHP
PHP经典算法集锦【经典收藏】
2016/09/14 PHP
php实现微信企业号支付个人的方法详解
2017/07/26 PHP
用javascript实现读取txt文档的脚本
2007/07/20 Javascript
Raphael一个用于在网页中绘制矢量图形的Javascript库
2013/01/08 Javascript
nodejs中exports与module.exports的区别详细介绍
2013/01/14 NodeJs
jquery中EasyUI实现同步树
2015/03/01 Javascript
js实现简单鼠标跟随效果的方法
2015/04/10 Javascript
JavaScript 链式结构序列化详解
2016/09/30 Javascript
用headjs来管理和加载js 提高网站加载速度
2016/11/29 Javascript
JS实现的自动打字效果示例
2017/03/10 Javascript
Javascript网页抢红包外挂实现分享
2018/01/11 Javascript
Vue2.0系列之过滤器的使用
2018/03/01 Javascript
使用webpack3.0配置webpack-dev-server教程
2018/05/29 Javascript
nodejs 生成和导出 word的实例代码
2018/07/31 NodeJs
jsonp实现百度下拉框功能的方法分析
2019/05/10 Javascript
解决vue 使用setTimeout,离开当前路由setTimeout未销毁的问题
2020/07/21 Javascript
Vue实现一种简单的无限循环滚动动画的示例
2021/01/10 Vue.js
比较详细Python正则表达式操作指南(re使用)
2008/09/06 Python
一些关于python 装饰器的个人理解
2020/08/31 Python
Funko官方商店:源自美国,畅销全球搪胶收藏玩偶
2018/09/15 全球购物
Tessabit日本:集世界奢侈品和设计师品牌的意大利精品买手店
2020/01/07 全球购物
.NET里面什么时候需要调用垃圾回收
2015/06/01 面试题
教师研修随笔感言
2014/01/23 职场文书
吃空饷专项治理工作实施方案
2014/03/04 职场文书
品牌推广策划方案
2014/05/28 职场文书
村主任“四风”问题个人整改措施
2014/10/04 职场文书
先进事迹材料范文
2014/12/29 职场文书
教师个人发展总结
2015/02/11 职场文书
个人总结格式范文
2015/03/09 职场文书
导游词幽默开场白
2019/06/26 职场文书
详解MySQL主从复制及读写分离
2021/05/07 MySQL
golang 实用库gotable的具体使用
2021/07/01 Golang
Window server中安装Redis的超详细教程
2021/11/17 Redis