我的编程空间,编程开发者的网络收藏夹
学习永远不晚

SingleFlight模式的Go并发编程学习

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

SingleFlight模式的Go并发编程学习

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

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

应用场景

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

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

  • 防止缓存击穿。

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

通过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))。 

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

  • 判断是第一个请求的协程(利用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并发编程学习的详细内容,更多关于SingleFlight模式Go并发编程的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

SingleFlight模式的Go并发编程学习

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

SingleFlight模式的Go并发编程实例分析

这篇文章主要介绍了SingleFlight模式的Go并发编程实例分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇SingleFlight模式的Go并发编程实例分析文章都会有所收获,下面我们一起来看看吧。在go
2023-06-30

JUC 并发编程学习笔记(总)

文章目录 1. 什么是JUC2. 进程和线程2.1 进程2.2 线程2.3 并发2.4 并行2.5 线程的状态2.6 wait 和 sleep 的区别 3. Lock锁(重点)3.1 传统Synchronized3.2 Lock
2023-08-18

深入理解Go语言的并发编程:Go的并发模型解析

Go语言作为一门流行的编程语言,以其出色的并发编程能力而闻名。并发编程是在同一时间内执行多个独立的任务,通过充分利用多核处理器的性能以提高程序的性能和效率。在Go语言中,并发编程是一种非常简单、直观和高效的方式来编写并行程序。本文将深入探讨
深入理解Go语言的并发编程:Go的并发模型解析
2024-03-04

Java并发编程学习方法是什么

这篇文章主要讲解了“Java并发编程学习方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java并发编程学习方法是什么”吧!一:并发基础和多线程首先需要学习的就是并发的基础知识,什么
2023-06-02

Go语言开发快速学习CGO编程

这篇文章主要为大家介绍了Go语言开发之快速学习CGO编程,看了本文你就会发现CGO编程其实没有想象的那么难,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-05-14
Java 并发集合的学习资源汇总:成为并发编程大师
2024-04-03

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录