昨晚和朋友一起尝试解决Golang 调用的API 处理时间过长,客户端等不及的问题。
场景
1
2
3
4
5
6
7
| /*
客户端 ----> service A ---> service B + cache
| <---- | |
| | ---------> |
| <--------- | <--------- |
*/
|
我们的服务A,要调用服务B的API, 服务B需要时间处理,来不及立刻返回结果给A
现在客户端调用A,希望A能返回一个信号,说任务在处理中了。过段时间再来去检验结果。 服务A过几分钟后再调用服务B,服务B会把上次的查询结果从Cache里面取出来,返回给服务A
解法
1. go func
在我们调用服务B的函数前面加一个“go”,说明不用等结果,先返回给客户端
2. 使用channel
(TBD)
3. 使用超时处理
大家有兴趣可以阅读Golang http请求的代码,如果只看超时/取消的原理,其实不难,模型其实都是一样的,通过select以及cancel channel来提前返回到底是超时还是正常的http事务,几乎所有可能超时的链路都使用了这个模型,比较有go的味道。
不过还是推荐使用context来取消/超时,不然channel到处乱飞,可维护性比较差。在我们自己的业务代码中,也可以通过这种方式来实现超时。
超时通用的代码模型如下,业务代码放到fn
里:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| type fn func(ctx context.Context) result
type result struct {
Val interface{}
Err error
}
func doWithTimeout(ctx context.Context, fn fn) result {
ch := make(chan result)
go func(ctx context.Context, ch chan<- result) {
ch <- fn(ctx)
}(ctx, ch)
select {
case <-ctx.Done(): // timeout
go func() { <-ch }() // wait ch return...
return result{Err: ctx.Err()}
case res := <-ch: // normal case
return res
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
result := doWithTimeout(ctx, func(ctx context.Context) result {
// replace with your own logic!
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.google.com/", nil)
resp, err := http.DefaultClient.Do(req)
return result{
Val: resp,
Err: err,
}
})
switch {
case ctx.Err() == context.DeadlineExceeded:
// handle timeout
case result.Err != nil:
// handle logic error
default:
// do with result.Val
}
}
|
将具体的逻辑放到了一个独立的协程中,然后select等待,需要注意的是如果已经存在上下了,没有特别的理由,就基于该context追加超时,而不是像上面那样使用context.Background()
。
最后还需要说明一下,超时虽然能够检测到,但是取消并不是那么容易,比如网络的不确定性,接收端很可能已经处理请求了,所以具体取消与否还是依赖于服务器存储的状态。
详细请参考:https://www.xiaolongtongxue.com/articles/2021/how-does-go-handle-http-request-timeout-and-cancel
文章作者
Hustbill billyzhang2010@gmail.com
上次更新
2021-11-16