脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|shell|

服务器之家 - 脚本之家 - Golang - Go语言使用singleflight解决缓存击穿

Go语言使用singleflight解决缓存击穿

2024-03-25 20:24陈明勇 Golang

在构建高性能的服务时,缓存是优化数据库压力和提高响应速度的关键技术,但使用缓存也会带来一些问题,其中就包括缓存击穿,下面我们就来看看Go语言中如何使用singleflight解决缓存击穿问题吧

前言

在构建高性能的服务时,缓存是优化数据库压力和提高响应速度的关键技术。使用缓存也会带来一些问题,其中就包括 缓存击穿,它不仅会导致数据库压力剧增,引起数据库性能的下降,严重时甚至会击垮数据库,导致数据库不可用。

在 Go 语言中,golang.org/x/sync/singleflight 包提供了一种机制,确保对于任何特定 key 的并发请求在同一时刻只执行一次。这个机制有效地防止了缓存击穿问题。

本文将深入探讨 Go 语言中 singleflight 包的使用。从缓存击穿问题的基础知识开始,进而详细介绍 singleflight 包的使用,展示如何利用它来避免缓存击穿。

准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

缓存击穿

缓存击穿 是指在高并发的情况下,某个热点的 key 突然过期,导致大量的请求直接访问数据库,造成数据库的压力过大,甚至宕机的现象。

Go语言使用singleflight解决缓存击穿

缓存击穿流程图.png

常见的解决方案:

  • 设置热点数据永不过期:对于一些确定的热点数据,可以将其设置为 永不过期,这样就可以确保不会因为缓存失效而导致请求直接访问数据库。
  • 设置互斥锁:为了防止缓存失效时所有请求同时查询数据库,可以采用锁机制确保仅有一个请求查询数据库并更新缓存,而其他请求则在缓存更新后再进行访问。
  • 提前更新:后台监控缓存的使用情况,当缓存即将过期时,异步更新缓存,延长过期时间。

singleflight 包

Package singleflight provides a duplicate function call suppression mechanism.

这段英文来自官方文档的介绍,直译过来的意思是:singleflight 包提供了一种“重复函数调用抑制机制”。

换句话说,当多个 goroutine 同时尝试调用同一个函数(基于某个给定的 key)时,singleflight 会确保该函数只会被第一个到达的 goroutine 调用,其他 goroutine 会等待这次调用的结果,然后共享这个结果,而不是同时发起多个调用。

一句话概括就是 singleflight 将多个请求合并成一个请求,多个请求共享同一个结果。

组成部分

Group:这是 singleflight 包的核心结构体。它管理着所有的请求,确保同一时刻,对同一资源的请求只会被执行一次。Group 对象不需要显式创建,直接声明后即可使用。

Do 方法:Group 结构体提供了 Do 方法,这是实现合并请求的主要方法,该方法接收两个参数:一个是字符串 key(用于标识请求资源),另一个是函数 fn,用来执行实际的任务。在调用 Do 方法时,如果已经有一个相同 key 的请求正在执行,那么 Do 方法会等待这个请求完成并共享结果,否则执行 fn 函数,然后返回结果。

Do 方法有三个返回值,前两个返回值是 fn 函数的返回值,类型分别为 interface{} 和 error,最后一个返回值是一个 bool 类型,表示 Do 方法的返回结果是否被多个调用共享。

DoChan:该方法与 Do 方法类似,但它返回的是一个通道,通道在操作完成时接收到结果。返回值是通道,意味着我们能以非阻塞的方式等待结果。

Forget:该方法用于从 Group 中删除一个 key 以及相关的请求记录,确保下次用同一 key 调用 Do 时,将立即执行新请求,而不是复用之前的结果。

Result:这是 DoChan 方法返回结果时所使用的结构体类型,用于封装请求的结果。这个结构体包含三个字段,具体如下:

  • Valinterface{} 类型):请求返回的结果。
  • Errerror 类型):请求过程中发生的错误信息。
  • Sharedbool 类型):表示这个结果是否被当前请求以外的其他请求共享。

安装

通过以下命令,在 go 应用中安装 singleflight 依赖:

go get golang.org/x/sync/singleflight

使用示例

// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/singleflight/usage/main.go
package main

import (
    "errors"
    "fmt"
    "golang.org/x/sync/singleflight"
    "sync"
)

var errRedisKeyNotFound = errors.New("redis: key not found")

func fetchDataFromCache() (any, error) {
    fmt.Println("fetch data from cache")
    returnnil, errRedisKeyNotFound
}

func fetchDataFromDataBase() (any, error) {
    fmt.Println("fetch data from database")
    return"程序员陈明勇", nil
}

func fetchData() (any, error) {
    cache, err := fetchDataFromCache()
    if err != nil && errors.Is(err, errRedisKeyNotFound) {
        fmt.Println(errRedisKeyNotFound.Error())
        return fetchDataFromDataBase()
    }
    return cache, err
}

func main() {
    var (
        sg singleflight.Group
        wg sync.WaitGroup
    )

    forrange5 {
        wg.Add(1)
        gofunc() {
            defer wg.Done()
            v, err, shared := sg.Do("key", fetchData)
            if err != nil {
                panic(err)
            }
            fmt.Printf("v: %v, shared: %v\n", v, shared)
        }()
    }
    wg.Wait()
}

Go语言使用singleflight解决缓存击穿

singleflight.png

这段代码模拟了一个典型的并发访问场景:从缓存获取数据,若缓存未命中,则从数据库检索。在此过程中,singleflight 库起到了至关重要的作用。它确保在多个并发请求尝试同时获取相同数据时,实际的获取操作(不论是访问缓存还是查询数据库)只会执行一次。这样不仅减轻了数据库的压力,还有效防止了高并发环境下可能发生的缓存击穿问题。

代码运行结果如下所示:

fetch data from cache
redis: key not found         
fetch data from database     
v: 程序员陈明勇, shared: true
v: 程序员陈明勇, shared: true
v: 程序员陈明勇, shared: true
v: 程序员陈明勇, shared: true
v: 程序员陈明勇, shared: true

根据运行结果可知,当 5 个 goroutine 并发获取相同数据时,数据获取操作实际上只由一个goroutine执行了一次。此外,由于所有返回的 shared 值均为 true,这表明返回的结果被其他 4 个goroutine共享。

最佳实践

key 的设计

在生成 key 的时候,我们应该保证它的唯一性与一致性。

  • 唯一性:确保传递给 Do 方法的 key 具有唯一性,以便 Group 区分不同请求。推荐使用结构化的命名方式来保证 key 的唯一性,例如,可以遵循类似 {类型}):{标识} 的规范来构建 key。以获取用户信息为例,相应的 key 可以是 user:1234,其中 user 标识数据类型,而 1234 则是具体的用户标识。
  • 一致性:对于相同的请求,无论何时调用,生成的 key 应该保持一致,以便 Group 正确地合并相同的请求,防止非预期的错误。

超时控制

在调用 Group.Do 方法时,第一个到达的 goroutine 可以成功执行 fn 函数,而其他随后到达的 goroutine 将进入阻塞状态。如果阻塞状态持续过长,可能需要采取降级策略以保证系统的响应性,这时候,我们可以利用 Group.DoChan 方法和结合 select 语句实现超时控制。

以下是一个实现超时控制的简单示例:

// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/singleflight/timeout_control/main.go
package main

import (
    "fmt"
    "golang.org/x/sync/singleflight"
    "time"
)

func main() {
    var sg singleflight.Group
    doChan := sg.DoChan("key", func() (interface{}, error) {
        time.Sleep(4 * time.Second)
        return"程序员陈明勇", nil
    })
    select {
    case <-doChan:
        fmt.Println("done")
    case <-time.After(2 * time.Second):
        fmt.Println("timeout")
        // 采用其他降级策略
    }
}

小结

本文首先介绍了 缓存击穿 的含义及其常见的解决方案。

然后深入探讨了 singleflight 包,从基础概念、组成部分到具体的安装和使用示例。

接着通过模拟一个典型的并发访问场景来演示如何利用 singleflight 来防止在高并发场景下可能发生的缓存击穿问题。

最后,探讨在实践中设计 key 和控制请求超时的最佳策略,以便更好地理解和应用 singleflight,从而优化并发处理逻辑。

到此这篇关于Go语言使用singleflight解决缓存击穿的文章就介绍到这了,更多相关Go singleflight缓存击穿内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://juejin.cn/post/7348832432072704038

延伸 · 阅读

精彩推荐
  • Golanggolang中json的omitempty使用操作

    golang中json的omitempty使用操作

    这篇文章主要介绍了golang中json的omitempty使用操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    wilsonyx13452021-03-23
  • Golang浅谈Go Slice 高级实践

    浅谈Go Slice 高级实践

    这篇文章主要介绍了浅谈Go Slice 高级实践,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧 ...

    sxssxs4202020-05-19
  • Golanggo语言的sql包原理与用法分析

    go语言的sql包原理与用法分析

    这篇文章主要介绍了go语言的sql包原理与用法,较为详细的分析了Go语言里sql包的结构、相关函数与使用方法,需要的朋友可以参考下 ...

    轩脉刃4682020-04-30
  • GolangGo语言题解LeetCode1051高度检查器示例详解

    Go语言题解LeetCode1051高度检查器示例详解

    这篇文章主要为大家介绍了Go语言题解LeetCode1051高度检查器示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪...

    刘09k114402022-12-30
  • GolangGo语言排序与接口实例分析

    Go语言排序与接口实例分析

    这篇文章主要介绍了Go语言排序与接口,实例分析了排序与接口的使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下 ...

    秋风秋雨1612020-04-19
  • Golang如何go语言比较两个对象是否深度相同

    如何go语言比较两个对象是否深度相同

    这篇文章主要介绍了如何go语言比较两个对象是否深度相同,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下...

    reaperhero9302022-10-10
  • Golanggolang 调用c语言动态库方式实现

    golang 调用c语言动态库方式实现

    本文主要介绍了golang 调用c语言动态库方式实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    小_村长8872022-01-26
  • Golang透过一个编译报错,总结两个Go程序编译的重要知识

    透过一个编译报错,总结两个Go程序编译的重要知识

    最近调研了一下某个做 APM 的厂商的 Go 探针程序,说是引入一个包,全程不用再修改其他代码就能在项目里引入探针。...

    网管叨bi叨8002021-11-30