[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007
系列文章
微服务实践之服务注册与发现(Nacos)-SpringCloud(2020.0.x)-1
微服务实践之负载均衡(Spring Cloud Load Balancer)-SpringCloud(2020.0.x)-2
本文将介绍微服务架构中的SpringCloud Gateway这个网关组件的入门使用,观后你应该可以大体知道如网关如何工作,如何结合分布式配置,如何结合服务注册中心服务使用,如何将请求负载均衡到不同的服务实例,如何限流,如何使用断路器等实操性功能。
编程这玩意对实践啊,理论背的天花乱坠,真用的时候还是不知道怎么下手,还是要动手实践一下…
本文是一个微服务demo的一部分,以一个简单的电商购物流程为案例,用以展示微服务架构中所要解决的问题及相应开源方案。
网关是微服务架构中举足轻重的组件,由于其是进入微服务内部边界的门户,所以可以完成非常多具有切面性质的功能
本文使用SpringCloud Gateway,有关于它的详情可参考官网或者其他同学的博客。它是基于Webflux实现的一个非阻塞IO的组件,与我们常使用的基于线程池的阻塞IO实现的SpringMvc相比,高并发下同样的硬件资源(内存,CPU)下具有更高的吞吐量,其优势主要提现在IO密集场景下。
SC Gateway最核心概念其实就是一个路由(Route)。
一个路由可以被看做是对一个请求的智能处理,你可以把它看成是你们小区大门口的保安,我们暂且叫它阿路吧。当有一个人来你家里取东西,阿路就会根据各种情况智能帮你处理。每个保安的有一个名字,例如阿路(路由的Id)。你大姨妈来串门,由于来访人太多,阿路让她排队进入(路由order),阿路问你大姨妈找哪家业主(路由的Predicate),她说找王二狗,于是阿路告诉她左转左转再左转15号楼512 。但是进之前的给她正经做个核酸(路由的前置Filter),等到她串门要出来时,阿路又来了,她你签个名,说明何时离开的方便流调(路由的后置Filter)。
SC Gateway既可以使用代码来写也可以使用yml
来写路由,我们这里使用yml
文件,例如下面这样
- id: route_goods_service #阿路
uri: lb://goods-service #15号楼512
predicates:
- Path=/goods-service/** #业主王二狗
filters:
- StripPrefix=1 #做个核酸
- name: Singnature #签个名
args:
sign: 大姨妈
order: 1 #排队顺序
原理和SpringMVC那一套挺相似的,简单过一下,有个宏观的概念
先将路由(Route)转化为RouteDefinition
保存起来,是不是熟悉的味道,想想SpringMVC的BeanDefinition
首次请求,调用DispatcherHandler
里的initStrategies(ApplicationContext context)
获取各种HandlerMapping
和HandlerAdapter
保存起来。
protected void initStrategies(ApplicationContext context) {
//获取HandlerMapping
Map mappingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
context, HandlerMapping.class, true, false);
this.handlerMappings = Collections.unmodifiableList(mappings);
//获取HandlerAdapter
Map adapterBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
context, HandlerAdapter.class, true, false);
this.handlerAdapters = new ArrayList<>(adapterBeans.values());
//获取结果处理器
Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
context, HandlerResultHandler.class, true, false);
this.resultHandlers = new ArrayList<>(beans.values());
}
是不是又有一股熟悉的味道,想想SpringMvc的DispatcherSevelet
的initStrategies
方法,两个方法连签名都一样
接着调用DispatcherHandler
的handle
方法。
@Override
public Mono handle(ServerWebExchange exchange) {
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
}
这个是分发流程,具体的逻辑就隐藏在操作符里面的那几个函数调用。其中HttpWebHandlerAdapter
与RoutePredicateHandlerMapping
比较关键。但是RoutePredicateHandlerMapping
的命名我比较懵逼,按说这应该是Adapter要干的事情,不知道为什么要Mapping。
想想SpringMVC的DispatcherServlet
的 doDispatch
方法,都是一个路子。
整体流程可以查看下图:图片来自于 SpringCloud实践:Gateway网关
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R3fD77yM-1666411221287)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9be51b758e0c48d596f4fda766f54604~tplv-k3u1fbpfcp-watermark.image?)]
前面的内容全当是铺垫,主要是为了后边的使用的时候容易理解。
一个微服务架构系统中的服务几乎都是时刻准备着朝生夕死,这是微服务架构的特征,特别是进入云原生时代,在Docker与K8s的加持下,这种趋势愈发明显。它内在的思想是:你不能保证一件事100%成功,但是你的有处理失败情况的解决方案?所以我们需要服务注册中心,来时刻获取当前可用服务的坐标,以便于请求。
此处我们使用阿里开源的Nacos,它既可以做分布式配置中心也可以做服务注册中心。
首先,得益于Alibaba的微服务组件拥抱了SpringCloud,所以现在在SrpingCloud中整合阿里的Nacos非常容易。
首先在pom.xml
中使用
加入spring-clound与spring-clound-alibaba的依赖声明,然后需要什么组件就引入那个组件的依赖。例如我们这里要集成nacos的配置功能,所以我们引入了spring-cloud-starter-alibaba-nacos-config
。
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring.cloud.alibaba.version}
pom
import
在resources/bootstrap.yml
里进行配置,这块是集成过程中最困难的地方了,因为其涉及到了nacos自身的一些概念。
我们先来看一下nacos中的配置文件长什么样,打开nacos管理后台。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6avMsp9H-1666411221289)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e90393ae8d1c445ba781f0c69bcd7645~tplv-k3u1fbpfcp-watermark.image?)]
从图中红框我们可以看到3个概念:
namespace: 这个比较好理解,例如你有两套环境,开发环境和生产环境,每套环境一个命名空间
data-id: 每个配置文件的id,这个也比较好理解,就是你的配置文件叫个啥,随便取。
group: 每个配置文件的group,这货最难理解,例如你有两个服务的配置文件,一个叫goods,一个叫orders,你可以把它们设置成同一个group:buy。
只有data-id和group组合不一样才可以存在(在所有的命名空间下),意思就是同一个命名空间中data-id可以重复,只要它们属于不同的group,如下图所示。不知道为什么要这么设计,data-id起名字的时候不要重复就好了,为什么需要group呢?有知道的告诉一下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DiqQ7th3-1666411221290)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/542eeba3204e42329d9429c6c9f03fd6~tplv-k3u1fbpfcp-watermark.image?)]
了解了上面的概念,我们就来配置一下让springboot程序从nacos上读取配置。下图就是一个通用的配置,每个boot程序都可以用,不要被它吓到我稍微解释一下你就明白了
spring:
application:
name: api-gateway
cloud:
nacos:
config:
server-addr: ${spring.cloud.nacos.server.address}
namespace: ${spring.cloud.nacos.server.namespace}
file-extension: ${spring.cloud.nacos.server.file-extension}
extension-configs[0]:
data-id: base-config.yaml
group: ${spring.config.activate.on-profile}
refresh: true
extension-configs[1]:
data-id: ${spring.application.name}.yaml
group: ${spring.config.activate.on-profile}
refresh: true
---
### 指定环境
spring:
config:
activate:
on-profile: dev
cloud:
nacos:
server:
address: 127.0.0.1:8849
namespace: ns-dev
file-extension: yaml
让我们来解释一下上面的配置。
第一:在yml
语法中,可以使用---
将两个配置写在一个文件中,所以我们的配置文件分为两部分。
第二:下半部分配置了当前激活的profile:dev,以及nacos server的信息:nacos服务的地址,命名空间,配置文件的扩展名。如下图所示,你创建的时候,会为配置文件选择一个扩展名。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P1BG6t9M-1666411221292)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e1083a8dc267404f9a7a1ec9ffb06d3f~tplv-k3u1fbpfcp-watermark.image?)]
第二:上半部的配置才是nacos配置中心真正的配置。其值都是从是从下半部分读取的。就像我们开始说的,要在nacos中定位一个配置文件,需要三个要素:namespace,data-id,group。
现在唯一注意的就是nacos支持一种类似继承的配置方式,例如你的3个服务配置文件里面都有同样的配置,那么nacos支持将其抽取出来,单独写一个配置,拉取的时候再把这两个文件的配置给合并了。这里的extension-configs[0]
就是那个通用的配置文件,extension-configs[1]
就是本服务的配置文件。
至此,nacos的配置功能已经可用了
在配置完nacos配置中心的功能后,服务注册中心就比较简单了,一样的路子。
这里我需要使用nacos的服务注册功能,所以需要引入相关的依赖。
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
还是在resources/bootstrap.yml
里进行配置,在nacos标签下配置discovery即可。
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
#读取下面配置的值
server-addr: ${spring.cloud.nacos.server.address}
namespace: ${spring.cloud.nacos.server.namespace}
可见,除了nacos的地址外,还需要配置命名空间。
服务注册功能还需要使用注解@EnableDiscoveryClient
开启
@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
SrpingClout GateWay的使用也比较简单了,就是在application.yml
文件中按需求配置路由,然后来拦截外界对其发起的请求。
spring:
cloud:
gateway:
routes:
#商品服务
- id: route_goods_service
uri: lb://goods-service
predicates:
- Path=/goods-service/**
filters:
- StripPrefix=1
- name: PrefixPath
args:
prefix: /goods
我觉着初学者第一次看到这玩意应该是较懵逼的,不怕你笑话我第一次看到就很懵逼,也许有的同学天资聪颖,一看就懂吧。上面的代码定义了一个叫route_goods_service
的路由,它的路由目标为goods-service
这个服务,接着配置了一个predicate,两个filter。
例如我的网关地址为http://localhost:9000
,当向网关发起一个http://localhost:9000/goods-service/makeOrder
请求时,就会被这个路由拦截。因为我们的请求路径匹配到了predicate的条件(存在goods-service),所以进入了下面两个filter,StripPrefix将请求路径中的第一个前缀goods-service
给去掉了,而PrefixPath接着给请求路径加了一个goods
前缀,所以最终的请求路径变为了http://(goods-service服务的ip+port)/goods/makeOrder
其中个人认为初学时最难理解的就是那个predicate和filter的写法。
关于断言的理论知识我们在前面已经介绍过了,接下来我们上点干货。
Path=/goods-service/**
这什么意思呢?这其实是断言的简写,前面是它的名称,后面跟着参数,多个参数以逗号顺序分割。那个Path其实是省略了后缀后的名称,全名为PathRoutePredicateFactory
,这基本上是一个约定命名,断言都以RoutePredicateFactory
为后缀,然后名称使用前缀。
要实现一个断言非常简单,只要继承AbstractRoutePredicateFactory
类即可,然后在类里面新建一个静态内部类,例如叫Config
,作为泛型参数,Override
里面的方法即可。
最重要的方法是apply
方法,断言的判断逻辑就在这个方法里。第二个是shortcutFieldOrder
方法,这个方法是用来实现配置简写模式的,如果你不实现,那么你的predicate在用的时候就不能使用如下的简写模式:
- VipCustomer=vip-key,i-am-vip`
只能使用复杂模式
- name: VipCustomer
args:
vipKey: vip-key
vipValue: i-am-vip
下面这个自定义的predicate发现请求的header里面的存在vip-key:i-am-vip
这一Header时则返回TRUE。
@Slf4j
public class VipCustomerRoutePredicateFactory extends AbstractRoutePredicateFactory {
public static final String VIP_KEY = "vipKey";
public static final String VIP_VALUE = "vipValue";
public VipCustomerRoutePredicateFactory() {
super(Config.class);
}
//实现了这个在application.yml中配置的时候可以使用简写
@Override
public List shortcutFieldOrder() {
return Arrays.asList(VIP_KEY,VIP_VALUE);
}
@Override
public Predicate apply(Config config) {
return serverWebExchange -> {
String value = serverWebExchange.getRequest().getHeaders().getFirst(config.getVipKey());
if (!StringUtils.hasText(value) || !value.equals(config.getVipValue())) {
log.info("屌丝用户");
return false;
}
log.info("Vip用户");
return true;
};
}
public static class Config {
private String vipKey;
private String vipValue;
public String getVipKey() {
return vipKey;
}
public Config setVipKey(String vipKey) {
this.vipKey = vipKey;
return this;
}
public String getVipValue() {
return vipValue;
}
public Config setVipValue(String vipValue) {
this.vipValue = vipValue;
return this;
}
}
}
内置的predicate也基本都是这样的,唯一区别就是其apply
方法的处理逻辑比较复杂。
过滤器和断言完全是一个路子,过滤器要继承的抽象类为 AbstractGatewayFilterFactory
,配置的名称也是使用类的前缀,例如StripPrefixGatewayFilterFactory
在yml
中的名称为:StripPrefix
。但也有极个别的例外,例如CircuitBreaker
这个filter的全名是SpringCloudCircuitBreakerFilterFactory
。
下面就是内置的StripPrefix
的源码,我们稍微来看一下。
public class StripPrefixGatewayFilterFactory
extends AbstractGatewayFilterFactory {
//这个值要和下面Config类里面声明的属性名称一致,这里是parts
public static final String PARTS_KEY = "parts";
public StripPrefixGatewayFilterFactory() {
super(Config.class);
}
//覆写了这个方法就可以在配置的时候使用简写模式了
@Override
public List shortcutFieldOrder() {
return Arrays.asList(PARTS_KEY);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
...
return chain.filter(exchange.mutate().request(newRequest).build());
}
};
}
public static class Config {
private int parts = 1;
public int getParts() {
return parts;
}
public void setParts(int parts) {
this.parts = parts;
}
}
}
可见这filter只有一个int型参数,参数名称为parts
,所以我们在yml
文件中可以按照如下配置
简写
filters:
- StripPrefix=1
完整写法:
filters:
- name: StripPrefix
args:
parts: 1
在理解了基本用法后,我们就可以实现我们最开始说的那些功能了。
基于filter实现,SC gateway提供了RequestRateLimiterGatewayFilterFactory
这个filter来完成限流,如下代码所示
@ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
public class RequestRateLimiterGatewayFilterFactory
extends AbstractGatewayFilterFactory{
...
public static class Config implements HasRouteId {
//限流接口
private RateLimiter rateLimiter;
}
}
同时其还提供了一个接口RateLimiter
并提供了一个实现类RedisRateLimiter
,其是基于Redis的采用令牌桶算法的限流器。 如果你不想用RedisRateLimiter
,那你就可以自己基于RateLimiter
接口实现一个自己的限流器,例如使用 Guava,Buket4j。
下面我们看如何使用RedisRateLimiter
由于我们要使用Redis限流,肯定的需要连上redis
org.springframework.boot
spring-boot-starter-data-redis-reactive
org.apache.commons
commons-pool2
配置redis连接
spring:
redis:
#redis数据库,其有16个数据库
database: 0
#redis服务器地址
host: localhost
#redis服务器端口号
port: 6379
#连接池配置
lettuce:
pool:
enabled: true
max-active: 8
max-wait: 10s
实现KeyResolver
由于我们的决定基于什么维度限流,例如到底是基于访问者IP限流呢,还是基于访问的url限流呢,还是基于用户限流呢?这就是由KeyResolver决定的。下面我们写了一个基于请求路径限流的KeyResolver。
@Configuration
public class GatewayConfig {
@Bean
public KeyResolver pathKeyResolver() {
return new KeyResolver() {
@Override
public Mono resolve(ServerWebExchange exchange) {
// Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
String path = exchange.getRequest().getURI().getPath();
return Mono.just(path);
}
};
}
}
最后一步就是将这个filter配置到路由里面去了。
filters:
#限流
- name: RequestRateLimiter #gateway内置的一个filter
args:
# 令牌桶每秒填充速率
redis-rate-limiter.replenishRate: 1
# 令牌桶的上容量
redis-rate-limiter.burstCapacity: 3
# 使用SpEL表达式从Spring容器中获取KeyResolver Bean,用来确定使用什么维度限流,例如使用请求IP限流
# 这个是我们在自己的Config文件中定义的bean
key-resolver: "#{@pathKeyResolver}"
里面有3个参数,注释已经说的很明白了。
经过以上3步就成功配置了限流器。 如果对令牌填充速率和令牌桶容量的参数含义有疑问的话,那你需要去看下令牌桶限流算法,网上关于令牌桶限流算法的文章特别多,挑一篇质量好的看看就行。
我们使用postman的批量执行功能来并发发起请求,我们配置的令牌桶容量为3,所以并发数为3,4个并发就会触发限流
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cs7g9jWm-1666411221293)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/78171c1591814fd282a06ca21aa2c695~tplv-k3u1fbpfcp-watermark.image?)]
从图中可以看到,前3个正常执行,第4个被限流,返回429.
网关也是个程序,它也会崩溃,需要你的保护,不能因为下游服务太拉胯将网关给耗死,所以其内置支持了断路器。你可能又猜到了,这个又是基于filter实现的。Sc gateway提供了SpringCloudCircuitBreakerFilterFactory
这个抽象类,如下所示
public abstract class SpringCloudCircuitBreakerFilterFactory
extends AbstractGatewayFilterFactory {
//这个就是你要在yml文件中配置filter的名称
public static final String NAME = "CircuitBreaker";
}
同时,Sc gateway还提供了一个继承此抽象类的实现类:SpringCloudCircuitBreakerResilience4JFilterFactory
,意图很明显,这是要原生支持Resilience4J
啊,其他的我也没用过,不知道集成阿里sentinel怎么弄,是否是需要继承SpringCloudCircuitBreakerFilterFactory
类,这块等有机会研究一下。
public class SpringCloudCircuitBreakerResilience4JFilterFactory extends SpringCloudCircuitBreakerFilterFactory {
}
关于断路器,现在普遍使用的就是Resilience4J、阿里Sentinel,还有一个Netflix的Hystrix 这个不开发了,进入了维护期。今天我们这里使用Resilience4j。
org.springframework.cloud
spring-cloud-starter-circuitbreaker-reactor-resilience4j
Resilience4J
的配置这个其实比较困难,因为你要知道如何配置,你就要先理解断路的设计原理,不然你怎么可能会配置呢,不会配置也能起步拉,使用默认配置就好啦
我们的目标就是要搞ReactiveResilience4JCircuitBreakerFactory
,它里面有一个配置方法configureDefault
需要一个Resilience4JCircuitBreakerConfiguration
类型的参数。于是问题转化为搞一个这个类型的实例出来,这个类又有两个配置类TimeLimiterConfig
和CircuitBreakerConfig
,于是问题转化为给这两个类型各搞一个实例出来。下面的代码就是在干上面描述的那些事。
TimeLimiterConfig
设置请求超时打开断路器CircuitBreakerConfig
设置断路器各种参数,包括状态的转换等,这块需要仔细研究一下,可以单独写一篇断路器的文章时再说。@Configuration
public class MsCircuitBreakerConfig {
//对Resilience4J的配置
@Bean
public Customizer defaultCustomizer() {
return new Customizer() {
@Override
public void customize(ReactiveResilience4JCircuitBreakerFactory factory) {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) // 滑动窗口的类型为请求个数
.slidingWindowSize(10) // 时间窗口的大小为10个
.minimumNumberOfCalls(1) // 在单位时间窗口内最少需要1次调用才能开始进行统计计算
.failureRateThreshold(50) // 在单位时间窗口内调用失败率达到50%后会启动断路器
.enableAutomaticTransitionFromOpenToHalfOpen() // 允许断路器自动由打开状态转换为半开状态
.waitDurationInOpenState(Duration.ofSeconds(2)) // 断路器打开状态转换为半开状态需要等待2秒
.permittedNumberOfCallsInHalfOpenState(2) // 在半开状态下允许进行正常调用的次数
.recordExceptions(Throwable.class) // 所有异常都当作失败来处理
.build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(200))//接口200毫秒没有响应就认为失败了
.build();
factory.configureDefault(id -> {
return new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakerConfig)
.build();
});
}
};
}
}
这里只是在演示gateway如何使用断路器,没有细聊断路器自己的知识,这块有点多。这里我们简单的描述一下,帮助理解上面的代码。
断路器有3个状态:开,半开,关。 开状态:请求被拦截,半开状态:允许尝试几个请求,关状态:请求顺利通过。他们直接的转换关系如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dn0JdZ1Q-1666411221294)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/259a4ca32b0d41579b8a739c3e7f21f1~tplv-k3u1fbpfcp-watermark.image?)]
仍然使用postman的批量执行功能研验证,首先把限流器开大一点。这个比较麻烦一点了。
我们的网关会调用这个goods-service
的这个方法,当参数goodsId是delay时这个方法就会延时300毫秒,就会触发网关超时,因为我们设置的网关超时是200毫秒。
@GetMapping("/checkGoods")
public BaseResponse getGoods(@RequestParam("goodsId") String goodsId){
log.info("开始商品调用:{}",goodsId);
if("delay".equals(goodsId)){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
log.error("睡眠失败",e);
}
}
log.info("结束商品调用:{}",goodsId);
return ResultUtil.ok("ok");
}
我们发起10个请求,每个请求间隔800毫秒,我们给出的goodsId参数顺序为:
ok
delay
delay
delay
delay
ok
ok
ok
delay
ok
发起请求后,结果为:
1 ok 200 OK close
2 delay 504 Gateway Timeout 触发open
3 delay 503 Service Unavailable open
4 delay 503 Service Unavailable open
5 delay 504 Gateway Timeout hafe-open 由于尝试请求失败,导致断路器打开,于是请求没有被转发
6 ok 503 Service Unavailable open
7 ok 503 Service Unavailable open
8 ok 200 OK half-open 半开状态下,请求成功,所以转变为cose状态
9 delay 504 Gateway Timeout 触发open 由于请求失败,又转变为open
10 ok 503 Service Unavailable open
本文上手实践了SpringCloud Gateway,并就其核心用法、原理与功能做了解释,在整理的过程中对我自己梳理知识也有很大的帮助,如果它也帮助到了你,请不要吝惜你的赞。
文中提到的断路器,以及限流的原理面试时候特别爱问,这块有时间可以整理一下。
一如既往,你可以从Github上获得本文源码:master-microservice,请不要吝啬你的小星星
# Circuit Breaking In Spring Cloud Gateway With Resilience4J