微服务总结
一. nacos
nacos 充当注册中心和配置中心。作为注册中心主要的作用:
- 实现服务间的解耦。
- 方便服务的水平扩展。
二. 单机版使用
2.1 安装
一. 解压
二. 修改 NACOS_HOME/bin 目录下的
startup.cmd
set MODE="standalone"
第三步,双击
startup.cmd
2.2 在项目中使用
一. 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
二. 配置
spring:
application:
# 注册到注册中心的服务名,也就给服务端调用的时候使用的名字
name: ms-provider
cloud:
nacos:
discovery:
# nacos的地址
server-addr: 127.0.0.1:8848
# 是否将服务注册到 nacos. false就是不注册,但是可以获取服务
register-enabled: true
三. 服务的调用方使用
private RestTemplate restTemplate;
private DiscoveryClient discoveryClient; // 服务发现的客户端
public TestController(RestTemplate restTemplate, DiscoveryClient discoveryClient) {
this.restTemplate = restTemplate;
this.discoveryClient = discoveryClient;
}
@GetMapping
public Object getRemoteData() {
// ms-provider 是服务提供方的服务名 spring.application.name 的值
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("ms-provider");
ServiceInstance si = serviceInstances.get(0); // 获取集群中的第一个服务
String url = "http://" + si.getHost() + ":" + si.getPort();
String fullURL = url + "/user";
List<String> results = restTemplate.getForObject(fullURL, List.class);
return results;
// List<String> results = restTemplate.getForObject("http://localhost:7834/user",
List.class);
// return results;
}
三. 集群的使用
将两个nacos的端口号改的不一样,
NACOS_HOME/conf/application.properties
3.1 集群的搭建
一. 将 NACOS_HOME/bin 下的 startup.cmd 的模式改为
cluster
set MODE="cluster"
二. 将 NACOS_HOME/conf 下的
cluster.conf.example
拷贝一份,叫做cluster.conf
,将所有的集群配置到文件中:
# 看实际情况
192.168.137.1:8848
192.168.137.1:9948
三. 分别启动两个nacos
3.2 在项目中使用
相对于单机版,只需要在
application.yml
文件中列出所有的集群节点:
spring:
application:
name: ms-provider
cloud:
nacos:
discovery:
# 集群配置
server-addr: 192.168.137.1:8848,192.168.137.1:9948
register-enabled: true
四. 修改nacos密码
一. 新建数据
nacos
, 设置编码
二. 导入
NACOS_HOME/conf/nacos-mysql.sql
文件到新建的nacos
数据库中。
三. 修改
users
表, 密码根据springboot-mp
中测试代码中PasswordTest
中的代码来生成
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
System.out.println(passwordEncoder.encode("3455"));
四. 将根据上面生成的代码替换数据数据中的密码
五. 微服务的版本
<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.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
六. Ribbon
Ribbon是客户端一个负载均衡的工具,所谓的客户端指的是服务的调用方.
一. 只需要将
RestTemplate
纳入到IOC容器的位置加上@LoadBalanced
即可
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
二. 调用服务的时候,不用再根据服务名获取IP和端口号,可以直接使用
List<String> results = restTemplate.getForObject("http://ms-provider/user", List.class);
七. Feign
Feign是基于Ribbon的另外一个客户端的负载均衡工具,进一步的简化服务间的调用代码。让我们在调用远程服务就像调用本地服务一样。
一. 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
二. 开启Feign, 在工程的启动类上加上
@EnableFeignClients
@SpringBootApplication
@EnableFeignClients
public class ConsumerApplication {
public static void main( String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
三. 编写接口,在接口上直接使用
@FeignClient
这个注解
// value是需要调用的服务名,spring cloud 会在底层帮我们生成该接口的实现类,并纳入到容器中
@FeignClient(value = "ms-provider")
public interface IUserService {
// ms-provider 服务中的接口
@GetMapping("/user")
List<String> getList();
}
四. 在Controller中使用
private IUserService userService;
public TestController2(IUserService userService) {
this.userService = userService;
}
@GetMapping
public Object get() throws InterruptedException {
return userService.getList();
}
八. sentinel
整个spring cloud 和 spring cloud alibaba 的区别就在于熔断的组件不同,spring cloud alibaba提供的熔断组件
sentinel
, 然后 spring cloud 本身提供的组件是hystrix
。 sentinel 提供一整套服务的
限流
、热点
、热点
、授权
的规则。主要用于服务的降级与熔断。 服务降级:就是调用服务的时候,因为服务本身原因,目前无法提供给我们想要的结果,但是还是会返回一个结果,但是结果并不是一个正确的结果。也是为了保护我们服务和整个系统。
熔断:服务暂时是无法提供对外访问的。
8.1 安装
下载:https://github.com/alibaba/Sentinel/releases
启动:java -jar sentinel-dashboard-1.8.2.jar
8.2 概念解释
QPS(Query Per Second): 表示每秒的请求次数。
单机阈值:是每秒请求次数。
8.2 流控
下图传达的意思是:每秒中查询次数最多一次,如果超过一次,就直接
sentinel内部有一个 ‘coldFactor(冷因子)’, 值为3.
下图传达意思是:在刚开开始访问的时候,每秒的访问此处最多 9 / 3(冷因子) = 3, 然后在 5秒(预热时长) 后达到9.
下图传达的意思是:每秒中最多处理 5 个请求,其余的请求会等待,如果等待的时间超过2000ms,还没能获取到请求,那么会失败。
8.3 熔断
RT(Response Time): 响应时间。
下图的意思:在连续5个请求中,如果有超过(包括) 20%(0.2)的请求,响应时间超过 200ms, 就会熔断。熔断时长为 5s, 也就是在未来的5s钟之内,所有的请求都无法处理。
8.4 热点规则
热点规则就是针对请求参数来进行限流的,我们以下面的一个请求为例:
@GetMapping("/1")
@SentinelResource("get")
public Object get(Integer id, String name) {
System.out.println(id + "###" + name);
return personService.getAll();
}
对于下图:参数索引指的是代码中
Integer id
, 如果在请求的过程中,可以不携带参数,但是如果携带了 id,那么在1秒中之内,只能有一个请求。
当我们在配置了热点规则之后,在热点规则中点击编辑又会多出
高级选项
,主要是针对特定的某个参数的值进行限流。 下图的意思是:参数索引指的是代码中
Integer id
, 如果在请求的过程中,可以不携带参数,但是如果携带了 id,那么在1秒中之内,只能有10请求;但是当id的值为 56 的时候,那么每秒中只能处理2个请求(需要点击"添加"按钮)。场景就是电商中突然出现爆款问题。
8.5 授权规则
授权规则就是用户在请求必须要携带指定 “白名单” 中的参数值。
下图的意思是:用户请求中(可以通过请求、或者请求头)携带一个指定参数,值为
web
或者android
才能访问我们的应用,必须要配合才能实现这个效果。
配合热点规则代码,如下所示:
// 这个类必须要实现 RequestOriginParser 接口
@Component
public class SentinelOriginParse implements RequestOriginParser {
// 该方法的返回值是作为授权规则的白名单或者黑名单来进行比对,如果符合白名单就是可以访问应用,
// 如果是在黑名单中或者不在白名单中,就不能访问应用
@Override
public String parseOrigin(HttpServletRequest request) {
String assign = request.getParameter("assign");
// 三元判断的目的,是为了解决当用户不传入的时候,还是可以正常访问,所以我们
// 就在用户什么都不传的时候,随机给他一个值。
return assign == null ? RandomStringUtils.random(16) : assign;
}
}
九. 自定义返回的错误信息
如果我们在 sentinel 中配置了各种的规则,请求违反这些规则之后,都是报一个 500 错误,不便于前端给用户进行一个友好的提示,所以我们需要根据实际的情况,返回特定的提示信息。
9.1 在类的内部进行处理
/**
* sentinel 针对违反了不同的规则的处理,底层是通过抛出异常的方式来进行处理。
* blockHandler的值就是针对违反规则的错误的处理函数。
*/
@GetMapping
@SentinelResource(value = "test2", blockHandler = "getHandler")
public Object get() throws InterruptedException {
return personService.getAll();
}
/**
* BlockException 是所有的限流或者熔断规则异常的顶级异常。
* AuthorityException: 授权异常
* FlowException: 流控异常。
* DegradeException: 熔断异常。
* ParamFlowException: 热点异常
*/
/**
* getHandler 要和 @SentinelResource 注解中的 blockHandler 的值要对应上,而且函数必须接受
* BlockException 接受这个异常,我们通过具体的异常类型,来判断用户的请求到底是违反了那个规则。
*/
// BlockException 是 限流、熔断、热点、授权四种异常的一个父级接口.
public Object getHandler(BlockException ex) {
if(ex instanceof AuthorityException) {
return "授权失败";
}else if(ex instanceof FlowException) {
return "你点的太快了";
}else if(ex instanceof DegradeException) {
return "熔断了";
}else if(ex instanceof ParamFlowException) {
return "你访问的数据被太多人访问.";
}else {
return "其他异常";
}
}
9.2 单独定义类来处理异常
我们可能存在一个请求,不同的接口中都会用到同一个错误的处理,但是
9.1
章节中的处理方式只能将错误的处理和接口放到一个类中。如果多个类中多个接口都要使用同一种错误处理,那么势必需要在每个类中都定义相同的代码。所以就很有必要将错误的处理,单独放到一个类中,以供其他接口来使用。
@RestController
@RequestMapping("/test2")
public class TestController2 {
private IUserService userService;
public TestController2(IUserService userServic) {
this.userService = userService;
this.personService = personService;
}
/**
* 需要在 blockHandlerClass 所对应的类来定义统一的错误处理。
* blockHandler对应的是 SentinelBlockHandler 这个类中的一个 “静态方法“,用来处理当前接口
* 违反规则之后的统一错误处理。
*/
@GetMapping
@SentinelResource(value = "test2", blockHandler = "getHandler", blockHandlerClass = SentinelBlockHandler.class)
public Object get() throws InterruptedException {
return userService.getList();
}
}
public class SentinelBlockHandler {
// 该方法必须是静态的
public static Object getHandler(BlockException ex) {
if(ex instanceof AuthorityException) {
return "授权失败";
}else if(ex instanceof FlowException) {
return "你点的太快了";
}else if(ex instanceof DegradeException) {
return "熔断了";
}else if(ex instanceof ParamFlowException) {
return "你访问的数据被太多人访问.";
}else {
return "其他异常";
}
}
}
十. Feign与Sentinel的整合
如果A服务调用B服务,如果B服务暂时无法提供服务,或者内部异常了,A就无法正常的使用,所以在服务间我们也需要进行服务的降级处理。
配置就是在 @FeignClient 注解加上一定的参数
10.1 简单的服务降级
// 通过fallback 去指定降级处理的类,该类需要实现 IUserService ,实现接口中所有的方法,当调用失败,就调用
// 实现类中对应的方法就行了。
@FeignClient(value = "ms-provider", fallback = UserServiceFallback.class)
public interface IUserService {
@GetMapping("/user")
List<String> getList();
}
/**
* 1.该类必须要纳入到IOC容器中。
* 2.该类必须要实现需要进行服务降级的接口,如果调用远程失败了,就调用该类中对应的方法。
*/
@Component
public class UserServiceFallback implements IUserService {
@Override
public List<String> getList() {
System.out.println("服务降级");
return Arrays.asList(new String[]{});
}
}
这种简单的服务降级,存在一个问题,就是我们无法得知我们的系统是因为什么原因导致了服务的降级,说白就是我们无法获取异常的信息。
10.2 异常处理服务降级
/**
* 通过 fallbackFactory 指定一个类,该类需要实现 FallbackFactory 接口
*/
@FeignClient(value = "ms-provider", fallbackFactory = UserServiceFallbackFactory.class)
public interface IUserService {
@GetMapping("/user")
List<String> getList();
}
/**
* 1.该类必须要纳入到IOC容器中。
* 2.泛型类型要是需要降级的接口。
*/
@Component
@Slf4j
public class UserServiceFallbackFactory implements FallbackFactory<IUserService> {
/*
* 我们可以通过 throwable 获取到错误的信息,无论是接口中哪个方法,调用失败之后
* 都会将错误传递给 throwable
*/
@Override
public IUserService create(Throwable throwable) {
return new IUserService() {
@Override
public List<String> getList() {
if(log.isDebugEnabled()) {
log.debug(throwable.getMessage());
}
return Arrays.asList(new String[]{});
}
};
}
}
十一. Gateway网关
zuul是spring cloud的第一代网关,gateway是第二代网关,它是基于netty来实现的。网关的作用:
- 实现路由。
- 过滤。
11.1 基础配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
register-enabled: false
gateway:
discovery:
locator:
# 为true, 表示我们可以通过服务名的方式去找到对应的服务
# http://localhost:9090/ms-consumer/test2
# http://localhost:9090/ms-provider/user
# 可以通过上面的形式来访问我们的服务
enabled: true
如上方式存在的问题:将服务名暴露出来的,这是不安全的。
12.2 升级配置
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
register-enabled: false
gateway:
discovery:
locator:
# 为true, 表示我们可以通过服务名的方式去找到对应的服务
# http://localhost:9090/ms-consumer/test2
# http://localhost:9090/ms-provider/user
# 可以通过上面的形式来访问我们的服务
enabled: false
routes:
# 每个路由都需要一个id, 名字可以任意, 但是我们通常写成服务名的形式
- id: ms-consumer
# lb -> load balance, 下面这句话, lb:// 是固定的写法, ms-consumer 必须是微服务中某个服务的名字
# 当请求 predicates 中 Path 下的某个路径的时候, 就会将服务打到该 ms-consumer 这个服务中
uri: lb://ms-consumer
# predicates 在程序中我们将其翻译成 "谓词". 表示判断, 是否满足某个条件
predicates:
# http://localhost:9090/test2
# http://localhost:9090/test/a
# - Path=/test2,/test/**
# http://localhost:9090/mc/a/test, http://localhost:9090/mc/a/test2
- Path=/mp/**
# 表示请求的参数必须是 GET 请求
- Method=GET
# 表示请求头中必须要携带 token, 而且只必须是 \w
- Header=token,\w+
# 表示请求的参数中必须有 origin 这个参数名, 参数值为 \w+
- Query=origin,\w+
#表示访问该服务必须是 2021-08-01 00:00:00 之后才能访问
- After=2021-07-01T00:00:00+08:00[Asia/Shanghai]
- RemoteAddr=192.168.52.64/27
# filters下能写的过滤器都具有一个特征, 以 GatewayFilterFactory 来结尾, 位于: spring-cloud-gateway-core-2.2.5.RELEASE.jar 包下
# org.springframework.cloud.gateway.filter.factory 这个包下的内容, 用的时候将 GatewayFilterFactory 这些去掉就可以了
filters:
# StripPrefix 对应的是 StripPrefixGatewayFilterFactory 这个类, 作用是当用户发起一个请求:
# http://localhost:9090/test 会将 uri 部分第一个 /字母 部分去掉, 也就是将 /mc 去掉.
- StripPrefix=1
12.3 限流
sentinel的限流只是针对某一个资源,在网关中的限流是针对整个服务的。在Gateway内部提供了一个基于Redis令牌桶的限流器。
spring:
redis:
host: localhost
port: 6379
# 前面部分和12.2 是相同的
filters:
# StripPrefix 对应的是 StripPrefixGatewayFilterFactory 这个类, 作用是当用户发起一个请求:
# http://localhost:9090/test 会将 uri 部分第一个 /字母 部分去掉, 也就是将 /mc 去掉.
- StripPrefix=1
- name: RequestRateLimiter
# 给 RequestRateLimiterGatewayFilterFactory 给它传入参数
args:
# 需要配置 IOC 容器中获取IP的 Bean, 使用 SpringEL 表达式语言来获取
keyResolver: '#{@hostKeyResolver}'
# 每秒中往令牌桶中放令牌的数量
redis-rate-limiter.replenishRate: 3
# 令牌桶中允许存放的令牌的总数
redis-rate-limiter.burstCapacity: 3
因为限流是根据ip来进行限流的,所以我们必须要获取到访问的远程客户端的ip地址,获取方式如下:
/**
* 在如上的配置中,keyResolver 中通过 #{@hostKeyResolver} 其实就是获取的当前的bean.
*/
@Component
public class HostKeyResolver implements KeyResolver {
/**
* React 反应式编程:
* Mono, 针对单个数据返回
* Flux, 针对批量数据返回.
* 反应式兴起不起来的主要原因, 受限与数据库.
*/
// 只需要返回用户的 ip
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
12.4 全局过滤器
全局过滤器是针对所有的请求都会进行过滤。
// @Order(20) 是用来控制过滤器的顺序,数字越小越先执行
@Bean
@Order(20)
public GlobalFilter firstFilter() {
return new GlobalFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("firstFilter");
return chain.filter(exchange); // 表示接着往下走
}
};
}
十二. 配置中心
配置中心的作用就是实现配置的集中管理,让整个产品从开发到生产不会出现回流问题,对于一些敏感的信息起到一个保护的作用。
配置中心使用 nacos, 将所有的配置都放到 nacos 上,在
bootstrap.yml
文件中连接到 nacos, 在启动的时候,会从nacos中读取所有的配置信息,用于项目的配置。
一. 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
二. 在工程中新建一个
bootstrap.yml
文件,该文件的启动是优先于application.yml
, 如果两个文件中配置出现冲突,那么以application.yml
为准。配置如下:
# 如果按照如下的配置, 在应用启动的时候会根据
# ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
spring:
cloud:
nacos:
config:
# 配置中心 nacos的地址
server-addr: localhost:8848
# 配置文件的后缀名
file-extension: yml
application:
# 这是服务名,也是从nacos中去获取配置的前缀名, 如果这里配置了,那么 application.yml 中就不用配置了
name: ms-consomer
profiles:
# active是哪个环境配置
active: test
三. 在nacos中新建不同环境的配置文件
ms-consomer-dev.yml
ms-consomer-test.yml
四. 在
ms-consomer-dev.yml
、ms-consomer-test.yml
中依据不同的环境配置不同的信息,例如:
server:
port: 7835
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 表示自己的服务不注册上去
register-enabled: true
sentinel:
transport:
# 这个配置是我们应用将信息上传到sentinel的连接
dashboard: http://localhost:8080
# 表示sentinel将某个配置信息推给我们的应用
port: 8719
feign:
sentinel:
# 开启feign与sentinel的整合
enabled: true
十三. 链路追踪
链路追踪的作用就是根据整个微服务调用的链路,查看分析各个服务调用的情况(耗时等信息)
一. 引入依赖(在每一个需要追踪的微服务中都需要添加依赖)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
二. 下载zip,下载地址:https://github.com/openzipkin/zipkin
三. 启动zipkin,
java -jar zipkin-server-2.23.2-exec.jar
四. 在每个需要进行链路追中的服务配置中加上如下的配置信息:
spring:
zipkin:
# zipkin的地址
base-url: http://localhost:9411
# 是否从nacos上获取zipkin的地址。
discovery-client-enabled: false
sleuth:
sampler:
# 抽样率 100%, 默认10%
probability: 0.2