通过 SingleFlight 模式学习 Go 并发编程
最近接触到微服务框架go-zero
,翻看了整个框架代码,发现结构清晰、代码简洁,所以决定阅读源码学习下,本次阅读的源码位于core/syncx/singleflight.go
。
在go-zero
中SingleFlight
的作用是:将并发请求合并成一个请求,以减少对下层服务的压力。
应用场景
- 查询缓存时,合并请求,提升服务性能。
假设有一个 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.WaitGroup
的Wait()
方法实现的,此处还是很巧妙的。要注意的是,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
欢迎使用 go-zero
并 star 支持我们!
微信交流群
关注『微服务实践』公众号并点击 交流群 获取社区群二维码。
通过 SingleFlight 模式学习 Go 并发编程的更多相关文章
- 如何深入学习Java并发编程?
在讲解深入学习Java并发编程的方法之前,先分析如下若干错误的观点和学习方法. 错误观点1:学习Java编程主要是学习多线程. 这话其实是说明了表面现象,多线程其实还真是并发编程的实现方式,但在实际高 ...
- python学习之并发编程
目录 一.并发编程之多进程 1.multiprocessing模块介绍 2.Process类的介绍 3.Process类的使用 3.1 创建开启子进程的两种方式 3.2 获取进程pid 3.3验证进程 ...
- [javaSE] 看博客学习java并发编程
共享性 多线程操作同一个数据,产生线程安全问题 新建一个类ShareData 设计一个int 型的成员变量count 设计一个成员方法addCount(),把count变量++ 在main函数中开启多 ...
- python学习之并发编程(理论部分)
第一章 操作系统 管理控制协调计算机中硬件与软件的关系. 操作系统的作用? 第一个作用: 将一些对硬件操作的复杂丑陋的接口,变成简单美丽的接口. open函数. 第二个作用: 多个进程抢占一个(CPU ...
- java 并发编程——Thread 源码重新学习
Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...
- Java工程师学习指南第4部分:Java并发编程指南
本文整理了微信公众号[Java技术江湖]发表和转载过的Java并发编程相关优质文章,想看到更多Java技术文章,就赶紧关注本公众号吧吧. [纯干货]Java 并发进阶常见面试题总结 [Java基本功] ...
- Java并发编程学习前期知识下篇
Java并发编程学习前期知识下篇 通过上一篇<Java并发编程学习前期知识上篇>我们知道了在Java并发中的可见性是什么?volatile的定义以及JMM的定义.我们先来看看几个大厂真实的 ...
- Java并发编程实战---第六章:任务执行
废话开篇 今天开始学习Java并发编程实战,很多大牛都推荐,所以为了能在并发编程的道路上留下点书本上的知识,所以也就有了这篇博文.今天主要学习的是任务执行章节,主要讲了任务执行定义.Executor. ...
- Java并发编程有多难?这几个核心技术你掌握了吗?
本文主要内容索引 1.Java线程 2.线程模型 3.Java线程池 4.Future(各种Future) 5.Fork/Join框架 6.volatile 7.CAS(原子操作) 8.AQS(并发同 ...
随机推荐
- 半吊子菜鸟学Web开发6 -- Vscode开发环境配置
1vscode上手一周不到,终于弄出点门路,终于弄清楚了点vscode的设置是什么样子的了....哭 2就我这两天的使用来看,一般vscode默认只让打开一个文件夹,然后在你打开的文件夹里面自动生成 ...
- Material Design with the Android Design Support Library
Material Design with the Android Design Support Library 原文http://www.sitepoint.com/material-design-a ...
- 假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以 某个固定的已知的前缀开头的,如果将它们全部找出来?
使用 keys 指令可以扫出指定模式的 key 列表. 对方接着追问:如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会 有什么问题? 这个时候你要回答 redis 关键的一个特 ...
- 什么是 MyBatis 的接口绑定?有哪些实现方式?
接口绑定,就是在 MyBatis 中任意定义接口,然后把接口里面的方法和 SQL 语句绑 定, 我们直接调用接口方法就可以,这样比起原来了 SqlSession 提供的方法我们可 以有更加灵活的选择和 ...
- zookeeper 的应用
不建议使用(单独)zookeeper 做分布式队列,有几点原因,以下原因摘抄于curator的官网: 1.zookeeper有1MB的传输限制.而在队列中,拥有很多的数据节点,通常包括数千个,如果有较 ...
- 提交Form表单,submit之前做js判断处理
效果: 在点击提交按钮时,首先进行js判断, 如果不符合条件,则alert出提示信息,并return false. 主要点就在于给form表单添加一个onsubmit事件. 在onsubmit事件中定 ...
- 几种常见布局的flex写法
flex布局目前基本上兼容主流的浏览器,且实现方式简单.我整理了flex的一些知识点,并且总结归纳了几种常见布局的flex写法 flex基础知识点 flex-grow和flex-shrink相关计算 ...
- Web最佳实践阅读总结(1)
介绍 最近开始刷一些书和题,此系列是介绍在读Web最佳实践的一些收获和体会. web前端发展现状 存在问题: 代码组织混乱 代码格式的问题突出 页面布局随意 网站整体性能差,没有意识到应用诸如缓存,动 ...
- 从零到一:用Phaser.js写意地开发小游戏(Chapter 3 - 加载游戏资源)
回顾 上一节我们搭建了游戏的骨架,添加了四个游戏场景,分别是加载.开始.游戏.结束.那么这一节我们来介绍加载这个场景,顺带丰富一下各个场景的基本内容. Phaser.Loader Phaser框架自带 ...
- 正则系列——JavaScript正则表达式入门心得
我发现有个别字符被这个编辑器给刷掉了,但是灰色区域显示正常,以灰色区域代码为准 什么玩意? 在我刚开始学习编程的时候,就听过正则了,也听说正则很牛逼,懂正则的更牛逼.但是苦于没有人指点,也没有使用正则 ...