hello,大家好啊,我是小楼。

最近在工作中对 Go 的 singleflight 包做了下增强,解决了一个性能问题,这里记录下,希望对你也有所帮助。

singleflight 是什么

singleflight 直接翻译为”单(次)飞(行)“,它是对同一种请求的抑制,保证同一时刻相同的请求只有一个在执行,且在它执行期间的相同请求都会 Hold 直到执行完成,这些 hold 的请求也使用这次执行的结果。

举个例子,当程序中有读(如 Redis、MySQL、Http、RPC等)请求,且并发非常高的情况,使用 singleflight 能得到比较好的效果,它限制了同一时刻只有一个请求在执行,也就是并发永远为1。

singleflight 的原理

最初 singleflight 出现在 groupcache 项目中,这个项目也是 Go 团队所写,后来该包被移到 Go 源码中,在 Go 源码中的版本经过几轮迭代,稍微有点复杂,我们以最原始的源码来讲解原理,更方便地看清本质。

https://github.com/golang/groupcache/blob/master/singleflight/singleflight.go

singleflight 把每次请求定义为 call,每个 call 对象包含了一个 waitGroup,一个 val,即请求的返回值,一个 err,即请求返回的错误。

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。

读可以抑制,写呢?

我们通过上面的介绍能了解,singleflight 能解决并发读的问题,但我又遇到一个并发写的问题。为了能让大家快速进入状态,先花一点篇幅描述一下遇到的实际问题:

微服务中的注册中心想必大家都有所了解,如果不了解,可以去查查相关概念,或者翻看我以前的文章,老读者应该能发现我写了很多相关的文章。

服务提供方在注册之后,会将变更事件推送到消费方,推送事件的处理流程是:接收到事件,查询组装出最新的数据,然后推送给订阅者。存在两种情况可能会导致短时间内注册请求非常多,推送事件多会影响整个注册中心的性能:

  • 接口级注册(类似 Dubbo),每台机器会注册N多次
  • 服务并发发布,例如每次发布重启100台机器,那么注册的并发就可能是100

拿到这种问题,第一想到的解法是:合并推送。但,怎么合并呢?

是不是每次推送的时候等一等,等事件都来了再一把推过去就可以了?但等多久呢?什么时候该等呢?粗暴点,每秒钟推送一次,这样就能将一秒内的时间都聚合,但这会影响推送的时效性,显然不符合我们精益求精的要求。

直接使用 singleflight,能行吗?

套用上面 singleflight ,在第一个事件推送过程中,其他相同的事件被 Hold 住,等第一个事件推送完成后,这些 Hold 的事件不再执行推送直接返回。

稍微想一下就知道这样是有问题的,假设有三个事件 A、B、C,分别对应到三个版本的数据A1、B1、C1,A 最先到达,在 A 开始推送后但没完成时 B、C 事件到达,A 事件触发推送了 A1 版本的数据,B、C 事件在 A 事件推送完成后,直接丢弃,最终推送到消费者上的数据版本为 A1,但我们肯定期望推送的数据版本为 C1,画个图线感受下:

增强一点点

假设有事件 A、B、C、D 先后到达,A 事件仍然先正常执行推送,在 A 事件推送的时候,B、C、D 事件 Hlod 住,当 A 事件推送完成后,B 事件开始推送,B 事件将把 A 事件推送时期积攒的事件都一起推送掉,即 B、C、D 一次性推送完成。

增强代码参考

增强的定义为 WriteGroup,借用 singleflight 原先的实现,具体代码就不必解读了,对照上面的例子应该很好理解。

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

使用增强版 singleflight 合并事件推送,效果炸裂!的更多相关文章

  1. 用c#开发微信 (4) 基于Senparc.Weixin框架的接收事件推送处理 (源码下载)

    本文讲述使用Senparc.Weixin框架来快速处理各种接收事件推送.这里的消息指的是传统的微信公众平台消息交互,微信用户向公众号发送消息后,公众号回复消息给微信用户.包括以下类型: 1 subsc ...

  2. C#微信公众号开发系列教程五(接收事件推送与消息排重)

    微信公众号开发系列教程一(调试环境部署) 微信公众号开发系列教程一(调试环境部署续:vs远程调试) C#微信公众号开发系列教程二(新手接入指南) C#微信公众号开发系列教程三(消息体签名及加解密) C ...

  3. C#微信公众号开发系列教程(接收事件推送与消息排重)

    微信服务器在5秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次.这样的话,问题就来了.有这样一个场景:当用户关注微信账号时,获取当前用户信息,然后将信息写到数据库中.类似于pc端网站的注册.可 ...

  4. 微信公众号开发C#系列-7、消息管理-接收事件推送

    1.概述 在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息.其中,某些事件推送在发生后,是允许 ...

  5. 微信公众平台设置URL和Token接收接口事件推送

    最近做对接微信闪开发票-微信发票名片,里面有个接收用户提交抬头接口是微信推送事件到公众号后台,该事件将发送至开发者填写的URL(登录公众平台进入[开发者中心设置]). 开发者可通过事件推送完成数据统计 ...

  6. 转:C#微信公众号开发之接收事件推送与消息排重的方法

    本文实例讲述了C#微信公众号开发之接收事件推送与消息排重的方法.分享给大家供大家参考.具体分析如下: 微信服务器在5秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次.这样的话,问题就来了.有这 ...

  7. jQuery 1.4版本的15个新功能(现在已经发布到jquery1.8,特别是增强版的live事件,支持 submit , change , focus 和 blur 事件)

    1.jQuery()创建DOM元素:支持传参设置属性 之前,jQuery可以通过 attr 方法设置元素的属性,既可传属性的名和值,也可以是包含几组特定 属性名值对 的 对象.在 jQuery 1.4 ...

  8. devexpress显示缓冲滚动条与实现类似QQ消息推送效果

    1.一般在项目中处理大数据,或者查询大量数据时,耗时会很长,这个时候缓冲条是必不可少的.这里展示一个devexpress不错的缓冲条,如图所示: 使用到了控件splashScreenManager,运 ...

  9. html5:服务器事件推送(server-sent events)Asp.net

    支持 不支持IE 个人理解说明 个人理解:这种消息推送方式不太推广,原因有以下三点~~~`我怎么老是学这些自己认为不会推广的东西呢~汗 在.net中,framework4.5以上就可以由SignalR ...

  10. 夺命雷公狗---微信开发17----自定义菜单的事件推送,响应菜单的CLICK

    废话不多说,index.php 代码如下所示: <?php /** * wechat php test */ //define your token require_once "com ...

随机推荐

  1. 声网赵斌:RTE 体验提升,新一代 Killer App 将成为现实丨RTE 2022

    一年以来,在疫情及诸多综合因素的推动下,元宇宙.无人驾驶. IoT.电商直播等行业迎来井喷式发展,RTE 实时互动技术也在越来越多的场景中发挥着关键作用.在刚刚过去的 RTE 2022 第八届实时互联 ...

  2. 如何基于 React Native 快速实现一个视频通话应用

    今天,我们将会一起开发一个包含 RTE (实时互动)场景的 Flutter 应用. 项目介绍 靠自研开发包含实时互动功能的应用非常繁琐,你要解决维护服务器.负载均衡等难题,同时还要保证稳定的低延迟. ...

  3. Java方法的定义

    前言 经过前面两篇关于面向对象.类和对象的文章,壹哥相信你现在对面向对象已经有了基本的了解.我们知道,在一个类中,包括属性和行为两大核心要素.我们之前已经给大家讲解了如何定义属性,但很多同学对方法还不 ...

  4. java 环境变量配置详细教程(2023 年全网最详细)

    前言: 在上一篇文章中,壹哥给大家重点讲解了 Java 实现跨平台的原理,不知道你现在有没有弄清楚呢?如果你还有疑问,可以在评论区留言- 之前的三篇文章,主要是理论性的内容,其实你暂时跳过不看也是可以 ...

  5. python爬取猫眼电影Top100榜单的信息

    爬取并写入MySQL中 import pymysql import requests from bs4 import BeautifulSoup headers = { 'User-Agent': ' ...

  6. Android LineChart 折线图Demo

    1 首先在 build.gradle 里导入包 implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' 2.新建 启动Activity Li ...

  7. css中所有的选择器(包括比较少见的选择器)

    jQuery.CSS常用选择器 符号 描述 示例 说明 紧接无符号 相当于"并且"关系 input.k-textbox{   ...} 选出input并且包含k-textbox类的 ...

  8. uniapp安卓离线打包权限问题

    安卓离线打包需要在androidManifest.xml配置 在 manifest 节点下面加上权限(对应uniapp当中manifest.json当中安卓的权限配置),例如: <manifes ...

  9. LabVIEW之同步——集合点vi

    这是一个对我来讲比较偏的工具,做过很多项目,没有用它也能完成各种各样的项目. 今天我们一起来了解下这个工具,所以称之为工具,因为它属于NI LabVIEW的白色节点,一般是有官方利用LabVIEW代码 ...

  10. [Git]git分支查询——图像化[转载]

    1 git reflog git reflog : 可查看所有分支的所有操作记录(含: commit / reset / merge / checkout等操作) 1-1 查看分支的所有变更记录(含: ...