在前面三节中,我已经介绍过了如果从零开始,编写 GRPC 的 Proto,编译成可以直接编写业务代码的脚手架,然后在上一节中我介绍了如果通过 GRPC 的服务端的 Interceptor 来做鉴权处理,那么在看完上一篇之后,除了脑子里想到的该如何实现 Interceptor 的问题之外,不知道你是否还有其他好奇的东西?

不知道你的答案是啥,但是对于我来说是有的,我之前的想法是既然服务端有这个东西,那么客户端有吗?如果客户端有,那么它的作用是什么?是的,今天这篇文章我要说的就是回答这个问题的一个实例,那就是客户端的调用超时重试。只要有开发过调用其他 API 代码的同学,无论是前端后端还是全栈,肯定对于调用失败都是门儿清。什么参数错误,认证授权失败,网络异常,服务器异常啊,等等,当遇到这些错误的时候,我们很多时候第一反应就是怎么可能是我的问题,肯定是服务器脑壳卡了,我再试一遍,于是乎,我们代码里面就会有不管怎样,再来一遍试试再说的冲动。

既然有这类需求,那么代码可能就会这么写了:

Line 11Line 13 就是一个一模一样的调用,只不过 Line 13 是在 Line 11 失败之后的重试。还是前面的问题,当我们只有一个函数调用的时候情况倒也还好,大不了写多一遍嘛,两个调用也行,那就写两遍,但是如果我们有一堆呢。例如我有一个函数,依赖于 5 个服务,分别是身份验证、权限验证、商品数量验证、订单价格核实、订单支付,那么我至少要写 10 遍,这是很恐怖的,毕竟这只是我下订单的一个函数,除此之外,还有浏览商品,加购物车这些操作呢,不崩溃才怪。

于是乎,我们就会想偷懒看是不是有什么方式可以在失败之后就自动重新调用了,事实上是有的,因为想偷懒的人不只有我们,还有一些比较前面的人已经偷懒过了,并且为我们留下了一下有用的东西,例如:grpc_retry。通过 grpc_retry,我们可以不用自己写重试代码了,直接就可以让代码天然支持重试,只需要这么设置一个:

可以看到这里的修改还是很简单的,这里为了简单,我也只写了一个 Unary 的 Interceptor,然后设置了一个参数 2,表示重试多一次总共试 2 次就好了,其他地方都不用改变;为了验证我们真的会重试,这里我对服务端也进行一个修改,让它在每次请求之后都会打印一下有客户端请求,我做的修改就像这样:

这里我打印一下服务端是接受到请求了,并且故意返回了一个错误,让客户端知道本次请求是失败的,以便触发客户端的重试,然后先运行服务器后再运行客户端你会在服务端的日志中看到两行 server 表示真的被请求了两次,这样我们的重试机制也算是成功了。

可能你会觉得事情到了这里也就完成了,但是,必须提醒一下的是,并不是所有请求都是可以重试的,例如经常被拿来举例子的扣库存的例子,如果因为网络不好或者拥堵导致其实库存服务已经减少了库存,但是调用端没有收到,又重试了一次,如果你没有一个良好的业务上的去重方式的话,那么会导致库存再次被减少,这在专业上有个名词叫做”非幂等”。所以,这里我们就多了一个新的需求,需要分辨调用的服务是否是”幂等”的。

可惜的是,这个库居然没有提供这种简单的机制,而是使用一个参数的形式来实现,如果让我来实现的话,我会选择通过 Context 传递是否是幂等的,然后根据这个属性来决定要不要进行重试,但是这里我选择直接使用它的库,代码如下:

虽然现在我们已经解决了幂等的问题,但是,接着又有问题来了,那就是我们发现,虽然有些服务是幂等的,但是有些情况下根本没有重试的必要,例如用户密码都错误了,这重试啥啊,不是浪费时间和资源嘛。所以,接下来一个问题就是如何针对有必要的异常做重试,这个也是比较简单的,看代码:

这里我加了一行,列举了一些 GRPC 的错误码,表示当遇到这些错误码的时候帮我重试看看,否则,就直接报错吧,不要辛苦了。

那么事情到这里就算告一段落啦,通过这几个示例,我相信以你的聪明才智应该已经能自己创造出一些花样来了,不妨去试试看。