Golang源码分析之golang/sync之singleflight

1.1. 项目介绍

golang/sync库拓展了官方自带的sync库,提供了errgroup、semaphore、singleflight及syncmap四个包,本次分析singlefliht的源代码。
singlefliht用于解决单机协程并发调用下的重复调用问题,常与缓存一起使用,避免缓存击穿。

1.2.使用方法

go get -u golang.org/x/sync

  • 核心API:Do、DoChan、Forget
  • Do:同一时刻对某个Key方法的调用, 只能由一个协程完成,其余协程阻塞直到该协程执行成功后,直接获取其生成的值,以下是一个避免缓存击穿的常见使用方法:
func main() {var flight singleflight.Groupvar errGroup errgroup.Group// 模拟并发获取数据缓存for i := 0; i < 10; i++ {i := ierrGroup.Go(func() error {fmt.Printf("协程%v准备获取缓存\n", i)v, err, shared := flight.Do("getCache", func() (interface{}, error) {// 模拟获取缓存操作fmt.Printf("协程%v正在读数据库获取缓存\n", i)time.Sleep(100 * time.Millisecond)fmt.Printf("协程%v读取数据库生成缓存成功\n", i)return "mockCache", nil})if err != nil {fmt.Printf("err = %v", err)return err}fmt.Printf("协程%v获取缓存成功, v = %v, shared = %v\n", i, v, shared)return nil})}if err := errGroup.Wait(); err != nil {fmt.Printf("errGroup wait err = %v", err)}
}
// 输出:只有0号协程实际生成了缓存,其余协程读取生成的结果
协程0准备获取缓存
协程4准备获取缓存
协程3准备获取缓存
协程2准备获取缓存
协程6准备获取缓存
协程5准备获取缓存
协程7准备获取缓存
协程1准备获取缓存
协程8准备获取缓存
协程9准备获取缓存
协程0正在读数据库获取缓存
协程0读取数据库生成缓存成功
协程0获取缓存成功, v = mockCache, shared = true
协程8获取缓存成功, v = mockCache, shared = true
协程2获取缓存成功, v = mockCache, shared = true
协程6获取缓存成功, v = mockCache, shared = true
协程5获取缓存成功, v = mockCache, shared = true
协程7获取缓存成功, v = mockCache, shared = true
协程9获取缓存成功, v = mockCache, shared = true
协程1获取缓存成功, v = mockCache, shared = true
协程4获取缓存成功, v = mockCache, shared = true
协程3获取缓存成功, v = mockCache, shared = true

DoChan:将执行结果返回到通道中,可通过监听通道结果获取方法执行值,这个方法相较于Do来说的区别是执行DoChan后不会阻塞到其中一个协程完成任务,而是异步执行任务,最后需要结果时直接从通道中获取,避免长时间等待。

func testDoChan() {var flight singleflight.Groupvar errGroup errgroup.Group// 模拟并发获取数据缓存for i :=; i < 10; i++ {i := ierrGroup.Go(func() error {fmt.Printf("协程%v准备获取缓存\n", i)ch := flight.DoChan("getCache", func() (interface{}, error) {// 模拟获取缓存操作fmt.Printf("协程%v正在读数据库获取缓存\n", i)time.Sleep( * time.Millisecond)fmt.Printf("协程%v读取数据库获取缓存成功\n", i)return "mockCache", nil})res := <-chif res.Err != nil {fmt.Printf("err = %v", res.Err)return res.Err}fmt.Printf("协程%v获取缓存成功, v = %v, shared = %v\n", i, res.Val, res.Shared)return nil})}if err := errGroup.Wait(); err != nil {fmt.Printf("errGroup wait err = %v", err)}
}
// 输出结果
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程准备获取缓存
协程正在读数据库获取缓存
协程读取数据库获取缓存成功
协程准备获取缓存
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true
协程获取缓存成功, v = mockCache, shared = true

2.源码分析

2.1.项目结构

  • singleflight.go:核心实现,提供相关API
  • singleflight_test.go:相关API单元测试

2.2.数据结构

  • singleflight.go
// singleflight.Group
type Group struct {mu sync.Mutex       // map的锁m  map[string]*call // 保存每个key的调用
}// 一次Do对应的响应结果
type Result struct {Val    interface{}Err    errorShared bool
}// 一个key会对应一个call
type call struct {wg sync.WaitGroupval interface{} // 保存调用的结果err error       // 调用出现的err// 该call被调用的次数dups  int// 每次DoChan时都会追加一个chan在该列表chans []chan<- Result
}

2.3.API代码流程

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {g.mu.Lock()if g.m == nil {// 第一次执行Do的时候创建mapg.m = make(map[string]*call)}// 已经存在该key,对应后续的并发调用if c, ok := g.m[key]; ok {// 执行次数自增c.dups++g.mu.Unlock()// 等待执行fn的协程完成c.wg.Wait()// ...// 返回执行结果return c.val, c.err, true}// 不存在该key,说明第一次调用,初始化一个callc := new(call)// wg添加,后续其他协程在该wg上阻塞c.wg.Add()// 保存key和call的关系g.m[key] = cg.mu.Unlock()// 真正执行fn函数g.doCall(c, key, fn)return c.val, c.err, c.dups >
}func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {normalReturn := falserecovered := false// 第三步、最后的设置和清理工作defer func() {// ...g.mu.Lock()defer g.mu.Unlock()// 执行完成,调用wg.Done,其他协程此时不再阻塞,读到fn执行结果c.wg.Done()// 二次校验map中key的值是否为当前call,并删除该keyif g.m[key] == c {delete(g.m, key)}// ...// 如果c.chans存在,则遍历并写入执行结果for _, ch := range c.chans {ch <- Result{c.val, c.err, c.dups >}}}}()// 第一步、执行fn获取结果func() {//、如果fn执行过程中panic,将c.err设置为PanicErrordefer func() {if !normalReturn {if r := recover(); r != nil {c.err = newPanicError(r)}}}()//、执行fn,获取到执行结果c.val, c.err = fn()//、设置正常返回结果标识normalReturn = true}()// 第二步、fn执行出错,将recovered标识设置为trueif !normalReturn {recovered = true}
}

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {// 一次调用对应一个chanch := make(chan Result,)g.mu.Lock()if g.m == nil {// 第一次调用,初始化mapg.m = make(map[string]*call)}// 后续调用,已存在keyif c, ok := g.m[key]; ok {// 调用次数自增c.dups++// 将chan添加到chans列表c.chans = append(c.chans, ch)g.mu.Unlock()// 直接返回chan,不等待fn执行完成return ch}// 第一次调用,初始化call及chans列表c := &call{chans: []chan<- Result{ch}}// wg加一c.wg.Add()// 保存key及call的关系g.m[key] = cg.mu.Unlock()// 异步执行fn函数go g.doCall(c, key, fn)// 直接返回该chanreturn ch
}

3.总结

  • singleflight经常和缓存获取配合使用,可以缓解缓存击穿问题,避免同一时刻单机大量的并发调用获取数据库构建缓存
  • singleflight的实现很精简,核心流程就是使用map保存每次调用的key与call的映射关系,每个call中通过wg控制只存在一个协程执行fn函数,其他协程等待执行完成后,直接获取执行结果,在执行完成后会删去map中的key
  • singleflight的Do方法会阻塞直到fn执行完成,DoChan方法不会阻塞,而是异步执行fn,并通过通道来实现结果的通知

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/182962.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

〔001〕虚幻 UE5 安装教程

✨ 目录 🎈 下载启动程序🎈 注册个人账户🎈 选择引擎版本🎈 选择安装选项🎈 虚幻商城的使用🎈 每月免费插件🎈 安装插件🎈 下载启动程序 下载地址:https://www.unrealengine.com/zh-CN/download点击上面地址,下载 UE5 启动程序并安装🎈 注册个人账户 打开商…

用Rust和Scraper库编写图像爬虫的建议

本文提供一些有关如何使用Rust和Scraper库编写图像爬虫的一般建议&#xff1a; 1、首先&#xff0c;你需要安装Rust和Scraper库。你可以通过Rustup或Cargo来安装Rust&#xff0c;然后使用Cargo来安装Scraper库。 2、然后&#xff0c;你可以使用Scraper库的Crawler类来创建一个…

Nginx默认会自动忽略请求头Headers里带下划线_的参数

起因&#xff1a;该接口设置了必须要传送app_code和app_secret才能正常访问。实际我在本地环境测试中&#xff0c;发现该接口是正常访问的&#xff0c;但是部署到正式系统之后发现&#xff0c;该接口一直提示app_code和app_secret不能为空。 后续排查&#xff1a;发现正式系统…

「Verilog学习笔记」位拆分与运算

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 1、寄存器的位是可以分开单独运算的&#xff0c;并不是一个输入就一定是一个数据&#xff0c;在很多情况下&#xff0c;一个输入既包括数据又包括地址等其他有效信息 2、需…

jsonlite库

jsonlite是一个R语言中用于处理JSON数据的库。它提供了一组简单而强大的函数&#xff0c;用于解析、生成和转换JSON数据。 使用jsonlite库&#xff0c;您可以轻松地将JSON数据解析为R语言中的数据结构&#xff0c;如列表或数据框。您还可以将R语言中的数据结构转换为JSON格式&…

网络协议的基本概念

网络协议的基本概念 随处可见的协议 在计算机网络与信息通信领域里&#xff0c;人们经常提及“协议”一词。互联网中常用的具有代表性的协议有IP、TCP、HTTP等。 “计算机网络体系结构”将这些网络协议进行了系统归纳。TCP/IP就是IP、TCP、HTTP等协议的集合。现在&#xff0…

卡尔曼滤波之二:Python实现

卡尔曼滤波之二&#xff1a;Python实现 1.背景描述2.构建卡尔曼滤波公式2.1 预测2.2 更新 3.代码实现3.1 输入值3.2 pykalman包实现3.3 不使用Python包实现3.4 效果可视化 参考文献 了解了卡尔曼滤波之一&#xff1a;基本概念&#xff0c;可以结合代码来理解下卡尔曼滤波的2个预…

3.27每日一题(常系数线性非齐次方程的特解)

常系数非齐次线性方程的特解如何假设&#xff08;两种&#xff09;形式&#xff1a; 1、题目中 e 的 x 次幂以及 1&#xff0c;都是第一种&#xff1a;1可以看成为e的0次幂 注&#xff1a;题目给的多项式是特殊的形式&#xff0c;我们要设为一般的形式的多项式 2、题目中sin…

Python 爬虫基础

Python 爬虫基础 1.1 理论 在浏览器通过网页拼接【/robots.txt】来了解可爬取的网页路径范围 例如访问&#xff1a; https://www.csdn.net/robots.txt User-agent: * Disallow: /scripts Disallow: /public Disallow: /css/ Disallow: /images/ Disallow: /content/ Disallo…

KaiOS APN配置文件apn.json调试验证方法(无需项目全编)

1、KaiOS 的应用就类似web应用&#xff0c;结合文件夹路径webapp字面意思理解。 2、KaiOS APN配置文件源代码在apn.json&#xff0c; &#xff08;1&#xff09;apn.json可以自定义路径&#xff0c;通过配置脚本实现拷贝APN在编译时动态选择路径在机器中生效。 &#xff08;…

4.网络之TCP

TCP协议(传输层) 文章目录 TCP协议(传输层)1. TCP报文格式2. TCP相关机制2.1 确认应答机制2.2 超时重传机制2.3 连接管理机制&#xff08;重点&#xff09;2.3.1 三次握手2.3.2 四次挥手 2.4 滑动窗口机制2.5 流量控制机制2.6 拥塞控制机制2.7 延迟应答机制2.8 捎带应答机制 3.…

登录Tomcat控制台,账号密码输入正确但点击登录没反应不跳转到控制台页面

在tomcat-users.xml里面可以查看登录tomcat控制台的账号密码&#xff0c;如果账号密码输入正确还是登录不进去&#xff0c;则很有可能是tomcat的账号被锁了&#xff08;可在catalina.xxx.log里面查看&#xff09;。tomcat账号被锁定后默认情况是不访问控制台后5分钟自动解锁&am…

访问者模式-操作复杂对象结构

商场里有许多的店铺&#xff0c;大致分为服装区、饮食区及休闲区&#xff0c;每天都会有络绎不绝的不同角色&#xff08;打工人、学生、有钱人&#xff09;的人来逛商场。商场想开发个系统用来模拟这些人的在这些店铺的行为。 public class SuperMarket {public static void m…

立创eda专业版学习笔记(8)(运行模式)

以前没注意过这个问题&#xff0c;我有2台电脑&#xff0c;都能登录eda专业版&#xff0c;但是一台是全在线模式&#xff0c;另外一台是半离线模式&#xff0c;虽然是同一个账号&#xff0c;但是打开里面的工程会发现&#xff0c;两边的工程完全不同&#xff0c;因为一台的工程…

【UE5 Cesium】actor随着视角远近来变化其本身大小

效果 步骤 1. 首先我将“DynamicPawn”设置为默认的pawn类 2. 新建一个父类为actor的蓝图&#xff0c;添加一个静态网格体组件 当事件开始运行后添加一个定时器&#xff0c;委托给一个自定义事件&#xff0c;每2s执行一次&#xff0c;该事件每2s获取一下“DynamicPawn”和acto…

kafka问题汇总

报错1&#xff1a; 解决方式 1、停止docker服务   输入如下命令停止docker服务 systemctl stop docker 或者service docker stop1   停止成功的话&#xff0c;再输入docker ps 就会提示出下边的话&#xff1a; Cannot connect to the Docker daemon. Is the docker daem…

YoloV5训练V3Det数据集实战

摘要 V3Det&#xff1a;一个庞大的词汇视觉检测数据集&#xff0c;在大量真实世界图像上具有精确注释的边界框&#xff0c;其包含13029个类别中的245k个图像&#xff08;比LVIS大10倍&#xff09;&#xff0c;数据集已经开源&#xff01; 图片的数量比COCO多一些&#xff0c;…

云计算的大模型之争,亚马逊云科技落后了?

文丨智能相对论 作者丨沈浪 “OpenAI使用了Azure的智能云服务”——在过去的半年&#xff0c;这几乎成为了微软智能云最好的广告词。 正所谓“水涨船高”&#xff0c;凭借OpenAI旗下的ChatGPT在全球范围内爆发&#xff0c;微软趁势拉了一波自家的云计算业务。2023年二季度&a…

嵌入式C语言自我修养《数据存储与指针》学习笔记

目录 一、数据类型和存储 1.大端模式和小端模式 2.有符号数和无符号数 二、数据对齐 1.为什么要数据对齐 2.结构体对齐 3.联合体对齐 三、数据的可移植性 四、 Linux内核中的size_t类型 五、typedef的使用 1. typedef的基本用法 2.使用typedef的优势 3. typedef的作用域 六…

【SQL篇】一、Flink动态表与流的关系以及DDL语法

文章目录 1、启动SQL客户端2、SQL客户端常用配置3、动态表和持续查询4、将流转为动态表5、用SQL持续查询6、动态表转为流7、时间属性8、DDL-数据库相关9、DDL-表相关 1、启动SQL客户端 启动Flink&#xff08;基于yarn-session模式为例&#xff09;&#xff1a; /opt/module/f…