背景
在服务端开发中我们经常会在一个服务中发起rpc请求调用其他服务。很多服务主要逻辑就是根据产品逻辑调用各个rpc请求,再把各个请求的结果组合在一起返回。业内把专注于开发这类服务的开发者称为API Boy。这类服务的特点是io密集,耗时主要是rpc请求。所以优化这类服务的耗时就是优化rpc请求的串并行关系。那么怎样的串并行关系才是最优的呢?其实很简单,只要做到下面两点就可以了。
- 所有逻辑都在依赖数据准备好的第一时间开始跑,每个rpc请求都在请求数据准备好的第一时间发起。
- 相同的数据只请求一次。
于是我们就把一个全局的问题分解成各个局部的问题,就可以分而治之,逐个击破。本文主要讨论如何在Go语言开发的服务中管理rpc调用,实现上述两点。
下面我们先讨论几种情况。
情况1 多个相同数据的调用合并为一个
func func1() {
resp := RPC()
handle1(resp)
}
func func2() {
resp := RPC()
handle2(resp)
}
func Case1() {
go func1()
go func2()
}
func1与func2分别调用同一个rpc获得同一个结果,然后分别对结果做处理。这里func1与func2应该合并调用一次rpc,减少下游的压力。一个问题是这里并不确定func1与func2谁会先发起rpc,解决方法是使用sync.Once。
情况2 一个协程生产,一个协程消费
func func1(resp *Resp) {
resp = RPC()
}
func func2(resp *Resp) {
handle(resp)
}
func Case2() {
var resp Resp
go func1(&resp)
go func2(&resp)
}
这里func2在handle之前必须保证func1已经生产出resp。在Go,这一般用channel来实现。这里不能用sync.Once是因为func2没有生产能力,func2调用sync.Once会导致RPC调用不了。
最初的想法:混合sync.Once与channel
我想做一个通用的包裹来包住resp,做一个通用的数据获取接口获取resp,使其能囊括上面两种情况。于是我混合了sync.Once与channel,写出了下面的代码。核心是提供了InitAndGet接口来生产数据,Get接口来获取数据。
type DataGetter[T any] struct {
Data T
DataOnce sync.Once
SelfOnce sync.Once
IsReady chan struct{}
}
func (d *DataGetter[T]) InitAndGet(initFunc func(*T)) *T {
d.InitSelf()
d.DataOnce.Do(func() {
DoInit(initFunc)
})