微服务
微服务是一种软件架构风格,它是以专注于单一职责的很多小型项目为基础,组合出复杂的大型应用。

微服务拆分
什么时候拆分?
- 创业型项目:先采用单体架构,快速开发,快速试错。随着规模扩大,逐渐拆分。
- 确定的大型项目:资金充足,目标明确,可以直接选择微服务架构,避免后续拆分的麻烦。
怎么拆分?
从拆分目标来说,要做到:
- 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
- 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖。
从拆分方式来说,一般包含两种方式:
- 纵向拆分:按照业务模块来拆分
- 横向拆分:抽取公共服务,提高复用性
黑马商城
以黑马商城为例,原来所有业务都在一个单体项目里,现在我们可以把他拆分为以下五个微服务
- 用户服务
- 商品服务
- 订单服务
- 购物车服务
- 支付服务
下面是拆分完毕的项目结构,这里因为测试了负载均衡,所以启动了两个商品微服务实例


注册中心
在微服务远程调用的过程中,包括两个角色:
- 服务提供者:提供接口供其它微服务访问,比如
item-service
- 服务消费者:调用其它微服务提供的接口,比如
cart-service
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念
我们这里就采用了Nacos(Alibaba公司出品,目前被集成在SpringCloudAlibaba中)
导入数据库
首先需要将nacos所需要的sql文件导入数据库(这里以mysql为例)

配置数据库
然后创建一个配置文件在/root/nacos/custom.env
注意这里其实就是在配置nacos的数据库
1 2 3 4 5 6 7 8 9
| PREFER_HOST_MODE=hostname MODE=standalone SPRING_DATASOURCE_PLATFORM=mysql MYSQL_SERVICE_HOST=192.168.88.188 MYSQL_SERVICE_DB_NAME=nacos MYSQL_SERVICE_PORT=3306 MYSQL_SERVICE_USER=root MYSQL_SERVICE_PASSWORD=123 MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
|
创建Nacos容器
使用docker创建nacos容器
1 2 3 4 5 6 7 8
| docker run -d \ --name nacos \ --env-file ./nacos/custom.env \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ --restart=always \ nacos/nacos-server:v2.1.0-slim
|
启动完成后就可以访问nacos登录页:http://IP:8848/nacos/,账号密码都是nacos
服务注册
在application.yml里添加nacos配置
1 2 3 4 5 6
| spring: application: name: item-service cloud: nacos: server-addr: 192.168.88.188:8848
|
访问控制台即可发现服务注册成功,可以看到服务实例信息
服务发现
在pom.xml里引入依赖,并在在application.yml里添加nacos配置
1 2 3 4 5
| <!--nacos 服务注册发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
之后,服务调用者就可以利用负载均衡的算法,从多个实例中挑选一个去访问订阅服务。
常见的负载均衡算法有:
这里我们可以选择最简单的随机负载均衡。
OpenFeign远程调用
引入依赖
OpenFeign可以让远程调用像本地方法调用一样简单
首先引入OpenFeign的依赖和负载均衡的依赖
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
|
还有连接池的依赖,这里选用okhttp
1 2 3 4 5
| <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
|
在application.yml里配置连接池生效
1 2 3
| feign: okhttp: enabled: true
|
启用组件
接下来,我们在项目启动类上添加注解,启动OpenFeign功能:
1 2 3 4 5 6 7 8
| @EnableFeignClients(basePackages = "top.zhengru.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class) @MapperScan("top.zhengru.hmall.pay.mapper") @SpringBootApplication public class PayApplication { public static void main(String[] args) { SpringApplication.run(PayApplication.class, args); } }
|
使用方法
一般来说,为了避免重复编写Client接口(后面会提到),会采取两种方法来抽取代码(如图)
- 思路1:抽取到微服务之外的公共
module
- 思路2:每个微服务自己抽取一个
module
方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。

我们这里采用了第二种方法,所以其他的服务可以通过依赖引入的方法来使用Client
1 2 3 4 5
| <dependency> <groupId>top.zhengru.hmall</groupId> <artifactId>hm-api</artifactId> <version>1.0.0</version> </dependency>
|
接着可以看一下Client的写法
1 2 3 4 5 6 7 8 9
| @FeignClient("item-service") public interface ItemClient {
@GetMapping("/items") List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
@PutMapping("/stock/deduct") void deductStock(@RequestBody List<OrderDetailDTO> items); }
|

在对应的service里,通过Client来远程调用
1 2
| private final ItemClient itemClient;
|
1 2
| List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
|
配置日志级别
需要在开头启动类的注解@EnableFeignClients里配置全局生效,想要局部生效也可以在某个Client里配置
1 2 3 4 5 6
| public class DefaultFeignConfig { @Bean public Logger.Level feignLogLevel(){ return Logger.Level.FULL; } }
|
网关是什么
数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。
如下图所示,前端需要先请求网关,再访问某个微服务,网关有安全控制,登录校验,还有路由转发,负载均衡的作用。

本文使用的网关实现方案是基于Spring的WebFlux技术,完全支持响应式编程,吞吐能力更强的SpringCloudGateway
网关使用
引入依赖
创建网关微服务,并且在pom.xml里引入SpringCloudGateway、NacosDiscovery依赖,并且和其他微服务一样,需要一个启动类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
|
网关服务的启动类
1 2 3 4 5 6 7 8 9 10 11 12
| package top.zhengru.hmall.gateway;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
|
配置路由
在application.yml里配置网关的路由,id是路由规则id,可以自定义且是唯一的,uri是路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表,predicates是路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务,下面使用的是请求路径作为判断规则
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
| server: port: 8080 spring: application: name: gateway cloud: nacos: server-addr: 192.168.88.188:8848 gateway: routes: - id: item-service uri: lb://item-service predicates: - Path=/items/**,/search/** - id: user-service uri: lb://user-service predicates: - Path=/addresses/**,/users/** - id: cart-service uri: lb://cart-service predicates: - Path=/carts/** - id: trade uri: lb://trade-service predicates: - Path=/orders/** - id: pay uri: lb://pay-service predicates: - Path=/pay-orders/**
|
路由断言
SpringCloudGateway中支持的断言类型也就是predicates有很多,以下是断言规则的说明
| 名称 |
说明 |
示例 |
| After |
是某个时间点后的请求 |
- After=2037-01-20T17:42:47.789-07:00[America/Denver] |
| Before |
是某个时间点之前的请求 |
- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
| Between |
是某两个时间点之前的请求 |
- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
| Cookie |
请求必须包含某些cookie |
- Cookie=chocolate, ch.p |
| Header |
请求必须包含某些header |
- Header=X-Request-Id, \d+ |
| Host |
请求必须是访问某个host(域名) |
- Host=.somehost.org,.anotherhost.org |
| Method |
请求方式必须是指定方式 |
- Method=GET,POST |
| Path |
请求路径必须符合指定规则 |
- Path=/red/{segment},/blue/** |
| Query |
请求参数必须包含指定参数 |
- Query=name, Jack或者- Query=name |
| RemoteAddr |
请求者的ip必须是指定范围 |
- RemoteAddr=192.168.1.1/24 |
| weight |
权重处理 |
|
网关鉴权
实现思路
我们知道,网关是所有微服务的入口,一切请求都需要先经过网关。所以我们完全可以把登录校验的工作放到网关,并且只需要在网关和用户服务保存秘钥和开发登录校验的功能
网关工作原理
下面这张图是Gateway的内部工作原理,而我们需要在请求转发之前完成登录校验,这里值得注意的地方是只有所有Filter的pre逻辑都依次顺序执行通过后,请求才会被路由到微服务,并且微服务返回结果后,还会再倒序执行Filter的post逻辑。

从图中的过滤器链我们可以知道,请求转发是由过滤器链中最后一个过滤器NettyRoutingFilter实现的,而我们需要在此过滤器之前,完成登录校验的功能。
内置过滤器
Gateway存在内置的GatewayFilter过滤器。只要在yaml文件中简单配置即可,其作用范围就是配置在哪个Route下
例如,有一个过滤器叫做AddRequestHeaderGatewayFilterFacotry,顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。
1 2 3 4 5 6 7 8 9 10
| spring: cloud: gateway: routes: - id: test_route uri: lb://test-service predicates: -Path=/test/** filters: - AddRequestHeader=key, value
|
也可以使用default-filters来作用于所有的路由
1 2 3 4 5 6 7 8 9 10
| spring: cloud: gateway: default-filters: - AddRequestHeader=key, value routes: - id: test_route uri: lb://test-service predicates: -Path=/test/**
|
自定义过滤器
网关过滤器链中的过滤器有两种:
GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
GlobalFilter:全局过滤器,作用范围是所有路由,不可配置
自定义GatewayFilter
通过实现AbstractGatewayFilterFactory可以来实现最简单的过滤器,但是注意的是自定义过滤器的类名后缀必须是GatewayFilterFactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Component public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> { @Override public GatewayFilter apply(Object config) { return new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); System.out.println("过滤器执行了"); return chain.filter(exchange); } }; } }
|
然后需要在配置文件中添加这个过滤器
1 2 3 4 5
| spring: cloud: gateway: default-filters: - PrintAny
|
如果想要实现动态配置参数,可以像下面这样写,不过非常复杂
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 44 45
| @Component public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override public GatewayFilter apply(Config config) { return new OrderedGatewayFilter(new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String a = config.getA(); String b = config.getB(); String c = config.getC(); System.out.println("a = " + a); System.out.println("b = " + b); System.out.println("c = " + c); return chain.filter(exchange); } }, 100); }
@Data static class Config{ private String a; private String b; private String c; } @Override public List<String> shortcutFieldOrder() { return List.of("a", "b", "c"); } @Override public Class<Config> getConfigClass() { return Config.class; }
}
|
配置文件如下,这种配置方式参数必须严格按照shortcutFieldOrder()方法的返回参数名顺序来赋值
1 2 3 4 5
| spring: cloud: gateway: default-filters: - PrintAny=1,2,3
|
还有一种用法,无需按照这个顺序
1 2 3 4 5 6 7 8 9
| spring: cloud: gateway: default-filters: - name: PrintAny args: a: 1 b: 2 c: 3
|
自定义GlobalGatewayFilter
刚刚提到了,除了路由过滤器之外,还有一个全局过滤器也就是GlobalGatewayFilter,这个过滤器实现GlobalFilter即可,十分简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Component public class PrintAnyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("未登录,无法访问");
ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); }
@Override public int getOrder() { return 0; } }
|
登录校验
定义一个登录校验的过滤器叫做AuthGlobalFilter,并配置它在过滤器链中的顺序
注意,这里我们获取到用户信息后,会将它添加到请求头中,转发至微服务以便微服务能够获取用户信息
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| @Component @RequiredArgsConstructor @EnableConfigurationProperties(AuthProperties.class) public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtTool jwtTool;
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); if(isExclude(request.getPath().toString())){ return chain.filter(exchange); } String token = null; List<String> headers = request.getHeaders().get("authorization"); if (!CollUtils.isEmpty(headers)) { token = headers.get(0); } Long userId = null; try { userId = jwtTool.parseToken(token); } catch (UnauthorizedException e) { ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); }
String userInfo = userId.toString(); ServerWebExchange swe = exchange.mutate() .request(builder -> builder.header("user-info", userInfo)) .build(); return chain.filter(exchange); }
private boolean isExclude(String antPath) { for (String pathPattern : authProperties.getExcludePaths()) { if(antPathMatcher.match(pathPattern, antPath)){ return true; } } return false; }
@Override public int getOrder() { return 0; } }
|
拦截用户信息
通过上面的过滤器,我们实现了在网关的登录校验,并且把用户信息放在了请求头里,但是微服务还需要获取请求头里的用户信息
所以,我们需要编写一个拦截器,获取用户信息并保存到UserContext,然后再放行,因为所有的微服务都引入了common模块,所以我们把拦截器写在common模块里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class UserInfoInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String userInfo = request.getHeader("user-info"); if (StrUtil.isNotBlank(userInfo)) { UserContext.setUser(Long.valueOf(userInfo)); } return true; }
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserContext.removeUser(); } }
|
然后再写一个配置类生效这个拦截器,注意,想要这个配置类生效,就需要这个类被Spring扫描到,又由于这里是common包,扫不到这里的配置类,想要在不同的包下的配置类生效,还需要在resource/META-INF定义一个spring.factories(早期文件名,不过现在也能生效)文件去记录这个配置类
1 2 3 4 5 6 7 8
| @Configuration @ConditionalOnClass(DispatcherServlet.class) public class MvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInfoInterceptor()); } }
|
spring.factories内容如下
1 2 3 4
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.hmall.common.config.MyBatisConfig,\ com.hmall.common.config.MvcConfig,\ com.hmall.common.config.JsonConfig
|
这里还有一个比较有意思的点,按照这样配置之后,会发现报错
这里报错的原因是,因为网关微服务也引用了这个common模块,并且WebMvcConfigurer属于WebMvc包下的接口,但是网关的底层是基于WebFlux的非阻塞式响应式的编程,没有Mvc的包。而我们希望的是,这个配置类只在微服务里生效,但不在网关里生效,所以这里使用了SpringBoot自动装配的条件注解,@ConditionalOnClass就是判断一个类是否存在,那么就加上SpringMvc的核心API:DispatcherServlet作为条件,加上@ConditionalOnClass(DispatcherServlet.class)注解,问题就解决了。
OpenFeign传递用户
我们知道可以将用户信息送至第一个微服务,但如果微服务之间存在远程调用的情况,那么这个过程中用户信息就会丢失,因为远程调用是一次新的请求,但这个请求头不会携带用户信息,就会导致被调用的那个微服务获取不到用户信息。想要解决这个问题,必须在微服务发起调用时把用户信息存入请求头。
这里我们借助Feign中提供的一个拦截器接口:feign.RequestInterceptor,并且实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。
在api模块的DefaultFeignConfig里添加一个Bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class DefaultFeignConfig { @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; }
@Bean public RequestInterceptor userInfoRequestInterceptor() { return new RequestInterceptor() { @Override public void apply(RequestTemplate requestTemplate) { Long userId = UserContext.getUser(); if (userId != null) { requestTemplate.header("user-info", userId.toString()); } } }; } }
|
这样就解决了微服务之间调用没有传递用户信息的问题
配置管理
在我们的项目中,网关路由和某些业务配置在配置文件中写死了,甚至还有重复,那么导致维护成本高,并且每次修改都需要重启微服务,这个问题可以通过统一的配置管理器服务解决。而Nacos不仅仅具备注册中心功能,也具备配置管理的功能:

微服务共享的配置可以统一交给Nacos保存和管理,在Nacos控制台修改配置后,Nacos会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新。
添加配置
在nacos控制台配置管理->配置列表中点击+可以新建一个配置
比如下面的JDBC配置信息

拉取共享配置
下面是SpringCloud项目初始化的流程图
这里注意:读取Nacos配置是SpringCloud上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml。也就是说引导阶段,application.yaml文件尚未读取
让SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件,如果我们将nacos地址配置到bootstrap.yaml中,那么在项目引导阶段就可以读取nacos中的配置了。

引入依赖
在微服务模块中引入Nacos配置管理和读取bootstrap文件的依赖
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
|
在resource目录下新建bootstrap.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| spring: application: name: cart-service profiles: active: dev cloud: nacos: server-addr: 192.168.88.188 config: file-extension: yaml shared-configs: - dataId: shared-jdbc.yaml - dataId: shared-log.yaml - dataId: shared-swagger.yaml
|
那么application.xml里的配置就可以简化为
1 2 3 4 5 6 7 8 9 10 11
| server: port: 8082 feign: httpclient: enabled: true hm: db: database: hm-cart swagger: title: "黑马商城购物车服务文档" package: top.zhengru.hmall.cart.controller
|
配置热更新
我们需要在nacos新建一个配置,并且这个配置的名字必须要符合下面的规范
文件名称由三部分组成:
服务名:我们是购物车服务,所以是cart-service
spring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置
后缀名:例如yaml
1
| [服务名]-[spring.active.profile].[后缀名]
|
以cart-service为例,则不管是dev还是local环境都可以共享该配置

而读取配置,实现热更新就是在属性读取类
1 2 3 4 5 6
| @Data @Component @ConfigurationProperties(prefix = "hm.cart") public class CartProperties { private Integer maxAmount; }
|
又或者是
1 2 3 4 5 6 7
| @Data @Component @RefreshScope public class CartProperties { @Value("${hm.cart.maxItems}") private Integer maxAmount; }
|
这样就可以实现从nacos热更新配置了
微服务保护
保证服务运行的健壮性,避免级联失败导致的雪崩问题,就属于微服务保护
保护方案
请求限流
并发太高就是服务故障最重要的原因,而请求限流可以限制、控制接口访问的并发流量,避免服务因流量过高而出现故障。
线程隔离
在高并发情况下,服务器需要同时处理大量的请求。每个请求可能需要创建一个线程或者分配一个线程来进行处理。当并发请求量非常大时,如果每个请求都需要一个独立的线程,那么就可能导致服务器的线程资源耗尽。
为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,比如限定某个业务的线程数量上限为10,这样就不会导致服务器的线程资源被耗尽,且不会影响到其它接口。

服务熔断
服务熔断是一种用于防止故障蔓延的设计模式,它通过监控服务调用的状态,并在服务出现异常或故障时采取措施来保护系统的稳定性。
- 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
- 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。
Sentinel
阿里巴巴开源的一款服务保护框架,目前已经加入SpringCloudAlibaba中
安装部署
这里我采用的Docker安装Sentinel
1 2 3
| docker pull bladex/sentinel-dashboard:1.8.6
docker run --name sentinel -d -p 8858:8858 -p 8719:8719 -d bladex/sentinel-dashboard:1.8.6 -e username=sentinel -e password=sentinel -e server=localhost:8858
|
然后访问localhost:8858,就可以看到sentinel的控制台了,这里默认的账号密码都是sentinel

整合微服务
首先我们在pom.xml里添加sentinel的依赖
1 2 3 4 5
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
|
接着在application.yml里添加sentinel的配置
1 2 3 4 5
| spring: cloud: sentinel: transport: dashboard: localhost:8858
|
重启微服务,注意了,此时登录 dashboard web 管理端,是看不到接入的服务的,因为 sentinel 默认是懒加载支持,需要随便访问一个接口,然后才会出来应用信息,也可以使用sentinel.eager=true 配置取消默认的懒加载
访问完接口以后,就可以在控制台看到统计信息,点击簇点链路可以看到下面的页面

簇点链路,就是单机调用链路,是一次请求进入服务后经过的每一个被Sentinel监控的资源。默认情况下,Sentinel会监控SpringMVC的每一个Endpoint(接口)。这里/carts这个接口路径就是其中一个簇点,我们可以对其进行限流、熔断、隔离等保护措施。
而默认情况下Sentinel会把路径作为簇点资源的名称,无法区分路径相同但请求方式不同的接口,查询、删除、修改等都被识别为一个簇点资源
在application.yml中添加下面的配置,就可以把请求方式 + 请求路径作为簇点资源名
1 2 3 4 5 6
| spring: cloud: sentinel: transport: dashboard: 127.0.0.1:8858 http-method-specify: true
|
请求限流
在簇点链路后面点击流控按钮,可以对其做限流配置,这里相当于把查询购物车这个簇点资源限制在了每秒6个,会发现QPS稳定在6附近

线程隔离
在application.yml中添加OpenFeign整合Sentinel的配置
1 2 3
| feign: sentinel: enabled: true
|
需要注意的是,默认情况下SpringBoot项目的tomcat最大线程数是200,允许的最大连接是8492,单机测试很难打满。
可以继续添加tomcat连接的配置便于我们测试
1 2 3 4 5 6 7
| server: port: 8082 tomcat: threads: max: 50 accept-count: 50 max-connections: 100
|
然后点击簇点资源后面的流控按钮,阈值类型点击并发线程数,就可以限制它使用的线程资源,起到线程隔离的作用,即使这个接口的并发非常高,也不会影响到其他接口
服务熔断
之前的限流、线程隔离保护了接口,导致了因为并发过高查询失败的请求只能抛出异常,但实际的做法应该是给查询失败设置一个降级处理逻辑,并且如果某一个接口的延迟较高,响应时间非常长,对于这些不太健康的接口,我们应该停止调用,走降级逻辑从而避免影响到当前的服务,也就是接口的熔断。
编写降级逻辑
给FeignClient编写失败后的降级逻辑有两种方式:
FallbackClass,无法对远程调用的异常做处理
FallbackFactory,可以对远程调用的异常做处理,我们一般选择这种方式。
在api模块创建ItemClientFallback
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Slf4j public class ItemClientFallback implements FallbackFactory<ItemClient> {
@Override public ItemClient create(Throwable cause) { return new ItemClient() { @Override public List<ItemDTO> queryItemByIds(Collection<Long> ids) { log.error("远程调用ItemClient#queryItemByIds方法出现异常,参数:{}", ids, cause); return CollUtils.emptyList(); }
@Override public void deductStock(List<OrderDetailDTO> items) { throw new BizIllegalException(cause); } }; } }
|
在Config类中注册为一个Bean
1 2 3 4
| @Bean public ItemClientFallback itemClientFallback() { return new ItemClientFallback(); }
|
还需要在ItemClient接口中进行使用
1 2 3 4 5 6
| @FeignClient(value = "item-service", configuration = DefaultFeignConfig.class, fallbackFactory = ItemClientFallback.class) public interface ItemClient { }
|
用JMeter会发现此时异常比例为0,即使限流也不再报错
服务熔断
Sentinel中的断路器不仅可以统计某个接口的慢请求比例,还可以统计异常请求比例。当这些比例超出阈值时,就会熔断该接口,即拦截访问该接口的一切请求,降级处理;当该接口恢复正常时,再放行对于该接口的请求。

我们可以点击簇点后面的熔断按钮来配置熔断策略,在弹出的表格中可以配置慢调用比例的阈值、最大RT,熔断时长等等
分布式事务