Spring cloud Alibaba 组件Nacos、Ribbon(Feign)、Sentinel
作者:互联网
Spring cloud Alibaba
一. 为什么使用spring cloud alibaba
很多人可能会问,有了spring cloud这个微服务的框架,为什么又要使用spring cloud alibaba这个框架了?最重要的原因在于spring cloud中的几乎所有的组件都使用Netflix公司的产品,然后在其基础上做了一层封装。然而Netflix的服务发现组件Eureka已经停止更新,在使用时会有一些小的Bug;而其他的众多组件预计会在2020年停止维护。所以急需其他的一些替代产品,也就是spring cloud alibaba,目前正处于蓬勃发展的态式。
二. 注册中心Nacos
2.1 介绍nacos
nacos是阿里巴巴研发的一个集注册中心与配置中心于一体的管理平台,使用其他非常的简单
文档地址:https://nacos.io/zh-cn/docs/what-is-nacos.html
下载地址为:https://github.com/alibaba/nacos/releases
nacos架构图
2.2 启动nacos
1.解压下载的nacos-server-1.4.2.zip文件
2.修改D:\nacos-server-1.4.2\nacos\bin目录下的startup.cmd文件,改为独立模式运行,修改内容如下:
set MODE="cluster" #默认配置是集群模式
set MODE="standalone" #修改后的内容
然后双击startup.cmd 启动,访问:http://localhost:8848/nacos/#/login
默认的登录名和密码是:nacos/nacos
nacos的主页如下图所示:
2.3 nacos对比
三. 入门案例
因为最新版本中的某些组件已经不更新了,我们本次案例使用的版本为:
Spring Boot Version: 2.3.2.RELEASE
Spring Cloud Version:Spring Cloud Hoxton.SR9
Spring Cloud Alibaba Version:2.2.6.RELEASE
3.1 创建maven父工程 springcloudalibaba-micro-service-manager
1.导入依赖
<!-- 打包方式POM -->
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Spring Cloud Hoxton.SR9</version>
</dependency>
</dependencies>
</dependencyManagement>
3.2 创建子工程 springcloudalibaba-micro-service-commons
1.导入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
2.创建User类
package com.qf.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private Integer id;
private String name;
private String password;
}
3.创建JsonResult工具类
package com.qf.utils;
import lombok.Data;
@Data
public class JsonResult<T> {
private Integer code;
private String msg;
private T data;
public static JsonResult ok(){
JsonResult jsonResult = new JsonResult();
jsonResult.setCode(200);
jsonResult.setMsg("success");
return jsonResult;
}
public static JsonResult error(){
JsonResult jsonResult = new JsonResult();
jsonResult.setCode(-20000);
jsonResult.setMsg("fail");
return jsonResult;
}
}
3.3 创建子工程 springcloudalibaba-micro-service-provider-7070
1.导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 注册到Nacos上 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 公共模块 -->
<dependency>
<groupId>com.qf</groupId>
<artifactId>springcloudalibaba-micro-service-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
2.创建 application.yml
server:
port: 7070
spring:
application:
name: micro-service-provider #服务提供方名称
cloud:
nacos:
discovery:
enabled: true #服务注册
server-addr: 127.0.0.1:8848 #服务注册地址
3.创建启动类
package com.qf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient//发现服务
public class MicroServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(MicroServiceProviderApplication.class,args);
}
}
4.创建UserController
package com.qf.controller;
import com.qf.pojo.User;
import com.qf.utils.JsonResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("user-provider")
public class UserController {
@RequestMapping("findAll")
public JsonResult findAll(){
List<User> users = Arrays.asList(
new User(1001, "张三", "123"),
new User(1002, "李四", "456"),
new User(1003, "王五", "789"));
JsonResult jsonResult = JsonResult.ok();
jsonResult.setData(users);
return jsonResult;
}
}
3.4 创建子工程 springcloudalibaba-micro-service-consumer-8080
1.添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 注册到Nacos上 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 公共模块 -->
<dependency>
<groupId>com.qf</groupId>
<artifactId>springcloudalibaba-micro-service-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
2.创建 application.yml
server:
port: 8080
spring:
application:
name: micro-service-consumer #服务消费方
cloud:
nacos:
discovery:
enabled: true #服务注册
server-addr: 127.0.0.1:8848 #服务注册地址
3.创建启动类
package com.qf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient//发现服务
public class MicroServiceConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(MicroServiceConsumerApplication.class,args);
}
}
4.创建 WebConfig,获取RestTemplate对象
package com.qf.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class WebConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
5.创建 UserController
package com.qf.controller;
import com.qf.pojo.User;
import com.qf.utils.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
@RequestMapping("user-consumer")
public class UserController {
@Autowired
private DiscoveryClient discoveryClient;//服务发现
@Autowired
private RestTemplate restTemplate;//用于发送网络请求
//消费方应该调用生产方的服务
@RequestMapping("getUsers")
public JsonResult getUsers(){
//需要获取生产方的IP地址,端口号,接口名
//通过服务名称获取服务对象
List<ServiceInstance> serviceInstances =
discoveryClient.getInstances("micro-service-provider");
//获取当前这个服务对象
ServiceInstance serviceInstance = serviceInstances.get(0);
//获取IP地址
String host = serviceInstance.getHost();
//获取端口号
int port = serviceInstance.getPort();
//编写要调用的服务地址
String url = "http://"+host+":"+port+"/user-provider/findAll";
//发送网络请求
JsonResult jsonResult = restTemplate.getForObject(url, JsonResult.class);
return jsonResult;
}
}
分别启动服务方以及消费方,然后访问:http://localhost:8080/user-consumer/getUsers 进行测试
四. Ribbon负载均衡
4.1 Ribbon介绍
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
4.2 负载均衡(Load Balance)的作用
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的高可用。
4.3 Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
4.4 实现负载均衡
Ribbon只是一个客户端的负载均衡器工具,实现起来非常的简单,我们只需要在注入RestTemplate的bean上加上@LoadBalanced就可以了,内容如下:
package com.qf.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class WebConfig {
@LoadBalanced//负载均衡,默认使用轮询规则
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
在早期版本中,spring-cloud-starter-netflix-eureka-client依赖已经引入了Ribbon,则我们可以直接使用,但因为自从SpringCloud2020.0.1.0版本时就已经不使用netflix了,所以如果我们使用的是最新版本的springcloud,则需要手动在服务消费方导入spring-cloud-starter-loadbalancer依赖支持
org.springframework.cloud spring-cloud-starter-loadbalancer 3.1.1 这里我们就不用再导入该依赖了!
4.5 启动一个服务消费方,多个服务提供方进行测试
1.修改springcloud-alibaba-microservice-consumer-8080工程中的UserController,然后启动即可
package com.qf.controller;
import com.qf.pojo.User;
import com.qf.utils.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
@RequestMapping("user-consumer")
public class UserController {
@Autowired
private DiscoveryClient discoveryClient;//服务发现
@Autowired
private RestTemplate restTemplate;//用于发送网络请求
//消费方应该调用生产方的服务
@RequestMapping("getUsers")
public JsonResult getUsers(){
//由于使用了@LoadBalanced,这里需要通过服务的名称来发送网络请求
String url = "http://micro-service-provider/user-provider/findAll";
JsonResult jsonResult = restTemplate.getForObject(url, JsonResult.class);
System.out.println(jsonResult);
return jsonResult;
}
//消费方应该调用生产方的服务
// @RequestMapping("getUsers")
// public JsonResult getUsers(){
// //需要获取生产方的IP地址,端口号,接口名
// //通过服务名称获取服务对象
// List<ServiceInstance> serviceInstances =
// discoveryClient.getInstances("micro-service-provider");
//
// //获取当前这个服务对象
// ServiceInstance serviceInstance = serviceInstances.get(0);
//
// //获取IP地址
// String host = serviceInstance.getHost();
//
// //获取端口号
// int port = serviceInstance.getPort();
//
// //编写要调用的服务地址
// String url = "http://"+host+":"+port+"/user-provider/findAll";
//
// //发送网络请求
// JsonResult jsonResult = restTemplate.getForObject(url, JsonResult.class);
//
// return jsonResult;
// }
}
2.修改springcloud-alibaba-microservice-provider-7070工程中的UserController
package com.qf.controller;
import com.qf.pojo.User;
import com.qf.utils.JsonResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("user-provider")
public class UserController {
@RequestMapping("findAll")
public JsonResult findAll(){
//使用并联启动的方式,启动多个服务提供方进行测试
//先输出7070,然后修改application.yml配置文件,端口设置为7070启动
//System.out.println("7070");
//再输出7071,然后修改application.yml配置文件,端口设置为7071启动
System.out.println("7071");
//再输出7072,然后修改application.yml配置文件,端口设置为7072启动
//System.out.println("7072");
List<User> users = Arrays.asList(
new User(1001, "张三", "123"),
new User(1002, "李四", "456"),
new User(1003, "王五", "789"));
JsonResult jsonResult = JsonResult.ok();
jsonResult.setData(users);
return jsonResult;
}
}
3.设置springcloud-alibaba-microservice-provider-7070工程多次启动
4.修改端口号,启动多个provider,然后启动consumer,访问浏览器进行测试
4.6 负载均衡策略
Ribbon提供了一个很重要的接口叫做IRule,其中定义了很多的负载均衡策略,默认的是轮询的方式,以下是Ribbon的负载均衡策略
类名 | 描述 |
---|---|
RoundRobbinRule | 轮询 |
RandomRule | 随机挑选 |
RetryRule | 按照轮询的方式去调用服务,如果其中某个服务不可用,但是还是会尝试几次,如果尝试过几次都没有成功,那么就不在调用该服务,会轮询调用其他的可用服务。 |
AvailabilityFilteringRule | 会先过滤掉因为多次访问不可达和并发超过阈值的服务,然后轮询调用其他的服务 |
WeightedResponseTimeRule | 根据平均响应时间计算权重,响应越快权重越大,越容易被选中。服务刚重启的时候,还未统计出权重会按照轮询的方式;当统计信息足够的时候,就会按照权重信息访问 |
ZoneAvoidanceRule | 判断server所在的区域性能和可用性选择服务器 |
BestAvailableRule | 会过滤掉多次访问都不可达的服务,然后选择并发量最小的服务进行调用,默认方式 |
改变Ribbon的负载均衡策略:
package com.qf.config;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class WebConfig {
@LoadBalanced//负载均衡
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
//创建对象实现改变Ribbon的负载均衡策略,随机规则
@Bean
public IRule getRule() {
return new RandomRule();
}
}
4.7 自定义负载均衡策略(了解)
我们自定义的负载均衡策略需要继承AbstractLoadBalancerRule这个类,然后重写choose方法,然后将其注入到容器中。
1.创建ServerInfo类
package com.qf.rule;
import com.netflix.loadbalancer.Server;
public class ServerInfo {
private Server server;
private int num;
public ServerInfo() {
}
public ServerInfo(Server server, int num) {
this.server = server;
this.num = num;
}
public Server getServer() {
return server;
}
public void setServer(Server server) {
this.server = server;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
2.创建CustomizeRule类
package com.qf.rule;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
//自定义规则,每个服务最多访问5次后,然后再继续访问下一个
public class CustomizeRule extends AbstractLoadBalancerRule {
private int limit = 3;
/**
* map的key是服务的名字,value是该服务调用的次数
*/
private Map<String, ServerInfo> map = new ConcurrentHashMap<>();
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) { }
/**
* 返回值的意思是,当该方法返回什么的时候,那么Ribbon或者Feign就调用谁。
*/
@Override
public Server choose(Object key) {
Server finalServer = null;
ILoadBalancer loadBalancer = getLoadBalancer();
// 获取所有的服务
List<Server> servers = loadBalancer.getAllServers();
// 获取所有的可用的服务
List<Server> reachableServers = loadBalancer.getReachableServers();
int allServiceSize = servers.size(); //获取所有的服务的长度
int upCount = reachableServers.size(); //获取所有的可用的服务的长度
if(0 == allServiceSize || 0 == upCount) {
return null;
}
for(int i = 0; i < allServiceSize; i++) {
Server server = servers.get(i); // 获取当前遍历的server
String instanceId = server.getMetaInfo().getInstanceId();
String providerName = instanceId.split("@@")[1]; //获取服务名
ServerInfo serverInfo = map.get(providerName); //获取对应服务
// 首次调用
if(null == serverInfo) {
serverInfo = new ServerInfo(server, 1);
map.put(providerName, serverInfo);
finalServer = server;
break;
} else { //不为空,表示之前肯定调用过
// 当前遍历的server与正在调用的server是同一个server
if(serverInfo.getServer().getId().equals(server.getId())) {
/**
* 1. 如果没有满5次,接着走该服务。
* 2. 如果满了5次,接着下个
*/
int num = serverInfo.getNum(); //获取已经调用的次数
if(num >= limit) { //超出了5次
// 超出了次数,要走下一个,需要判断是否有下一个,如果没有下一个,就回到第一个
if(i == (allServiceSize - 1)) { //表示当前服务为整个集群的最后一个服务,拿第一个
Server firstServer = servers.get(0); //如果为最后一个就拿第一个
ServerInfo firstServerInfo = new ServerInfo(firstServer, 1);
map.put(providerName, firstServerInfo);
finalServer = firstServer;
}else { //不是最后一个
Server nextServer = servers.get(i + 1); //取下一个
ServerInfo nextServerInfo = new ServerInfo(nextServer, 1);
map.put(providerName, nextServerInfo);
finalServer = nextServer;
}
break;
}else {
serverInfo.setNum(++num);
finalServer = server;
break;
}
}
}
}
return finalServer;
}
}
3.修改WebConfig,添加配置
package com.qf.config;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import com.qf.rule.CustomizeRule;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class WebConfig {
@Bean
@LoadBalanced//均衡负载,默认规则:轮询
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
//创建对象实现改变Ribbon的负载均衡策略,随机规则
// @Bean
// public IRule getRule() {
// return new RandomRule();
// }
//自定义均衡负载服务器
@Bean
public IRule getRule() {
return new CustomizeRule();
}
}
五. Feign负载均衡
5.1 Feign介绍
Feign 和 Ribbon 是 Spring Cloud 的 Netflix 中提供的两个实现软负载均衡的组件,Ribbon 和 Feign 都是用于调用其他服务的,方式不同,Feign 则是在 Ribbon 的基础上进行了一次改进,采用接口的方式,将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建 http 请求,不过要注意的是抽象方法的注解、方法名要和提供服务的方法对应上
5.2 Feign和Ribbon 区别
1.启动类使用的注解不同,Ribbon 用的是@RibbonClient,Feign 用的是@EnableFeignClients。
2.服务的指定位置不同,Ribbon 是在@RibbonClient 注解上声明,Feign 则是在定义抽象方法的接口中使用@FeignClient 声明。
3.调用方式不同,Ribbon 需要自己构建 http 请求,模拟 http 请求然后使用 RestTemplate 发送给其他服务,步骤相当繁琐,Feign 是直接通过接口方式调用。
5.3 Feign使用
1.在springcloudalibaba-micro-service-consumer-8080的pom.xml中添加依赖
<!-- Feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
2.在启动类上加上@EnableFeignClients这个注解
package com.qf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class MicroServiceConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(MicroServiceConsumerApplication.class,args);
}
}
3.创建UserService
package com.qf.service;
import com.qf.utils.JsonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
@Service
@FeignClient("micro-service-provider")
public interface UserService {
@RequestMapping("/user-provider/findAll")
public JsonResult findAll();
}
4.创建FeignUserController
package com.qf.controller;
import com.qf.pojo.User;
import com.qf.service.UserService;
import com.qf.utils.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("feign")
public class FeignUserController {
@Autowired
private UserService userService;
@RequestMapping("findAll")
public JsonResult findAll(){
return userService.findAll();
}
}
5.启动多个provider,然后启动consumer,访问:http://localhost:8080/feign/findAll 进行测试
5.4服务之间的参数传递
1.在springcloudalibaba-micro-service-provider-7070工程中的UserController添加CRUD方法
package com.qf.controller;
import com.qf.pojo.User;
import com.qf.utils.JsonResult;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("user-provider")
public class UserController {
@RequestMapping("findAll")
public JsonResult findAll(){
//使用并联启动的方式,启动多个服务提供方进行测试
//先输出7070,然后修改application.yml配置文件,端口设置为7070启动
//System.out.println("7070");
//再输出7071,然后修改application.yml配置文件,端口设置为7071启动
System.out.println("7071");
//再输出7072,然后修改application.yml配置文件,端口设置为7072启动
//System.out.println("7072");
List<User> users = Arrays.asList(
new User(1001, "张三", "123"),
new User(1002, "李四", "456"),
new User(1003, "王五", "789"));
JsonResult jsonResult = JsonResult.ok();
jsonResult.setData(users);
return jsonResult;
}
//模拟数据库操作
//查询单个
@GetMapping("findById")
public JsonResult findById(@RequestParam("id") Integer id){
User user = new User(id, "jack", "123");
JsonResult jsonResult = JsonResult.ok();
jsonResult.setData(user);
return jsonResult;
}
//删除单个
@DeleteMapping("deleteById/{id}")
public JsonResult deleteById(@PathVariable("id") Integer id){
System.out.println("deleteById:"+id);
return JsonResult.ok();
}
//添加
@PostMapping("addUser")
public JsonResult addUser(@RequestBody User user){
System.out.println("addUser:"+user);
return JsonResult.ok();
}
//修改
@PutMapping("updateUser")
public JsonResult updateUser(@RequestParam Integer id,@RequestParam String username,@RequestParam String password){
System.out.println("updateUser:"+id+"--"+username+"--"+password);
return JsonResult.ok();
}
}
2.在springcloudalibaba-micro-service-consumer-8080工程中的UserService添加对应方法
package com.qf.service;
import com.qf.pojo.User;
import com.qf.utils.JsonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
@Service
@FeignClient("micro-service-provider")
public interface UserService {
@RequestMapping("/user-provider/findAll")
public JsonResult findAll();
//模拟数据库操作
//查询单个
@GetMapping("/user-provider/findById")
public JsonResult findById(@RequestParam("id") Integer id);
//删除单个
@DeleteMapping("/user-provider/deleteById/{id}")
public JsonResult deleteById(@PathVariable("id") Integer id);
//添加
@PostMapping("/user-provider/addUser")
public JsonResult addUser(@RequestBody User user);
//修改
@PutMapping("/user-provider/updateUser")
public JsonResult updateUser(@RequestParam Integer id,@RequestParam String username,@RequestParam String password);
}
3.在springcloudalibaba-micro-service-consumer-8080工程中的FeignUserController添加对应方法
package com.qf.controller;
import com.qf.pojo.User;
import com.qf.service.UserService;
import com.qf.utils.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("feign")
public class FeignUserController {
@Autowired
private UserService userService;
@RequestMapping("findAll")
public JsonResult findAll(){
return userService.findAll();
}
//模拟数据库操作
//查询单个
@GetMapping("findById")
public JsonResult findById(@RequestParam("id") Integer id){
return userService.findById(id);
}
//删除单个
@DeleteMapping("deleteById/{id}")
public JsonResult deleteById(@PathVariable("id") Integer id){
return userService.deleteById(id);
}
//添加
@PostMapping("addUser")
public JsonResult addUser(User user){
return userService.addUser(user);
}
//修改
@PutMapping("updateUser")
public JsonResult updateUser(@RequestParam Integer id,@RequestParam String username,@RequestParam String password){
return userService.updateUser(id,username,password);
}
}
4.在postman中选择对应的请求方式进行测试
六. 熔断与服务降级
分布式系统中一个微服务需要依赖于很多的其他的服务,那么服务就会不可避免的失败。例如A服务依赖于B、C、D等很多的服务,当B服务不可用的时候,会一直阻塞或者异常,更不会去调用C服务和D服务。同时假设有其他的服务也依赖于B服务,也会碰到同样的问题,这就及有可能导致雪崩效应。
如下案例:一个用户通过通过web容器访问应用,他要先后调用A、H、I、P四个模块,一切看着都很美好。
由于某些原因,导致I服务不可用,与此同时我们没有快速处理,会导致该用户一直处于阻塞状态。
当其他用户做同样的请求,也会面临着同样的问题,tomcat支持的最大并发数是有限的,资源都是有限的,将整个服务器拖垮都是有可能的。
Sentinel是一个用于分布式系统的延迟和容错的开源库,在分布式系统中,许多依赖会不可避免的调用失败,例如超时,异常等,Sentinel能保证在一个依赖出现问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
断路器本身是一种开关装置,当某个服务单元发生故障后,通过断路器的故障监控(类似于保险丝),向调用者返回符合预期的,可处理的备选响应,而不是长时间的等待或者抛出无法处理的异常,这样就保证了服务调用的线程不会被长时间,不必要的占用,从而避免故障在分布式系统中的蔓延,乃至雪崩。
Sentinel在网络依赖服务出现高延迟或者失败时,为系统提供保护和控制;可以进行快速失败,缩短延迟等待时间;提供失败回退(Fallback)和相对优雅的服务降级机制;提供有效的服务容错监控、报警和运维控制手段。
6.1 依赖
在springcloud-alibaba-microservice-consumer-8080和springcloud-alibaba-microservice-provider-7070工程的pom.xml文件导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
在D:\sentinel-dashboard目录使用cmd打开,输入 java -jar sentinel-dashboard-1.8.2.jar --server.port=8081 命令启动
6.2 配置
在springcloudalibaba-micro-service-consumer-8080和springcloudalibaba-micro-service-provider-7070工程的application.yml 中添加 sentinel 配置
server:
port: 8080
spring:
application:
name: micro-service-consumer #服务消费方
cloud:
nacos:
discovery:
enabled: true
server-addr: 127.0.0.1:8848
sentinel:
transport:
port: 8719 #推送数据端口
dashboard: 127.0.0.1:8081 #启动端口
web-context-unify: false #false表示针对调用同一接口的不同url进行链路限制
启动服务提供方以及消费方并调用服务(多调用几次),然后查看sentinel的控制面板
6.3 sentinel的控制面板
在浏览器输入localhost:8081,用户名和密码都是 sentinel,然后访问sentinel的控制面板
接下来对sentinel的控制面板一一讲解:
6.3.1 实时监控
集成控制台后,当有请求时,实时监控页面会显示当前服务各个接口的访问信息,以图表的形式展示给用户,包含访问时间、通过 QPS、拒绝QPS、响应时间(ms)等信息。
6.3.2 簇点链路
列表:用于展示服务所有接口,包含通过QPS、拒绝QPS、线程数、平均RT、分钟通过、分钟拒绝等信息,端口为服务与Sentinel控制台交互的端口,服务本地会起一个该端口占用的HttpServer,该 Server 会与 Sentinel 控制台做交互。
比如: Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。
操作:可以点击,然后对当前资源添加流控、降级、热点、授权等相关操作
6.3.3 流控规则
流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
资源名:唯一名称,默认是请求路径,可自定义
针对来源:流控针对的调用来源,若为 default 则不区分调用来源
阈值类型:
QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流
线程数:当调用该接口的线程数达到阈值的时候,进行限流
单机阈值:QPS或线程数的阈值数,达到阈值则限流
是否集群:是否开启集群流控,不选择则默认为单机模式
流控模式:
直接(默认):接口达到限流条件时,开启限流
关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]
链路:当从某个接口过来的资源达到限流条件时,开启限流
流控效果:
快速失败:该方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。
Warm Up(冷启动):该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。
排队等待:这种方式严格控制了请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
6.3.4 降级规则
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一
资源名: 要实现降级的资源。
慢调用比例:
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
调用:一个请求发送到服务器,服务器给与响应,一个响应就是一个调用
慢调用:当调用的时间(响应的实际时间)>设置的RT的时,这个调用叫做慢调用
慢调用比例:在所以调用中,慢调用占有实际的比例,= 慢调用次数 / 调用次数
RT:响应时间,指系统对请求作出响应的时间。
比例阈值:RT模式下慢速请求比率的阈值。默认1.0d,自己设定, 慢调用次数 / 调用次数=比例阈值
熔断时长:断路器打开时的恢复超时(以秒为单位)。超时后,断路器将转换为半开状态以尝试一些请求,单位秒,图中表示触发熔断后,接下来的10秒内请求会自动被熔断,经过10S后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
最小请求数:可以触发熔断中断的最小请求数(在有效的统计时间范围内)。默认值为5,设置的调用最小请求数
统计时长: (单位为 ms),如 60*1000 代表分钟级(1.8.0 引入),默认1000
异常比例:
当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
异常数:
当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
6.3.5 代码实现
1.使用@SentinelResource注解完成对流控以及降级规则的实现
2.在springcloudalibaba-micro-consumer-8080工程中的FeignUserController中添加对应的方法进行测试
/**
1. 如果没有用blockHandler,fallback无论是违反了什么规则,都走fallback
2. blockHandler和 fallback 如果都配置了,优先走blockHandler,可以区别限流、降级、热点等。
3. blockHandler是将服务降级的方法与目标方法在同一个类中。如果不同的类中,有些方法需要使用相同的服务降级, 就可以使用blockHandlerClass来定义个一类,然后通过blockHandler来指定对应的降级的方法。方法必须是静态。
4. fallback可以处理非 sentinel 的异常。
*/
//blockHandler对应方法的名称,该方法返回值要和调用的方法一致,并且在同一类中
//@SentinelResource(value = "yangl-findUsers",blockHandler = "findUsersBlockHandler")
@SentinelResource(value = "yangl-findUsers",blockHandler = "findUsersBlockHandler",blockHandlerClass = MyBlockHandler.class)
//@SentinelResource(value = "yangl-findUsers",fallback = "findUsersfallback",fallbackClass = Myfallback.class)
@RequestMapping("findUsers")
public JsonResult findUsers(){
//int i = 1/0;
return userService.findAll();
}
// public JsonResult findUsersBlockHandler(BlockException e){
//
// JsonResult jsonResult = JsonResult.error();
//
// if (e instanceof FlowException){
// jsonResult.setMsg("流控异常");
// }else if (e instanceof DegradeException){
// jsonResult.setMsg("降级异常");
// }else{
// jsonResult.setMsg("其他sentinel异常");
// }
// return jsonResult;
// }
3.除了在本类中编写blockHandler对应的方法外,也可以在自己单独创建的类中编写
package com.qf.sentinel;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.qf.utils.JsonResult;
public class MyBlockHandler {
public static JsonResult findUsersBlockHandler(BlockException e){
JsonResult jsonResult = JsonResult.error();
if (e instanceof FlowException){
jsonResult.setMsg("流控异常");
}else if (e instanceof DegradeException){
jsonResult.setMsg("降级异常");
}else if (e instanceof ParamFlowException){
jsonResult.setMsg("热点异常");
}else{
jsonResult.setMsg("其他sentinel异常");
}
return jsonResult;
}
}
4.除了在本类中编写fallback对应的方法外,也可以在自己单独创建的类中编写
package com.qf.sentinel;
import com.qf.utils.JsonResult;
public class Myfallback {
public static JsonResult findUsersfallback(Throwable throwable){
JsonResult jsonResult = JsonResult.error();
if(throwable instanceof FlowException){
jsonResult.setData("流控异常");
}else if(throwable instanceof DegradeException){
jsonResult.setData("降级异常");
}else{
jsonResult.setData(throwable.getMessage());
}
return jsonResult;
}
}
5.新增流控或降级规则进行测试,在@SentinelResource的value对应的值的那一栏点击流控或降级进行配置
6.访问:http://localhost:8080/feign/findUsers 测试即可
6.3.6 热点规则
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
代码实现:
1.在UserFeignController中添加该方法
//该方法只是用于测试热点规则
@SentinelResource(value = "yangl-getById",blockHandler = "getByIdBlockHandler",blockHandlerClass = MyBlockHandler.class)
@GetMapping("getById")
public JsonResult getById(@RequestParam("uid") Integer uid){
return userService.findById(uid);
}
2.在MyBlockHandler中添加对应的方法(返回值,参数列表都一致)
package com.qf.sentinel;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.qf.utils.JsonResult;
public class MyBlockHandler {
public static JsonResult findUsersBlockHandler(BlockException e){
JsonResult jsonResult = JsonResult.error();
if (e instanceof FlowException){
jsonResult.setMsg("流控异常");
}else if (e instanceof DegradeException){
jsonResult.setMsg("降级异常");
}else if (e instanceof ParamFlowException){
jsonResult.setMsg("热点异常");
}else{
jsonResult.setMsg("sentinel异常");
}
return jsonResult;
}
//处理的方法要和调用方法的返回值以及参数一致
public static JsonResult getByIdBlockHandler(Integer uid,BlockException e){
JsonResult jsonResult = JsonResult.error();
if (e instanceof ParamFlowException){
jsonResult.setMsg("热点异常");
}else if( e instanceof AuthorityException){
jsonResult.setMsg("来源不明");
}
return jsonResult;
}
}
3.添加热点规则,设置单机阈值(表示该方法访问限制),设置对应的索引(0表示对访问方法的第一个参数的限制),点击新增
4.添加成功后,点击编辑热点规则,此时可以显示高级选项,可以对索引设置值以及限流阈值进行测试
5.访问:http://localhost:8080/feign/getById?uid=其他值 和 http://localhost:8080/feign/getById?uid=123 进行测试
6.3.7 授权规则
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制(黑白名单控制)的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
代码实现:
1.创建SentinelOriginParser类实现指定接口,指定要传递的origin参数(参数名任意)
package com.qf.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.util.StringUtil;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class SentinelOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String origin = httpServletRequest.getParameter("origin");
if(StringUtil.isBlank(origin)) {
throw new IllegalArgumentException("origin parameter is must not be empty");
}
//授权规则配置的就是该参数的值
return origin;
}
}
2.添加授权规则,white是origin的值,黑白名单表示是否允许访问
3.访问:localhost:8080/feign/getById?uid=1001&origin=white 进行测试
6.3.8 系统规则(了解)
系统自适应限流,Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏
6.4 sentinel对比
七. Feign与Sentinel的整合
7.1 配置application.yml
在springcloudalibaba-micro-consumer-8080工程中的application.yml中添加 feign 的配置
server:
port: 8080
spring:
application:
name: micro-service-consumer #服务消费方
cloud:
nacos:
discovery:
enabled: true
server-addr: 127.0.0.1:8848
sentinel:
transport:
port: 8719
dashboard: 127.0.0.1:8081
feign:
sentinel:
enabled: true #feign整合sentinel
7.2 配置服务降级后的处理
在 UserService 上的@FeignClient注解上添加 fallback 属性
package com.qf.service;
import com.qf.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Service
//服务提供方的名称(也可以写在方法上)
//配置fallback,当方法熔断降级后调用指定类中对应的方法
@FeignClient(value = "micro-service-provider",fallback = UserServiceHandler.class)
public interface UserService {
@RequestMapping("/user-provider/findAll")
public JsonResult findAll();
//模拟数据库操作
//查询单个
@GetMapping("/user-provider/findById")
public JsonResult findById(@RequestParam("id") Integer id);
//删除单个
@DeleteMapping("/user-provider/deleteById/{id}")
public JsonResult deleteById(@PathVariable("id") Integer id);
//添加
@PostMapping("/user-provider/addUser")
public JsonResult addUser(@RequestBody User user);
//修改
@PutMapping("/user-provider/updateUser")
public JsonResult updateUser(@RequestParam Integer id,@RequestParam String username,@RequestParam String password);
}
7.3 编写降级对应的类
编写 UserServiceHandler类,我们这里先以findById方法为例
package com.qf.service;
import com.qf.pojo.User;
import com.qf.utils.JsonResult;
import org.springframework.stereotype.Component;
//和UserService中的方法一一对应
@Component
public class UserServiceHandler implements UserService{
@Override
public JsonResult getById(Integer id) {
JsonResult jsonResult = JsonResult.error();
jsonResult.setData("sentinel异常");
return jsonResult;
}
@Override
public JsonResult findAll() {
return JsonResult.error();
}
@Override
public JsonResult findById(Integer id) {
return JsonResult.error();
}
@Override
public JsonResult deleteById(Integer id) {
return JsonResult.error();
}
@Override
public JsonResult addUser(User user) {
return JsonResult.error();
}
@Override
public JsonResult updateUser(Integer id, String username, String password) {
return JsonResult.error();
}
}
7.4 配置限流或者降级
4.在sentinel面板上找到对应的方法,配置限流或者降级
5.启动访问:http://localhost:8080/feign/findById?uid=1001&origin=white 测试即可
八. Sentinel的持久化(了解)
通过接入 Sentinel Dashboard 后,在页面上操作来更新规则,都无法避免一个问题,那就是服务重新后,规则就丢失了,因为默认情况下规则是保存在内存中的。sentinel中持久化的方式有两种,pull模式和push模式。
pull模式是指站在第三方持久化系统(redis, nacos)的角度,他们去到sentinel中定时去拉去配置信息,可能会造成数据的不一致性。
push模式是站在sentinel的角度,将其配置信息主动推送给第三方持久化系统,sentinel官方也推荐在线上使用该模式。
8.1 sentinel-dashboard改造
A. 将sentinel的源码clone到本地
B. 进入到sentinel-dashboard目录下,修改pom.xml文件
<!-- 修改之前的内容 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<scope>test</scope>
</dependency>
<!-- 修改之后的内容 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
C. 修改 src\main\webapp\resources\app\scripts\directives\sidebar\sidebar.html 文件
未修改之前的内容
<!-- <li ui-sref-active="active" ng-if="entry.appType==0"> -->
<!-- <a ui-sref="dashboard.flow({app: entry.app})"> -->
<!-- <i class="glyphicon glyphicon-filter"></i> 流控规则 V1</a> -->
<!-- </li> -->
<li ui-sref-active="active" ng-if="!entry.isGateway">
<a ui-sref="dashboard.flowV1({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则
</a>
</li>
修改之后的内容
<li ui-sref-active="active" ng-if="entry.appType==0">
<a ui-sref="dashboard.flow({app: entry.app})">
<i class="glyphicon glyphicon-filter"></i> 流控规则
</a>
</li>
<!-- <li ui-sref-active="active" ng-if="!entry.isGateway">-->
<!-- <a ui-sref="dashboard.flow({app: entry.app})">-->
<!-- <i class="glyphicon glyphicon-filter"></i> 流控规则
</a> -->
<!-- </li>-->
D. 将 src\test\java\com\alibaba\csp\sentinel\dashboard\rule\nacos
目录下的四个Java文件拷贝到src\main\java\com\alibaba\csp\sentinel\dashboard\rule
目录下
E. 修改 src\main\webapp\resources\app\scripts\controllers\identity.js
文件,修改内容如下:
F. 重新打包生成Jar包,进入到sentinel目录下(注:不是sentinel-dashboard目录),执行如下命令:
mvn clean
mvn install -DskipTests
G. 进入到sentinel-dashboard/target目录下,执行如下内容:
java -jar sentinel-dashboard.jar
8.2 配置
spring:
cloud:
sentinel:
datasource:
# 这个名字随意,但是要有意义
flow:
nacos:
server-addr: 192.168.31.173:8848
groupId: SENTINEL_GROUP
rule-type: flow
8.3 测试
在sentinel-dashboard控制面板添加一个流量控制规则
标签:Feign,JsonResult,Spring,Nacos,springframework,org,import,com,public 来源: https://www.cnblogs.com/qtyanan/p/16464401.html