本系列代码地址:https:github。comJoJoTecspringcloudparent需要重试的场景 微服务系统中,会遇到在线发布,一般的发布更新策略是:启动一个新的,启动成功之后,关闭一个旧的,直到所有的旧的都被关闭。SpringBoot具有优雅关闭的功能,可以保证请求处理完再关闭,同时会拒绝新的请求。对于这些拒绝的请求,为了保证用户体验不受影响,是需要重试的。 云上部署的微服务,对于同一个服务,同一个请求,很可能不会所有实例都同时异常,例如:Kubernetes集群部署的实例,可能同一个虚拟机Node在闲时部署了多个不同微服务实例,当压力变大时,就需要迁移和扩容。这时候由于不同的微服务压力不同,当时处于哪一个Node也说不定,有的可能处于压力大的,有的可能处于压力小的。对于同一个微服务,可能并不会所有实例位于的Node压力都大。云上部署一般会跨可用区部署,如果有一个可用区异常,另一个可用区还可以继续提供服务。某个业务触发了Bug,导致实例一直在GC,但是这种请求一般很不常见,不会发到所有实例上。 这时候,就需要我们对请求进行无感知的重试。重试需要考虑的问题重试需要重试与之前不同的实例,甚至是不处于同一个虚拟机Node的实例,这个主要通过LoadBalancer实现,可以参考之前的LoadBalancer部分。后面的文章,我们还会改进LoadBalancer重试需要考虑到底什么请求能重试,以及什么异常能重试:假设我们有查询接口,和没有做幂等性的扣款接口,那么很直观的就能感觉出查询接口是可以重试的,没有做幂等性的扣款接口是不能重试的。业务上不能重试的接口,对于特殊的异常(其实是表示请求并没有发出去的异常),我们是可以重试的。虽然是没有做幂等性的扣款接口,但是如果抛出的是原因是ConnectTimeout的IOException,这样的异常代表请求还没有发出去,是可以重试的。重试策略:重试几次,重试间隔。类比多处理器编程模式中的BusySpin策略会造成很大的总线通量从而降低性能这个现象,如果失败立刻重试,那么在某一个实例异常导致超时的时候,会在同一时间有很多请求重试到其他实例。最好加上一定延迟。使用resilience4j实现FeignClient重试 FeignClient本身带重试,但是重试策略相对比较简单,同时我们还想使用断路器以及限流器还有线程隔离,resilience4j就包含这些组件。原理简介 Resilience4J提供了Retryer重试器,官方文档地址:https:resilience4j。readme。iodocsretry 从配置上就能理解其中的原理,但是官方文档配置并不全面,如果想看所有的配置,最好还是通过源码: RetryConfigurationProperties。java重试间隔,默认500msNullableprivateDurationwaitDuration;重试间隔时间函数,和waitDuration只能设置一个,默认就是waitDurationNullableprivateClasslt;?extendsIntervalBiFunctionObjectintervalBiFunction;最大重试次数,包括本身那次调用NullableprivateIntegermaxAttempts;通过抛出的异常判断是否重试,默认是只要有异常就会重试NullableprivateClasslt;?extendsPredicateThrowableretryExceptionPredicate;通过结果判断是否重试,默认是只要获取到结果就不重试NullableprivateClasslt;?extendsPredicateObjectresultPredicate;配置抛出这些异常以及子类则会重试SuppressWarnings(unchecked)NullableprivateClasslt;?extendsThrowable〔〕retryExceptions;配置抛出这些异常以及子类则不会重试SuppressWarnings(unchecked)NullableprivateClasslt;?extendsThrowable〔〕ignoreExceptions;启用ExponentialBackoff延迟算法,初次重试延迟时间为waitDuration,之后每次重试延迟时间都乘以exponentialBackoffMultiplier,直到exponentialMaxWaitDurationNullableprivateBooleanenableExponentialBackoff;privateDoubleexponentialBackoffMultiplier;privateDurationexponentialMaxWaitDuration;启用随机延迟算法,范围是waitDurationrandomizedWaitFactorwaitDurationwaitDurationrandomizedWaitFactorwaitDurationNullableprivateBooleanenableRandomizedWait;privateDoublerandomizedWaitFactor;NullableprivateBooleanfailAfterMaxAttempts; 引入resilience4jspringboot2的依赖,就可以通过Properties配置的方式去配置Retryer等所有resilience4j组件,例如: application。ymlresilience4j。retry:configs:default:最大重试次数,包括第一次调用maxRetryAttempts:2重试等待时间waitDuration:500ms启用随机等待时间,范围是waitDurationrandomizedWaitFactorwaitDurationwaitDurationrandomizedWaitFactorwaitDurationenableRandomizedWait:truerandomizedWaitFactor:0。5testclient1:最大重试次数,包括第一次调用maxRetryAttempts:3重试等待时间waitDuration:800ms启用随机等待时间,范围是waitDurationrandomizedWaitFactorwaitDurationwaitDurationrandomizedWaitFactorwaitDurationenableRandomizedWait:truerandomizedWaitFactor:0。5 这样,我们就可以通过如下代码,获取到配置对应的Retryer:AutowiredRetryRegistryretryRegistry;读取resilience4j。retry。configs。testclient1下的配置,构建Retry,这个Retry命名为retry1Retryretry1retryRegistry。retry(retry1,testclient1);读取resilience4j。retry。configs。default下的配置,构建Retry,这个Retry命名为retry1不指定配置名称即使用默认的default下的配置Retryretry2retryRegistry。retry(retry2); 引入resilience4jspringcloud2的依赖,就相当于引入了resilience4jspringboot2的依赖。并在其基础上,针对springcloudconfig的动态刷新RefreshScope机制,增加了兼容。dependencygroupIdio。github。resilience4jgroupIdresilience4jspringcloud2artifactIddependency使用resilience4jfeign给OpenFeign添加重试 官方提供了粘合OpenFeign的依赖库,即resilience4jfeigndependencygroupIdio。github。resilience4jgroupIdresilience4jfeignartifactIddependency 接下来,我们使用这个依赖,给OpenFeign添加重试,首先启用OpenFeignClient并指定默认配置: OpenFeignAutoConfigurationEnableFeignClients(valuecom。github。jojotech,defaultConfigurationDefaultOpenFeignConfiguration。class) 在这个默认配置中,通过覆盖默认的Feign。Builder的方式粘合resilience4j添加重试:BeanpublicFeign。Builderresilience4jFeignBuilder(ListFeignDecoratorBuilderInterceptorfeignDecoratorBuilderInterceptors,FeignDecorators。Builderbuilder){feignDecoratorBuilderInterceptors。forEach(feignDecoratorBuilderInterceptorfeignDecoratorBuilderInterceptor。intercept(builder));returnResilience4jFeign。builder(builder。build());}BeanpublicFeignDecorators。BuilderdefaultBuilder(Environmentenvironment,RetryRegistryretryRegistry){Stringnameenvironment。getProperty(feign。client。name);Retryretrynull;try{retryretryRegistry。retry(name,name);}catch(ConfigurationNotFoundExceptione){retryretryRegistry。retry(name);}覆盖其中的异常判断,只针对feign。RetryableException进行重试,所有需要重试的异常我们都在DefaultErrorDecoder以及Resilience4jFeignClient中封装成了RetryableExceptionretryRetry。of(name,RetryConfig。from(retry。getRetryConfig())。retryOnException(throwable{returnthrowableinstanceoffeign。RetryableException;})。build());returnFeignDecorators。builder()。withRetry(retry);}