当前位置: 首页 > news >正文

golang sync.singleflight 解决热点缓存穿透问题

gosync 包中,有一个 singleflight 包,里面有一个 singleflight.go 文件,代码加注释,一共 200 行出头。内容包括以下几块儿:

  1. Group 结构体管理一组相关的函数调用工作,它包含一个互斥锁和一个 map,mapkey 是函数的名称,value 是对应的 call 结构体。
  2. call 结构体表示一个 inflight 或已完成的函数调用,包含等待组件 WaitGroup、调用结果 valerr、调用次数 dups 和通知通道 chans
  3. Do 方法接收一个 key 和函数 fn,它会先查看 map 中是否已经有这个 key 的调用在 inflight,如果有则等待并返回已有结果,如果没有则新建一个 call 并执行函数调用。
  4. DoChan 类似 Do 但返回一个 channel 来接收结果。
  5. doCall 方法包含了具体处理调用的逻辑,它会在函数调用前后添加 deferrecover panic 和区分正常 returnruntime.Goexit
  6. 如果发生 panic,会将 panicwraps 成错误返回给等待的 channel,如果是 goexit 会直接退出。正常 return 时会将结果发送到所有通知 channel
  7. Forget 方法可以忘记一个 key 的调用,下次 Do 时会重新执行函数。

这个包通过互斥锁和 map 实现了对相同 key 的函数调用去重,可以避免对已有调用的重复计算,同时通过 channel 机制可以通知调用者函数执行结果。在一些需要确保单次执行的场景中,可以使用这个包中的方法。

通过 singleflight 可以很容易实现缓存和去重的效果,避免重复计算,接下来,我们来模拟一下并发请求可能导致的缓存穿透场景,以及如何用 singleflight 包来解决这个问题:

package mainimport ("context""fmt""golang.org/x/sync/singleflight""sync/atomic""time")type Result string
// 模拟查询数据库
func find(ctx context.Context, query string) (Result, error) {return Result(fmt.Sprintf("result for %q", query)), nil
}func main() {var g singleflight.Groupconst n = 200waited := int32(n)done := make(chan struct{})key := "this is key"for i := 0; i < n; i++ {go func(j int) {v, _, shared := g.Do(key, func() (interface{}, error) {ret, err := find(context.Background(), key)return ret, err})if atomic.AddInt32(&waited, -1) == 0 {close(done)}fmt.Printf("index: %d, val: %v, shared: %v\n", j, v, shared)}(i)}select {case <-done:case <-time.After(time.Second):fmt.Println("Do hangs")}time.Sleep(time.Second * 4)
}

在这段程序中,如果重复使用查询结果,shared 会返回 true,穿透查询会返回 false

上面的设计中还有一个问题,就是在 Do 阻塞时,所有请求都会阻塞,内存可能会出现大的问题。

此时,Do 可以更换为DoChan,两者实现上完全一样,不同的是,DoChan() 通过 channel 返回结果。因此可以使用 select 语句实现超时控制

ch := g.DoChan(key, func() (interface{}, error) {ret, err := find(context.Background(), key)return ret, err
})
// Create our timeout
timeout := time.After(500 * time.Millisecond)var ret singleflight.Result
select {
case <-timeout: // Timeout elapsedfmt.Println("Timeout")return
case ret = <-ch: // Received result from channelfmt.Printf("index: %d, val: %v, shared: %v\n", j, ret.Val, ret.Shared)
}

在超时时主动返回,不阻塞。

此时又引入了另一个问题,这样的每一次的请求,并不是高可用的,成功率是无法保证的。这时候可以增加一定的请求饱和度来保证业务的最终成功率,此时一次请求还是多次请求,对于下游服务而言并没有太大区别,此时使用  singleflight  只是为了降低请求的数量级,那么可以使用 Forget() 来提高下游请求的并发。

ch := g.DoChan(key, func() (interface{}, error) {go func() {time.Sleep(10 * time.Millisecond)fmt.Printf("Deleting key: %v\n", key)g.Forget(key)}()ret, err := find(context.Background(), key)return ret, err
})

当然,这种做法依然无法保证100%的成功,如果单次的失败无法容忍,在高并发的场景下需要使用更好的处理方案,比如牺牲一部分实时性、完全使用缓存查询 + 异步更新等。

http://www.lryc.cn/news/101517.html

相关文章:

  • 4、Linux驱动开发:设备-设备号设备号注册
  • C++(MFC)调用Python
  • 深度学习实践——循环神经网络实践
  • docker简单web管理docker.io/uifd/ui-for-docker
  • SpringBoot内嵌的Tomcat:
  • 企业级docker应用注意事项
  • 腾讯云高性能计算集群CPU服务器处理器说明
  • tinkerCAD案例:23.Tinkercad 中的自定义字体
  • Box-Cox 变换
  • Linux wc命令用于统计文件的行数,字符数,字节数
  • Python读取多个栅格文件并提取像元的各波段时间序列数据与变化值
  • Linux 之 wget curl
  • AngularJS 和 React区别
  • 【Solr】Solr搜索引擎使用
  • 一起学算法(选择排序篇)
  • 智能体的主观和能动
  • AB 压力测试
  • 多旋翼物流无人机节能轨迹规划(Python代码实现)
  • Vue通过指令 命令将打包好的dist静态文件上传到腾讯云存储桶 (保存原有存储目录结构)
  • Linux 新硬盘分区,挂载
  • Stable Diffusion 开源模型 SDXL 1.0 发布
  • NoSQL--------- Redis配置与优化
  • Ubuntu中关闭防火墙
  • java-马踏棋盘
  • 系统架构设计师-软件架构设计(4)
  • 51单片机--AD/DA
  • 网络安全-防御需知
  • C#百万数据处理
  • windows端口占用
  • 如何理解Diffusion