共计 4178 个字符,预计需要花费 11 分钟才能阅读完成。
在 Golang 中有一个并发原语是 Singleflight,如同晓得的开发者并不多。其中驰名的 https://github.com/golang/groupcache 就用到了这个并发原语。
Golang 版本
go1.15.5
相干知识点
map、Mutex、channel、
应用场景
个别用在对指定资源频繁操作的状况下,如高并发下的“缓存击穿”问题。
缓存击穿:一个存在的 key,在缓存过期的霎时,同时有大量的申请过去,造成所有申请都去 DB 读取数据,这些申请都会击穿缓存到 DB,造成刹时 DB 申请量大、压力霎时骤增,导致数据库负载过高,影响整个零碎失常运行。(缓存击穿不同于 缓存雪崩 和 缓存穿透)
缓存击穿
怎么了解这个原语呢,简略的讲就是将对同一个资源的多个申请合并为一个申请。
举例说明,如果当有 10 万个申请来获取同一个 key 的值的时候,失常状况下会执行 10 万次 get 操作。而应用 singleflight 并发语后,只须要首次的地个申请执行一次 get 操作就能够了,其它申请再过去时,只须要只须要期待即可。待执行后果返回后,再把后果别离返回给期待中的申请,每个申请再返回给客户端,由此看看,在肯定的高并发场景下能够大大减少零碎的负载,节俭大量的资源。
留神这个与 sync.Once 是不一样的,sync.Once 是全局只能有一个,但本并发原语则是依据 key 来划分的,并且能够依据需要来决定什么状况下共用一个。
实现原理
次要应用 Mutext 和 Map 来实现,以 key 为键,值为 call。每个call 中存储有一个申请 chans 字段,用来存储所有申请此 key 的客户端,等有返回后果的时候,再从 chans 字段中读取进去,别离写入即可。
源文件为 /src/internal/singleflight/singleflight.go
Singleflight 数据结构如下
- Do()
这个办法是一个执行函数并返回执行后果
参数key
要申请的 key,多个申请可能申请的是同一个 key,同时也只有一个函数在执行fn
对应执行函数,此函数有三个返回值v
,err
,shared
。其中shared
示意以后返回后果是否为多个申请后果 - DoChan()
类型与Do()
办法,但返回的是个 ch 类型,等函数 fn 执行后,能够通过读取返回的 ch 来获取函数后果 - Forget()
在官网库internal/singleflight/singleflight.go
中这个名字是ForgetUnshared,通知 Group 遗记申请的这个 key,下次再申请时,间接当作新的 key 来解决就能够了。其实就是将这个 key 从 map 中删除,后续再有这个 key 的操作的话,即视为新一轮的解决逻辑。
其中 Do()
和 DoChan()
的性能一样,只是获取数据的形式有所区别,开发者能够依据本人的状况来抉择应用哪一个。另外还蕴含一个由 Do() 办法调用的公有办法 doCall(),间接执行 key 解决办法的函数
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {...}
实现数据结构
实现外部次要的数据结构一共三个,别离是 call、Group 和 Result。
// 定义 map[key] 的值 call 类型
// 在 call 外部存储对应 key 相干的所有申请客户端信息
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.
// 申请返回后果,会在 sync.WaitGroup 为 Done 的时候执行
val interface{}
err error
// 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.
// 能够了解为申请 key 的个数,每减少一个申请,则值加 1
dups int
// 存储所有 key 对应的 Result{}, 一个申请对应一个 Result
chans []chan<- Result}
// Group represents a class of work and forms a namespace in
// which units of work can be executed with duplicate suppression.
// SingleFlight 的主构造体
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
// Result holds the results of Do, so they can be passed
// on a channel.
// 定义申请后果数据结构
type Result struct {Val interface{}
Err error
Shared bool
}
call 数据结构是用来记录 key 相干数据,即申请以后 key 的个数和申请后果
Group 是并发原语 SingleFlight 的主数据结构, m 字段用来存储 key 与申请的关系,而 mu 则是 Mutex 锁
Result 申请后果,其中 Val
是实现申请后返回的后果,Err
示意是否出错,而 Shared
字段则示意是否为多个申请后果。如果以后 key 只有一个申请的话,则返回 false, 否则返回 true
用法
package main
import (
"fmt"
"golang.org/x/sync/singleflight"
"log"
"sync"
"time"
)
func getDataFromDB(key string) (string, error) {defer func() {log.Println("db query end")
}()
log.Println("db query begin")
time.Sleep(2 * time.Second)
result := key + "abcxyz"
return result, nil
}
func main() {
var singleRequest singleflight.Group
getData := func(requestID int, key string) (string, error) {log.Printf("request %v start request ...", requestID)
// 合并申请
value, _, _ := singleRequest.Do(key, func() (ret interface{}, err error) {log.Printf("request %v is begin...", requestID)
ret, err = getDataFromDB(key)
log.Printf("request %v end!", requestID)
return
})
return value.(string), nil
}
var wg sync.WaitGroup
key := "orderID"
for i := 1; i < 10; i++ {wg.Add(1)
go func(wg *sync.WaitGroup, requestID int) {defer wg.Done()
value, _ := getData(requestID, key)
log.Printf("request %v get value: %v", requestID, value)
}(&wg, i)
}
wg.Wait()
fmt.Println("main end")
}
输入后果
2020/11/20 12:11:44 request 6 start request …
2020/11/20 12:11:44 request 6 is begin…
2020/11/20 12:11:44 request 2 start request …
2020/11/20 12:11:44 request 9 start request …
2020/11/20 12:11:44 request 7 start request …
2020/11/20 12:11:44 request 3 start request …
2020/11/20 12:11:44 request 4 start request …
2020/11/20 12:11:44 request 1 start request …
2020/11/20 12:11:44 request 5 start request …
2020/11/20 12:11:44 request 8 start request …
2020/11/20 12:11:44 db query begin
2020/11/20 12:11:46 db query end
2020/11/20 12:11:46 request 6 end!
2020/11/20 12:11:46 request 6 get value: orderIDabcxyz
2020/11/20 12:11:46 request 8 get value: orderIDabcxyz
2020/11/20 12:11:46 request 2 get value: orderIDabcxyz
2020/11/20 12:11:46 request 9 get value: orderIDabcxyz
2020/11/20 12:11:46 request 7 get value: orderIDabcxyz
2020/11/20 12:11:46 request 3 get value: orderIDabcxyz
2020/11/20 12:11:46 request 4 get value: orderIDabcxyz
2020/11/20 12:11:46 request 1 get value: orderIDabcxyz
2020/11/20 12:11:46 request 5 get value: orderIDabcxyz
main end
从下面的输入内容能够看到,咱们同时发动了 10 个 gorourtine 来查问同一个订单信息,真正在 DB 层查问操作只有一次,大大减少了 DB 的压力。
总结
应用 SingleFlight 时,在高并发下且对大量 key 频繁读取的话,能够大大减少服务器负载。在肯定场景下特地的有用,如 CDN。
举荐 Golang 并发原语之 - 信号量 Semaphore