最近在工作中对 Go 的 singleflight 包做了下增强,解决了一个性能问题,这里记录下,希望对你也有所帮助。
singleflight 是什么
举个例子,当程序中有读(如 Redis、MySQL、Http、RPC等)请求,且并发非常高的情况,使用 singleflight 能得到比较好的效果,它限制了同一时刻只有一个请求在执行,也就是并发永远为1。
singleflight 的原理
https://github.com/golang/groupcache/blob/master/singleflight/singleflight.go
type call struct {
wg sync.WaitGroup
val interface{}
err error
}
再定义全局的 Group,包含一个互斥锁 Mutex,一个 key 为 string,value 为 call 的 map。
type Group struct {
mu sync.Mutex
m map[string]*call
}
Group 对象有一个 Do 方法,其第一个参数是 string 类型的 key,这个 key 也就是上面说的 map 的 key,相同的 key 标志着他们是相同的请求,只有相同的请求会被抑制;第二个参数是一个函数 fn,这个函数是真正要执行的函数,例如调用 MySQL;返回值比较好理解,即最终调用的返回值和错误信息。
func (g *Group Do(key string, fn func( (interface{}, error (interface{}, error {
// ①
g.mu.Lock(
if g.m == nil {
g.m = make(map[string]*call
}
// ②
if c, ok := g.m[key]; ok {
g.mu.Unlock(
c.wg.Wait(
return c.val, c.err
}
// ③
c := new(call
c.wg.Add(1
g.m[key] = c
g.mu.Unlock(
c.val, c.err = fn(
c.wg.Done(
g.mu.Lock(
delete(g.m, key
g.mu.Unlock(
return c.val, c.err
}
将整个代码分成三块:
- ① 懒加载方式初始化 map;
- ② 如果当前 key 存在,即相同请求正在调用中,就等它完成,完成后直接使用它的 value 和 error;
- ③ 如果当前 key 不存在,即没有相同请求正在调用中,就创建一个 call 对象,并把它放进 map,接着执行 fn 函数,当函数执行完唤醒 waitGroup,并删除 map 相应的 key,返回 value 和 error。
读可以抑制,写呢
微服务中的注册中心想必大家都有所了解,如果不了解,可以去查查相关概念,或者翻看我以前的文章,老读者应该能发现我写了很多相关的文章。
- 接口级注册(类似 Dubbo),每台机器会注册N多次
- 服务并发发布,例如每次发布重启100台机器,那么注册的并发就可能是100
拿到这种问题,第一想到的解法是:合并推送。但,怎么合并呢?
直接使用 singleflight,能行吗?
套用上面 singleflight,在第一个事件推送过程中,其他相同的事件被 Hold 住,等第一个事件推送完成后,这些 Hold 的事件不再执行推送直接返回。
增强一点点 🤏🏻
增强代码参考
package singleflight
import (
"sync"
type WriteGroup struct {
mu sync.Mutex
wgs map[string]*sync.WaitGroup
group Group
}
func (g *WriteGroup Do(key string, fn func( error error {
g.mu.Lock(
if g.wgs == nil {
g.wgs = make(map[string]*sync.WaitGroup
}
wg, ok := g.wgs[key]
if !ok {
wg = &sync.WaitGroup{}
wg.Add(1
g.wgs[key] = wg
}
g.mu.Unlock(
if !ok {
err := fn(
g.mu.Lock(
wg.Done(
delete(g.wgs, key
g.mu.Unlock(
return err
}
wg.Wait(
_, err := g.group.Do(key, func( (interface{}, error {
return nil, fn(
}
return err
}
效果如何?
理论上,如果没有并发,事件和以前一样推送,没有合并,当然这也没毛病。当并发大于 2 时,开始发挥威力。在实际的压测上,注册并发 1500 时,合并的事件达到 99.9%,效果相当炸裂!
最后感谢能抽空看到这里,如果你能点赞
、在看
、分享
,我会更加感激不尽~
搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践
- 进技术交流群加微信 MrRoshi