Go语言使用singleflight解决缓存击穿
作者:陈明勇
前言
在构建高性能的服务时,缓存是优化数据库压力和提高响应速度的关键技术。使用缓存也会带来一些问题,其中就包括 缓存击穿,它不仅会导致数据库压力剧增,引起数据库性能的下降,严重时甚至会击垮数据库,导致数据库不可用。
在 Go
语言中,golang.org/x/sync/singleflight
包提供了一种机制,确保对于任何特定 key
的并发请求在同一时刻只执行一次。这个机制有效地防止了缓存击穿问题。
本文将深入探讨 Go
语言中 singleflight
包的使用。从缓存击穿问题的基础知识开始,进而详细介绍 singleflight
包的使用,展示如何利用它来避免缓存击穿。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。
缓存击穿
缓存击穿 是指在高并发的情况下,某个热点的 key
突然过期,导致大量的请求直接访问数据库,造成数据库的压力过大,甚至宕机的现象。
缓存击穿流程图.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() }
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缓存击穿内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!