前言

最近从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. 经典的 Fork 炸弹解析

    原文出处: saymagic Jaromil 在 2002 年设计了最为精简的一个Linux Fork炸弹,整个代码只有13个字符,在 shell 中运行后几秒后系统就会宕机: ::(){:|:&am ...

  2. python爬虫以及后端开发--实用加密模板整理

    都是作者累积的,且看其珍惜,大家可以尽量可以保存一下,如果转载请写好出处https://www.cnblogs.com/pythonywy 一.md5加密 1.简介 这是一种使用非常广泛的加密方式,不 ...

  3. SQL获取多个字段中最大小值

    1.语法最大值: GREATEST(expr_1, expr_2, ...expr_n)最小值: LEAST(expr_1, expr_2, ...expr_n) 2.说明GREATEST(expr_ ...

  4. JavaScript学习系列博客_18_JavaScript中的匿名函数

    匿名函数 - 用函数声明的方式创建一个函数时,不加函数名称. function sum(){ console.log("我是函数sum")} - 不加名称,这样写浏览器是会报错的. ...

  5. Eclipse导入项目后JSP页面出现报红

    Multiple annotations found at this line:- javax.servlet.jsp.JspException cannot be resolved to a typ ...

  6. 怎么创建一个良好的Git提交信息

    译   原文:https://dev.to/chrissiemhrk/git-commit-message-5e21 提交信息是对提交之前添加和更改的文件所做的更改的简短描述. 良好的提交信息不仅对你 ...

  7. 系综的实现方式(nve、nvt、npt)

    一.NVE系综 更新位置和速度,v和e恒定. 二.NVT系综 几种实现的方式: 如: fix 1 all nve #更新位置和速度,e和V保持不变.若只有这个命令,就只nve系综,如果和控温命令一起, ...

  8. Node.js小项目——学生信息管理系统

    这是迄今为止第一次接触后端的东西,是一个很小的项目,但是对于前端学习入门很好.我是先学了VUE框架再学的Node,学起来比较轻松,不过每个人都有自己的学习方法️ 一.项目描述 学生信息管理系统,可以实 ...

  9. 初识ABP vNext(6):vue+ABP实现国际化

    Tips:本篇已加入系列文章阅读目录,可点击查看更多相关文章. 目录 前言 开始 语言选项 语言切换 注意 最后 前言 上一篇介绍了ABP扩展实体,并且在前端部分新增了身份认证管理和租户管理的菜单,在 ...

  10. centos6.8上安装部署 jhipster-registry

    必备环境:jdk8,git,maven 1.安装nodejs #由于采用编译的方式很容易出现一些意外的惊喜,所以我们这儿直接用yum命令安装 #1.查看nodejs版本(命令中不要加 -y 如果版本不 ...