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

2024-04-18 0 460
目录
  • 前言
  • 缓存击穿
  • singleflight 包
    • 组成部分
    • 安装
    • 使用示例
  • 最佳实践
    • key 的设计
    • 超时控制
  • 小结

    前言

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

    在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方法返回结果时所使用的结构体类型,用于封装请求的结果。这个结构体包含三个字段,具体如下:

    • Val(interface{}类型):请求返回的结果。
    • Err(error类型):请求过程中发生的错误信息。
    • Shared(bool类型):表示这个结果是否被当前请求以外的其他请求共享。

    安装

    通过以下命令,在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 cacheredis: key not found fetch data from database v: 程序员陈明勇, shared: truev: 程序员陈明勇, shared: truev: 程序员陈明勇, shared: truev: 程序员陈明勇, shared: truev: 程序员陈明勇, 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缓存击穿内容请搜索悠久资源网以前的文章或继续浏览下面的相关文章希望大家以后多多支持悠久资源网!

    您可能感兴趣的文章:

    • 使用Golang的singleflight防止缓存击穿的方法
    • golang 防缓存击穿singleflight的实现
    • Gosingleflight使用以及原理
    • golang使用sync.singleflight解决热点缓存穿透问题
    • go singleflight缓存雪崩源码分析与应用
    • 一文教你学会Go中singleflight的使用

    收藏 (0) 打赏

    感谢您的支持,我会继续努力的!

    打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
    点赞 (0)

    悠久资源 Golang Go语言使用singleflight解决缓存击穿 https://www.u-9.cn/jiaoben/golang/186938.html

    常见问题

    相关文章

    发表评论
    暂无评论
    官方客服团队

    为您解决烦忧 - 24小时在线 专业服务