目录
在调用第三方服务时或者微服务间调用时,重试策略是一种常见的开发模式,即当调用服务不符合预期时(错误、数据异常),进行多次重试,以期待在下一次重试时,获得期待的返回值。
1. 在什么情况下应该重试
我们需要考虑的因素包括
- 错误类型:例如网络异常或者请求频次限制一般可以通过重试解决,而如果是请求数据参数缺失,返回 4xx 错误,此时就不应该重试
- 调用服务的幂等性:要特别关注,你调用的服务重复处理相同参数的请求,会不会造成 bug,例如数据一致性的问题
- 成本和资源消耗:例如下游服务本身非常耗费资源,就要谨慎考虑是否需要重试;另外还需要考虑网络成本
- 用户体验:考虑重试对用户体验的影响,如是否会导致用户长时间等待或出现错误提示;这里的用户也可能是接口调用方,因为太多的重试耗费的时间,可能直接导致你的上游调用方超时
下面的表格例举了一些常见错误的重试策略
错误类型 | 接口幂等 是否应该重试 | 接口非幂等 是否应该重试 |
Connection Timeout 链接超时,这时目标服务还没有开始处理请求 | 是 | 是 |
Read Timeout 读超时,这时目标服务已经开始处理请求了 | 是 | 否 |
Circuit Breaker Tripped 微服务中的一种保护策略,目标服务并未收到请求 | 是 | 是 |
400: Bad Request 请求异常,例如请求方法不对,参数不全等,重试没有意义 | 否 | 否 |
401: Unauthorized 未授权,重试没有意义 | 否 | 否 |
404: Not Found 资源不存在,,重试没有意义 | 否 | 否 |
429: Too Many Request 请求过多,需要较长时间的重试间隔 否则会造成短时间的请求翻倍,增加问题严重性 | 是(长重试间隔) | 是(长重试间隔) |
500: Internal Server Error 造成服务器内部错误的问题可能会有很多,有的通过重试可以解决,有的则不行,所以最好进行重试 | 是 | 是 |
503: Service Umavailable 这个错误一种常见的场景是网关层检测不到后端可用的服务时返回,通过重试,有望解决该问题 | 是 | 是 |
2. 常见的重试实现方式
一般有如下两种常见的重试方式
- 程序流程内重试
- 方案:在循环中,try catch 住请求代码,在判断需要进行重试时,sleep 一段时间
- 优点
- 实现简单
- 实时请求和后台服务都可以尝试这种重试策略
- 缺点
- 可能会导致程序在重试期间占用过多资源,影响整体性能
- 实现复杂的重试策略时,会造成重试逻辑和业务逻辑耦合在一起,增加维护成本
- 可能在本来下游服务压力大的时候,造成请求成倍增长
- 通过消息队列控制重试
- 方案
- 当发生意外情况时,将 上下文数据和重试次数 打包发送到某个队列系统或者类似功能的系统(数据库也可以担任这一角色)中,其中 上下文数据 用来恢复现场
- 单独的定时任务或者队列消费者,异步进行重试操作。
- 优点
- 解耦重试逻辑与主业务逻辑,使主流程更加简洁清晰,在实现复杂的重试逻辑时尤为明显
- 便于对重试操作进行集中管理和监控,方便统计重试次数、成功率等指标
- 对于一些资源密集型或耗时较长的重试操作,可以在单独的线程或进程中进行,减少对主服务的影响
- 缺点
- 不适用于实时服务,更加适用于后台服务
- 引入了额外的消息队列或类似系统,增加了系统的复杂性和维护成本
- 方案
3. 开箱即用的重试工具
以下是 Java 和 Python 中一些常见的开箱即用或便捷实现重试的机制和第三方包(但这些工具都只简化了程序流程内重试这一方案):
- Java:
- Spring Retry:如果您使用 Spring 框架,可以使用 Spring Retry 模块来实现重试功能。
- Guava:提供了 Retryer 类来实现简单的重试逻辑。
- Python:
- tenacity:这是一个强大且易于使用的 Python 重试库。
- retrying:可以方便地设置重试策略,例如重试次数、间隔等。
参考文档: