为什么需要共享调用
想象一下并发场景下多个请求执行同样的操作,为了防止压力过大我们一般会这样做?
比如:并发的从数据库获取同一个用户的信息,如果都放开请求,无疑导致的资源严重浪费,并且数据库压力也会比较大。
常规做法是: 加互斥锁,从根源上阻止同一个时间的并发调用。
这种方式实现起来比较简单,但是需要根据不同场景考虑是加非阻塞锁还是阻塞锁,高并发场景推荐非阻塞锁,低频场景如果是阻塞锁超时时间设置多久呢?
那么有没有更简单高效的方式来解决上面提到的边界问题呢?
有的,共享调用!
什么是共享调用
所谓共享调用指的是对于相同操作并发调用时,只会产生一个真实调用操作,其他调用共享结果。
这种机制可以完美的解决缓存击穿问题。
缓存击穿
某一热点 key缓存失效,导致大量相同的请求直接查询DB。 \
缓存雪崩
对于多个热点 key ,在某一刻同时失效,导致大量请求直接查询DB,造成DB压力过大崩溃。\
缓存穿透
请求id非法,实际数据库并不存在该id导致无法命中缓存所有请求直接查询DB。
后面写篇文章专门谈go-zero的缓存策略(挖坑中)。
代码实现
core/syncx/singleflight.go
核心机制第一个请求利用 waitGroup 只添加一个信号量限制只执行一次真实调用,其他人都得等待 waitGroup.Done()事件通知。
java 中的 countDownLatch 也可以实现类似效果。
执行真实调用后,是先删除 map key,value 还是先执行 waitGroup.Done()呢?
go-zero 官网文档有这样的描述,我不是很理解
// delete key first, done later. can't reverse the order, because if reverse,
// another Do call might wg.Wait() without get notified with wg.Done()
我的理解是
func (g *flightGroup) makeCall(c *call, key string, fn func() (interface{}, error)) {
//执行完成时回调此函数
defer func() {
c.wg.Done()
//加锁
g.lock.Lock()
//为什么可以先删除map记录再执行waitGroup.Done()呢?
//关键在于 c, ok := g.calls[key] 这里拿到的其实是指针
//所以不管先清除map还是waitGroup.Done()其实都是可以的
delete(g.calls, key)
g.lock.Unlock()
}()
//执行函数
c.val, c.err = fn()
}
package syncx
import "sync"
type (
// SharedCalls is an alias of SingleFlight.
// Deprecated: use SingleFlight.
SharedCalls = SingleFlight
// SingleFlight lets the concurrent calls with the same key to share the call result.
// For example, A called F, before it's done, B called F. Then B would not execute F,
// and shared the result returned by F which called by A.
// The calls with the same key are dependent, concurrent calls share the returned values.
// A ------->calls F with key<------------------->returns val
// B --------------------->calls F with key------>returns val
//进程内共享调用
//同一时间多个相同的操作只会产生一次真实调用
//其他人等待调用完成并共享结果
SingleFlight interface {
//方法调用
Do(key string, fn func() (interface{}, error)) (interface{}, error)
//与Do的区别在于返回值多了一个是否执行标识
DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error)
}
//函数调用结果结构体
call struct {
//同步标识
wg sync.WaitGroup
//返回值
val interface{}
err error
}
//实现类
flightGroup struct {
calls map[string]*call
//go中没有并发安全的map,只能依靠lock
lock sync.Mutex
}
)
// NewSingleFlight returns a SingleFlight.
func NewSingleFlight() SingleFlight {
return &flightGroup{
calls: make(map[string]*call),
}
}
// NewSharedCalls returns a SingleFlight.
// Deprecated: use NewSingleFlight.
func NewSharedCalls() SingleFlight {
return NewSingleFlight()
}
func (g *flightGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
//创建函数调用接收者,如果已经创建则等待其他执行者完成并返回
c, done := g.createCall(key)
//其他人已执行,直接拿结果
if done {
return c.val, c.err
}
//我是第一个执行人,执行调用
g.makeCall(c, key, fn)
return c.val, c.err
}
func (g *flightGroup) DoEx(key string, fn func() (interface{}, error)) (val interface{}, fresh bool, err error) {
c, done := g.createCall(key)
if done {
return c.val, false, c.err
}
g.makeCall(c, key, fn)
return c.val, true, c.err
}
//创建函数调用接收者
func (g *flightGroup) createCall(key string) (c *call, done bool) {
//并发场景下这里需要加锁
g.lock.Lock()
//有其他人正在执行
if c, ok := g.calls[key]; ok {
//第一时间释放锁
g.lock.Unlock()
//等待完成其他人完成调用
c.wg.Wait()
return c, true
}
//没有人执行任务
//就由我第一个来创建接收对象
c = new(call)
//同步信号+1
//共享调用的关键就在于此
//只能有一个人执行真正的函数调用,其他人都在等待
c.wg.Add(1)
g.calls[key] = c
g.lock.Unlock()
return c, false
}
//执行函数调用
func (g *flightGroup) makeCall(c *call, key string, fn func() (interface{}, error)) {
//执行完成时回调此函数
defer func() {
//加锁
g.lock.Lock()
//第一次看这里的逻辑很疑惑
//为什么可以先删除map记录再执行waitGroup.Done()呢?
//关键在于 c, ok := g.calls[key] 这里拿到的其实是指针
指针就就意味着所有人都可以监听到waitGroup.Done()信号。
//所以不管先清除map还是waitGroup.Done()其实都是可以的
delete(g.calls, key)
g.lock.Unlock()
c.wg.Done()
}()
//执行函数
c.val, c.err = fn()
}