3 minutes reading time
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
我在 go-redis/cache 中发现了库使用了 singleflight , 经过查阅资料,了解了 这个库的主要作用就是将一组相同的请求合并成一个请求,实际上只会去请求一次,然后对所有的请求返回相同的结果。 这样会大大降低数据库的压力。
type Group struct
// Do 执行函数, 对同一个 key 多次调用的时候,在第一次调用没有执行完的时候
// 只会执行一次 fn 其他的调用会阻塞住等待这次调用返回
// v, err 是传入的 fn 的返回值
// shared 表示是否真正执行了 fn 返回的结果,还是返回的共享的结果
func (key string, fn func() (interface, error)) (v interface, err error, shared bool)
// DoChan 和 Do 类似,只是 DoChan 返回一个 channel,也就是同步与异步的区别
func (key string, fn func() (interface, error)) <-chan Result
// Forget 用于通知 Group 删除某个 key 这样后面继续这个 key 的调用的时候就不会在阻塞等待了
func (key string)
接下来我们来讲解一个简单的例子,我们来看看 singleflight 的使用方式,先来看一个简单的例子:
先使用一个普通的例子,这时一个获取blog文章详情的函数,我们在函数里面使用一个 count 模拟不同并发下的耗时的不同,并发越多请求耗时越多。
func getBlogDetail(id int) (string, err error)
我们使用 singleflight 的时候就只需要 new(singleflight.Group) 然后调用一下相对应的 Do 方法就可了,是不是很简单
func singleflightGetArticle(sg *singleflight.Group, id int) (string, error)
我们接下来来对比下2个函数的执行时间,先来看一个简单的例子:
package main
import (
"fmt"
"golang.org/x/sync/singleflight"
"sync"
"sync/atomic"
"time"
)
var count int32
func singleflightGetBlogArticle(sg *singleflight.Group, id int) (string, error)
func getBlogDetail(id int) (string, error)
func main()
这是没有使用 singleflight 的情况下的耗时:
同时发起 1000 次请求,耗时: 1.0221228s
Process finished with the exit code 0
使用 singleflight 的情况下的耗时是:
同时发起 1000 次请求,耗时: 21.9379ms
Process finished with the exit code 0
可以看到使用了 singleflight 的耗时比未使用效果更好。
使用 singleflight 我们比较常见的是直接使用 Do 方法,但是这个极端情况下会导致整个程序 hang 住,如果我们的代码出点问题,有一个调用 hang 住了,那么会导致所有的请求都 hang 住