前言

最近从java转到go,来公司第一个开发工作就是对一个资源请求去重复,最终发现这个singleflight这个好东西,分享一下。

singleflight使用场景

  1. 缓存击穿:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

    • 绝大多数公司都是这么用的
  2. 请求资源去重复
    • 我们的用法,需要改动一行代码。

singleflight 简介

singleflightgolang.org/x/sync/singleflight 项目下,对外提供了以下几个方法

//Do方法,传入key,以及回调函数,如果key相同,fn方法只会执行一次,同步等待
//返回值v:表示fn执行结果
//返回值err:表示fn的返回的err
//返回值shared:表示是否是真实fn返回的还是从保存的map[key]返回的,也就是共享的
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
//DoChan方法类似Do方法,只是返回的是一个chan
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
//暂时未用到:设计Forget 控制key关联的值是否失效,默认以上两个方法只要fn方法执行完成后,内部维护的fn的值也删除(即并发结束后就失效了)
func (g *Group) Forget(key string)

singleflight的使用

从singleflight的test最简单用法

func TestDo(t *testing.T) {
var g Group
// key 可以理解资源的id
v, err, _ := g.Do("key", func() (interface{}, error) {
// do what you want
return "bar", nil
})
if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
t.Errorf("Do = %v; want %v", got, want)
}
if err != nil {
t.Errorf("Do error = %v", err)
}
}

验证并发重复请求

func process(g *Group, t *testing.T, ch chan int, key string) {
for count := 0; count < 10; count++ {
v, err, shared := g.Do(key, func() (interface{}, error) {
time.Sleep(1000 * time.Millisecond)
return "bar", nil
})
t.Log("v = ", v, " err = ", err, " shared =", shared, " ch :", ch, "g ", len(g.m))
if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
t.Errorf("Do = %v; want %v", got, want)
}
if err != nil {
t.Errorf("Do error = %v", err)
}
}
ch <- 1
} func TestDo1(t *testing.T) {
var g Group
channels := make([]chan int, 10)
key := "key"
for i := 0; i < 10; i++ {
channels[i] = make(chan int)
go process(&g, t, channels[i], key)
}
for i, ch := range channels {
<-ch
fmt.Println("routine ", i, "quit!")
}
}
  • 结果

singleflight的原理

call

call 用来表示一个正在执行或已完成的函数调用。

// call is an in-flight or completed singleflight.Do call
type call struct {
wg sync.WaitGroup // These fields are written once before the WaitGroup is done
// and are only read after the WaitGroup is done.
//val和err用来记录fn发放执行的返回值
val interface{}
err error // forgotten indicates whether Forget was called with this call's key
// while the call was still in flight.
// 用来标识fn方法执行完成之后结果是否立马删除还是保留在singleflight中
forgotten bool // These fields are read and written with the singleflight
// mutex held before the WaitGroup is done, and are read but
// not written after the WaitGroup is done.
//dups 用来记录fn方法执行的次数
dups int
//用来记录DoChan中调用次数以及需要返回的数据
chans []chan<- Result
}

Group

Group 可以看做是任务的分类。

// Group represents a class of work and forms a namespace in which
// units of work can be executed with duplicate suppression.
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}

Do 函数

// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait()
return c.val, c.err, true
}
c := new(call)
// 设置forgotten = true, doCall时 不再调用delete(g.m, key)
// c.forgotten = true
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock() g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
} // doCall handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
c.val, c.err = fn()
c.wg.Done() g.mu.Lock()
if !c.forgotten {
delete(g.m, key)
}
for _, ch := range c.chans {
ch <- Result{c.val, c.err, c.dups > 0}
}
g.mu.Unlock()
}

在Do方法中是通过waitgroup来控制的,主要流程如下:

  1. 在Group中设置了一个map,如果key不存在,则实例化call(用来保存值信息),并将key=>call的对应关系存入map中通过mutex保证了并发安全
  2. 如果已经在调用中则key已经存在map,则wg.Wait
  3. 在fn执行结束之后(在doCall方法中执行)执行wg.Done
  4. 卡在第2步的方法得到执行,返回结果

其他的DoChan方法也是类似的逻辑,只是返回的是一个chan。

参考

singleflight包原理解析

使用Golang的singleflight防止缓存击穿


你的鼓励也是我创作的动力

打赏地址

[go语言]-深入理解singleflight的更多相关文章

  1. 深入理解C语言-深入理解指针

    关于指针,其是C语言的重点,C语言学的好坏,其实就是指针学的好坏.其实指针并不复杂,学习指针,要正确的理解指针. 指针是一种数据类型 指针也是一种变量,占有内存空间,用来保存内存地址 指针就是告诉编译 ...

  2. 深入理解C语言-深入理解void

    void的字面意思是"无类型",void *则为"无类型指针",void *可以指向任何类型的数据 void含义 void几乎只有注释和限制程序的作用,定义一个 ...

  3. C语言 指针理解

    1.指针 指针全称是指针变量,其实质是C语言的一种变量.这种变量比较特殊,通常他的值会被赋值为某个变量的地址值(p = &a),然后我们可以使用 *p 这样的方式去间接访问p所指向的那个变量. ...

  4. java语言浅显理解

    从厉害的c语言.到经久不衰的java语言.到不太火的安卓和IOS,到当下流行的python,这些都是软件开发中的一员. 之前在传智播客上的免费视频资源上,听了老师对java语言的介绍,感觉挺好了.今天 ...

  5. js的语言的理解

    1.所谓字面量,就是语言语法 2.在js编译器读到语法时候,执行时候创建对象:在赋值的时候创建一个对象,或者是一个匿名对象. 3.函数定义本身是一个对象:执行时候不产生实例对象:这跟python类不一 ...

  6. 信号量和互斥量C语言示例理解线程同步

    Table of Contents 1. 线程同步 1.1. 用信号量进行同步 1.2. 用互斥量进行同步 2. 参考资料 线程同步 了解线程信号量的基础知识,对深入理解python的线程会大有帮助. ...

  7. iOS_02_第一个C语言程序(理解编译、连接、运行)

    一.开发工具的选择 1. 可以用来写代码的工具:记事本.ULtraEdit.Vim.Xcode等. 2. 选择XCode的原因:苹果公司官方提供的开发利器.简化开发的工程.有高亮显示功能. 3. 使用 ...

  8. 深入理解C语言-深入理解数组

    数组,作为C语言中常见的复杂数据类型,了解其本质有助于深入了解C语言 数组概念 元素类型角度:数组是相同类型的变量的有序集合测试指针变量占有内存空间大小 内存角度:联系的一大片内存空间 数组初始化 数 ...

  9. 深入理解C语言-深入理解内存四区

    数组与指针 当数组做函数参数的时候,会退化为一个指针 此时在函数内是得不到数组大小的 因此,数组做函数参数的时候需要传递数组大小,也就是多传递一个参数 void func(int arr[], int ...

随机推荐

  1. openvswitch 流表测试 ovs-appctl

    [root@ostack170 ~]# ovs-appctl ofproto/trace br-mirror in_port=,dl_vlan=,dl_src=:ea:cb:5d:e4:ee,dl_d ...

  2. Jupyter Notebook 入门指南

    https://www.jianshu.com/p/061c6e5c4b0d cmd输入 :jupyter notebook

  3. JS 对象基本用法

    声明对象的两种语法 let obj = { 'name': 'frank', 'age': 18 } let obj = new Object({'name': 'frank'}) 如何删除对象的属性 ...

  4. Linux基本命令及编程环境实验

    目录 一.Linux基本命令详细汇总 1.目录及文件相关命令 2.系统信息查询 3.文件操作(统计.过滤.搜索.权限) 4.其他命令 二.Linux终端上vi命令编程 1.进入vi命令模式 2.vi编 ...

  5. Gradle Wrapper

    Gradle Wrapper 当把本地一个项目放入到远程版本库的时候,如果这个项目是以gradle构建的,那么其他人从远程仓库拉取代码之后如果本地没有安装过gradle会无法编译运行,如果对gradl ...

  6. 力扣Leetcode 198. 打家劫舍

    打家劫舍 你是一个专业的小偷,计划偷窃沿街的房屋.每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警. 给定 ...

  7. Nginx Ingress 高并发实践

    概述 Nginx Ingress Controller 基于 Nginx 实现了 Kubernetes Ingress API,Nginx 是公认的高性能网关,但如果不对其进行一些参数调优,就不能充分 ...

  8. 牛客多校训练AFJ(签到)

    题目链接https://ac.nowcoder.com/acm/contest/881/A(单调栈) #include<cstdio> #include<iostream> # ...

  9. 消息队列之-RocketMQ入门

    简介 RocketMQ是阿里开源的消息中间件,目前已经捐献个Apache基金会,它是由Java语言开发的,具备高吞吐量.高可用性.适合大规模分布式系统应用等特点,经历过双11的洗礼,实力不容小觑. 官 ...

  10. 大厂运维必备技能:PB级数据仓库性能调优

    摘要:众所周知,数据量大了之后,性能是大家关注的一点,所以我们在业务开发的时候,特别关注性能,做为一个架构师,必须对性能要了解,要懂.才能设计出高性能的业务系统. 一.GaussDB分布式架构 所谓集 ...