Go singlefight 源码详解|图解

写在前面

通俗的来说就是 singleflight 将相同的并发请求合并成一个请求,进而减少对下层服务的压力,通常用于解决缓存击穿的问题。

在这里插入图片描述

详解

基础结构

golang.org/x/sync/singleflight singleflight结构体:

type call struct {wg sync.WaitGroup// 这些字段在 WaitGroup 结束前写入一次// 只有在 WaitGroup 结束后才会被读取。val interface{}err error// 这些字段在 WaitGroup 结束前使用 singleflight 互斥锁进行读写// 在 WaitGroup 结束后读取但不写入。dups  intchans []chan<- Result
}

Group 代表分成多个工作组,形成一个命名空间,在这个命名空间中,各工作单元可以重复执行。

type Group struct {mu sync.Mutex       // 互斥锁m  map[string]*call // 懒加载
}

Result 保存 Do 方法的结果,以便在通道上传递。做异步处理。

type Result struct {Val    interface{}Err    errorShared bool
}

简单demo

func TestSingleFightExample(t *testing.T) {var group singleflight.Group// 模拟一个并发请求for i := 0; i < 5; i++ {go func(i int) {key := "example"tmp := i // 将tmp放进去val, err, _ := group.Do(key, func() (interface{}, error) {// 模拟一次耗时操作time.Sleep(time.Second)return fmt.Sprintf("result_%d", tmp), nil})if err != nil {fmt.Println("Error:", err)}fmt.Println("Value:", val)}(i)}// 等待所有请求完成time.Sleep(3 * time.Second)
}

结果:这是一个很随机的过程,0~4都有可能,主要看哪个协程最先进来。

Value: result_2
Value: result_2
Value: result_2
Value: result_2
Value: result_2

Do 执行函数:对同一个 key 多次调用的时候,在第一次调用没有执行完的时候, 只会执行一次 fn,其他的调用会阻塞住等待这次调用返回, shared 表示fn的结果是否被共享

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

DoChan 和 Do 类似,只是 DoChan 返回一个 channel,也就是同步与异步的区别

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

Forget:用于通知 Group 删除某个 key 这样后面继续这个 key 的调用的时候就不会在阻塞等待了

func (g *Group) Forget(key string){g.mu.Lock()if c, ok := g.m[key]; ok {c.forgotten = true}delete(g.m, key)g.mu.Unlock()
}

singleflight的本质是对某次函数调用的复用,只执行1次,并将执行期间相同的函数返回相同的结果。由此产生一个问题,如果实际执行的函数出了问题,比如超时,则在此期间的所有调用都会超时,由此需要一些额外的方法来控制。

在一些对可用性要求极高的场景下,往往需要一定的请求饱和度来保证业务的最终成功率。一次请求还是多次请求,对于下游服务而言并没有太大区别,此时使用 singleflight 只是为了降低请求的数量级,那么使用 Forget() 提高下游请求的并发。

常见面试题

singleflight 是什么?什么时候用的?

缓存失效,合并请求的时候用的,这样我们就可以减少对DB的请求压力。
在这里插入图片描述

如果这个goruntine超时怎么办?

singleflight 内部使用 waitGroup 来让同一个 key 的除了第一个请求的后续所有请求都阻塞。直到第一个请求执行 func 返回后,其他请求才会返回。
这意味着,如果 func 执行需要很长时间,那么后面的所有请求都会被一直阻塞。
这时候我们可以使用 DoChan 结合ctx + select做超时控制

func TestSingleFightTimeout(t *testing.T) {ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)go doFly(ctx)time.Sleep(2 * time.Second)cancel() // 2秒后超时
}func doFly(ctx context.Context) {var g singleflight.Groupkey := "example"// 使用 DoChan 结合 select 做超时控制result := g.DoChan(key, func() (interface{}, error) {time.Sleep(5 * time.Second) // 模拟超时return "result", nil})select {case r := <-result:fmt.Println("r", r.Val)case <-ctx.Done():fmt.Println("done")return}
}

结果输出:

done

上述代码中,我们将主进程先sleep 2秒,然后再进行cancel,那么此时我们将会让DoChan这个方法 time.Sleep 5秒模拟超时。那么我们会发现函数过了2秒之后就会输出done

doChan方法具体是怎么实现的?

在DoChan方法中,有一个 go g.doCall(c, key, fn) 的操作,当一个 goroutine 来执行,并通过channel 来返回数据,这样外部可以自定义超时逻辑,防止因为 fn 的阻塞,导致大量请求都被阻塞。

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {ch := make(chan Result, 1)g.mu.Lock()if g.m == nil {g.m = make(map[string]*call)}if c, ok := g.m[key]; ok { // 如果没有这个keyc.dups++c.chans = append(c.chans, ch)g.mu.Unlock()return ch}c := &call{chans: []chan<- Result{ch}} // 构造异步返回结构体,可以接参数进行超时c.wg.Add(1)g.m[key] = cg.mu.Unlock()go g.doCall(c, key, fn) // 异步执行return ch
}

如果请求失败了怎么办?

如果第一个请求失败了,那么后续所有等待的请求都会返回同一个 error。但实际上可以根据下游能支撑的 rps 定时 forget 这个 key,让更多的请求能有机会走到后续逻辑。

go func() {time.Sleep(100 * time.Millisecond)g.Forget(key)}()

比如1秒内有100个请求过来,正常是第一个请求能执行queryDB,后续99个都会阻塞。增加这个 Forget 之后,每 100ms 就能有一个请求执行 queryDB,相当于是多了几次尝试的机会,相对的也给DB造成了更大的压力,需要根据具体场景进去取舍。 因为有可能前几次是因为DB的抖动导致的查询失败,重试之后就能实现了。

参考链接

[1] https://pkg.go.dev/golang.org/x/sync/singleflight
[2] https://www.lixueduan.com/posts/go/singleflight
[3] https://juejin.cn/post/7093859835694809125

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

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

相关文章

Linux系统下非高精度对时实操

测试对时偏差 使用 ntpdate -d 选项, 挑选一个合适的ntp对时地址比如 ntp.tencent.com 或者 time.pool.aliyun.com 使用 /usr/sbin/ntpdate -d ntp.tencent.com, 可以看到如下结果, 其中offset 就代表NTP服务器时间- 系统时间, 所以offset 0.012036 sec 就代表系统时间比NTP服…

[Java] TDengine时序数据库时间戳(timestamp)字段插入数据的实现方法

&#x1f449;原文阅读 目录 &#x1f449;[原文阅读](https://b1ankc-mov.github.io/posts/tdengine_timestamp/) &#x1f4d8;正文开始实体类Mapper接口Controller控制器 &#x1f4d8;正文开始 实体类 定义实体类&#xff0c;插入数据分别代表打卡时间、员工id&#xff0…

如何在WPS中加载EndNote X9插件

如何在WPS中加载EndNote X9插件 步骤1&#xff1a;关闭WPS 确保所有WPS文档和窗口都已关闭。 步骤2&#xff1a;修改文件后缀 打开文件资源管理器&#xff0c;导航到路径&#xff1a;C:\Program Files (x86)\EndNote X9\Product-Support\CWYW。找到文件 Cwyw_X86.dat&#…

正排索引和倒排索引的区别

正排索引和倒排索引是数据库中常见的两种索引方式&#xff0c;它们有以下区别&#xff1a; 1. 数据结构不同&#xff1a;正排索引是按照文档的顺序存储索引&#xff0c;而倒排索引是根据关键词来存储索引。 2. 查询方式不同&#xff1a;正排索引适合按文档顺序进行查询&a…

glibc函数malloc的工作原理

glibc提供了malloc函数来动态分配内存&#xff0c;我们只知道调用malloc会返回给我们一个指针&#xff0c;指向一块内存空间或NULL&#xff0c;那么malloc的工作原理是什么呢&#xff1f; 概述&#xff1a; 1.小于128kB的空间&#xff0c;使用内存池&#xff08;在堆上&#…

高效能光伏监控解决方案 —— ARMxy工业计算机深度解析

在浩瀚的戈壁滩上&#xff0c;一座现代化的光伏电站正沐浴在烈日之下&#xff0c;将无尽的阳光转化为清洁的电能。这背后&#xff0c;离不开一项关键技术的支撑——ARMxy工业计算机&#xff0c;它如同一位智慧的指挥官&#xff0c;精确掌控着这座绿色能源基地的心跳。 面对广袤…

鱼香肉丝ROS一键安装

鱼香肉丝ROS一键安装实际上是指通过特定的脚本和工具&#xff0c;快速地在Ubuntu系统上安装ROS&#xff08;Robot Operating System&#xff0c;机器人操作系统&#xff09;。由于“鱼香肉丝”并非与ROS安装直接相关的术语&#xff0c;我猜测这里可能是对某个特定ROS安装脚本或…

王学岗鸿蒙开发(北向)——————(十三)音乐播放器

AudioRenderer适合录音 AVPlayer:简单的本地单曲播放 MP3文件放置的地方 import media from ohos.multimedia.media import common from ohos.app.ability.common; Entry Component struct Index {//第1步&#xff1a;avPlayer:media.AVPlayer nullasync onPageShow(){//第…

常见的100道java面试题及答案【java学习+面试指南】(五)

消息队列有哪些应用场景&#xff1f; 异步处理、流量控制、服务解耦、消息广播 ZGC收集器中的染色指针有什么用&#xff1f; 染色指针是一种直接将少量额外的信息存储在指针上的技术&#xff0c;可是为什么指针本身也可以存储额外信息呢&#xff1f;在64位系统中&#xff0c…

kafka集成spark

1.新建Scala项目 具体教程可见在idea中创建Scala项目教程-CSDN博客 1.1右键项目名-添加框架支持-勾选scala 1.2main目录下新建scala目录-右键Scala目录-将目录标记为-勾选源代码根目录 1.3创建包com.ljr.spark 1.4引入依赖&#xff08;pox.xml) <dependencies><…

[FreeRTOS 基础知识] 保存现场与恢复现场

文章目录 什么是现场&#xff1f;保存现场的数据存放在哪里&#xff1f;保护现场的场景 什么是现场&#xff1f; 在[FreeRTOS 基础知识] 栈 与 汇编语言文章中解析了fun_c汇编函数&#xff0c;假设在执行fun_c函数的过程中产生高优先级的中断。如下图所示。 此时刚从RAM的SP栈…

秋招突击——6/10——复习{(树形DP)树的最长路径、}——新作{电话号码的字母组合}

文章目录 引言复习树形DP——树的最长路径思路分析参考思路求图的最长的直径的通用方法证明 树形DP分析方法问题 参考代码使用一维数组模拟邻接表存储树形结构或者稀疏图 新作电话号码的组合思路分析参考实现 总结 引言 中间面试了两天&#xff0c;去上海呆了一天&#xff0c;…

Linux 安装ab测试工具

yum -y install httpd-tools ab -help #10个并发连接&#xff0c;100个请求 ab -n 200 -c 100 http://www.baidu.com/

基于51单片机的车辆动态称重系统设计

一 动态称重 所谓动态称重是指通过分析和测量车胎运动中的力,来计算该运动车辆的总重量、轴重、轮重和部分重量数据的过程。动态称重系统按经过车辆行驶的速度划分,可分为低速动态称重系统与高速动态称重系统。因为我国高速公路的限速最高是120,所以高速动态称重系统在理论…

奇数求和【菜蛋题解】

计算非负整数m到n(包括m和n&#xff09;之间的所有奇数的和&#xff0c;其中&#xff0c;m不大于n,且n不大于300.例如m3&#xff0c;n12,其和则为&#xff1a;35791135 输入&#xff1a;两个数m和n(0<m<n<300),两个数以一个空格分开 输出&#xff1a;一行&#xff0…

【Rd-03E】使用CH340给Rd03_E雷达模块烧录固件

Rd03_E 指导手册 安信可新品雷达模组Rd-03搭配STM32制作简易人体感应雷达灯教程 http://t.csdnimg.cn/mqhkE 测距指导手册网址&#xff1a; https://docs.ai-thinker.com/_media/rd-03e%E7%B2%BE%E5%87%86%E6%B5%8B%E8%B7%9D%E7%94%A8%E6%88%B7%E6%89%8B%E5%86%8C%E4%B8%AD%…

万能表单与AI的完美融合,打造个性化AI小程序

在人工智能技术日益成熟的今天&#xff0c;如何将AI智能与用户界面无缝结合&#xff0c;已成为软件开发领域的新挑战。MyCms 以其创新的“万能表单结合AI”功能&#xff0c;为开发者提供了一个全新的解决方案&#xff0c;让个性化AI小程序的开发变得前所未有的简单和高效。 一、…

【解读】小提琴图

ref&#xff1a;解读文献中的箱线图&#xff08;Box-plot&#xff09;和小提琴图&#xff08;Violin-plot)&#xff09;_小提琴图和箱线图的区别-CSDN博客小提琴图展示了每个变量的数据分布情况&#xff0c;通过图中的“小提琴”形状可以看出数据的密度和分布情况。 在图中&…

2024-6-10-Model-Agnostic Meta-Learning (MAML)

摘自&#xff1a;Meta-Transfer Learning for Zero-Shot Super-Resolution 近年来&#xff0c;提出了各种元学习算法。它们可以分为三类&#xff1a; 基于度量的方法&#xff1a;这些方法通过学习度量空间&#xff0c;使得在少量样本内进行高效的学习。例如[35, 38, 39]。基于…

JS实现移动端的轮播图滑动事件

在移动端实现轮播图滑动事件&#xff0c;我们通常使用 touchstart、touchmove 和 touchend 这三个事件。下面是一个基本的示例&#xff0c;展示了如何使用原生JavaScript来创建一个简单的移动端轮播图滑动效果&#xff1a; HTML结构&#xff1a; <div id"carousel&qu…