Spring Cloud Alibaba学习记录

本文最后更新于 2022年3月11日 下午

Spring Cloud Alibaba

微服务相关概念

为什么要使用微服务/分布式架构

架构演变过程:

单体架构在业务规模不大的情况下,开发维护容易。随着企业的发展,业务规模与日递增,单体应用变得愈发臃肿。由于单体应用将各种业务模块聚合在一起,并且部署在一个进程内,所以通常我们对其中一个业务模块的修改也必须将整个应用重新打包上线。为了解决单体应用变得庞大脯肿之后产生的难以维护的问题,出现了微服务,分布式等架构。

优点:

  • 服务原子化拆分,独立打包、部署和升级,保证每个微服务清晰的任务划分,利于扩展
  • 微服务之间采用Restful等轻量级http协议相互调用

缺点:

  • 分布式系统开发的技术成本高(容错、分布式事务等)

  • 复杂性更高。各个微服务进行分布式独立部署,当进行模块调用的时候,分布式将会变得更加麻烦。

Spring Cloud Alibaba相关概念

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

流程

组件:

  • nacos:一个更易于构建云原生应用的动态服务发现配置管理和服务管理平台。
  • sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
  • seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。

Spring Cloud的不同实现方式:

Spring Cloud Alibaba 配置

先使用Spring Initializr创建一个项目,对应版本参照WIKI:

本项目Spring Cloud Alibaba采用2.2.7.RELEASE版本。

父POM文件配置为:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modules>
<module>Order</module>
<module>Stock</module>
<module>Order_OpenFeign</module>
<module>Config</module>
<module>Order-Sentinel</module>
<module>Order_OpenFeign_Sentinel</module>
<module>03-alibaba-order-seata</module>
<module>04-alibaba-stock-seata</module>
</modules>

<groupId>com.example</groupId>
<artifactId>SpringCloudAlibaba</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>

<properties>
<java.version>1.8</java.version>
<spring.cloud.alibaba.version>2.2.7.RELEASE</spring.cloud.alibaba.version>
<spring.boot.version>2.3.12.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR12</spring.cloud.version>
</properties>


<!--子模块直接继承,无需显示声明依赖-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>


<!--子模块需要显式声明依赖-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

Nacos 搭建及使用

Nacos相关概念

Nacos运行及服务调用流程

更加详细的流程图:

其中,server端都要定时发心跳包,使注册注册中心即使发现服务以及检测服务当前状态。


一些注册中心的对比:

项目服务的一些代码编写

在项目中创建Order,Stock两个服务,其中Order调用Stock接口。

在不使用Feign调用接口时,用的是restTemplate,其中一些代码:

OrderApplication:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootApplication
//@RibbonClients(value = {
// @RibbonClient(name = "stock-service", configuration = RandRule.class)
//})
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}

@Bean
@LoadBalanced
public RestTemplate restTemplate(RestTemplateBuilder builder){
RestTemplate restTemplate = builder.build();
return restTemplate;
}
}

OrderController:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/order")
public class OrderController {

@Resource
RestTemplate restTemplate;

@GetMapping("/add")
public String add() {
String msg = restTemplate.getForObject("http://stock-service/stock/deduct", String.class);
return "下单成功" + msg;
}
}

StockController:

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
@RestController
@RequestMapping("/stock")
public class StockController {

@Value("${server.port}")
private String port;

@GetMapping("/deduct")
public String deduct() {
System.out.println("库存扣除");
return "库存扣除,当前端口:" + port;
}

@GetMapping("/error")
public String errorTest() {
int a = 1 / 0;
System.out.println("库存扣除");
return "库存扣除,当前端口:" + port;
}

@GetMapping("/get/{id}")
public String buy(@PathVariable Integer id) {
return "商品购买,id = " + id;
}
}

Nacos搭建及配置

Server端搭建

步骤参照:https://nacos.io/zh-cn/docs/quick-start-docker.html

首先需要在修改nacos-docker/example/.env文件

改为上面对应的版本

单机模式:

standalone-mysql-5.7.yaml

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
version: "2"
services:
nacos:
image: nacos/nacos-server:${NACOS_VERSION}
container_name: nacos-standalone-mysql
env_file:
- ../env/nacos-standlone-mysql.env
volumes:
- ./standalone-logs/:/home/nacos/logs
- ./init.d/custom.properties:/home/nacos/init.d/custom.properties
ports:
- "8848:8848"
- "9848:9848"
- "9555:9555"
depends_on:
- mysql
restart: on-failure
mysql:
container_name: mysql
image: nacos/nacos-mysql:5.7
env_file:
- ../env/mysql.env
volumes:
- ./mysql:/var/lib/mysql
ports:
- "3306:3306"

提前在当前目录建立好volumes对应的目录和文件。

直接docker-compose -f example/standalone-mysql-5.7.yaml up -d运行容器

访问http://ip:8848/nacos/ 正常。

集群模式:

cluster-hostname.yaml,修改一些配置(JVM内存设置,防止打不开):

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
61
62
63
64
65
66
67
68
69
70
version: "3"
services:
nacos1:
hostname: nacos1
container_name: nacos1
image: nacos/nacos-server:${NACOS_VERSION}
volumes:
- ./cluster-logs/nacos1:/home/nacos/logs
- ./init.d/custom.properties:/home/nacos/init.d/custom.properties
ports:
- "8848:8848"
- "9848:9848"
- "9555:9555"
env_file:
- ../env/nacos-hostname.env
environment:
- JVM_XMS=512m
- JVM_XMX=512m
- JVM_XMN=128m
restart: always
depends_on:
- mysql

nacos2:
hostname: nacos2
image: nacos/nacos-server:${NACOS_VERSION}
container_name: nacos2
volumes:
- ./cluster-logs/nacos2:/home/nacos/logs
- ./init.d/custom.properties:/home/nacos/init.d/custom.properties
ports:
- "8849:8848"
- "9849:9848"
env_file:
- ../env/nacos-hostname.env
environment:
- JVM_XMS=512m
- JVM_XMX=512m
- JVM_XMN=128m
restart: always
depends_on:
- mysql
nacos3:
hostname: nacos3
image: nacos/nacos-server:${NACOS_VERSION}
container_name: nacos3
volumes:
- ./cluster-logs/nacos3:/home/nacos/logs
- ./init.d/custom.properties:/home/nacos/init.d/custom.properties
ports:
- "8850:8848"
- "9850:9848"
env_file:
- ../env/nacos-hostname.env
environment:
- JVM_XMS=512m
- JVM_XMX=512m
- JVM_XMN=128m
restart: always
depends_on:
- mysql
mysql:
container_name: mysql
image: nacos/nacos-mysql:5.7
env_file:
- ../env/mysql.env
volumes:
- ./mysql:/var/lib/mysql
ports:
- "3306:3306"

还是建立好文件和文件夹,启动容器,docker ps看容器均正常运行,打开网站,均能正常访问。

配置nginx:

nginx_docker.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
version: "3"
services:
nginx:
restart: always
image: nginx:latest
container_name: nginx
ports:
- "8855:80"
- "8856:443"
- "9080:9080"
volumes:
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./data/nginx/:/usr/share/nginx/html/
- ./log/nginx/:/var/log/nginx/

nginx.conf:

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
61
#user  nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}

stream {
upstream nacosgrpcc {
server 192.168.2.125:9848;
server 192.168.2.125:9849;
server 192.168.2.125:9850;
}
server {
listen 9080;
proxy_pass nacosgrpcc;
}
}

http {
include mime.types;
default_type application/octet-stream;

#加上下面这句可以让单个上传文件在100MB内
client_max_body_size 100m;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

upstream nacos {
server 192.168.2.125:8848 weight=10;
server 192.168.2.125:8849 weight=10;
server 192.168.2.125:8850 weight=10;
}

server{
listen 80;
server_name 192.168.2.125;
location / {
proxy_pass http://nacos/;
}
}
}

配置完网页可以正常访问,但是程序并不能启动,连上注册中心,待解决。

可选方案:https://gitee.com/little_wear/docker-compose-nacos-nginx 待验证。

客户端配置

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

加上web和nacos discovery依赖即可。

application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8010

spring:
application:
name: order-service
cloud:
nacos:
server-addr: 192.168.2.125:8848
discovery:
username: nacos
password: nacos
namespace: public

一启动两个服务,在注册中心中可以看到对应的服务:

负载均衡

创建两个Stock服务,使用Edit Configuration:

改好端口就行,

之前程序中使用了RestTemplate,@LoadBalanced,消费端默认使用轮询机制。

其中,Spring Cloud使用的Ribben负载均衡为客户端负载均衡,配置均在客户端中,获取注册中心服务段相关信息,由客户端选择提供服务的服务端。

而nginx负载均衡为服务段负载均衡,客户端先发送请求,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;需要集中式的配置。即在服务器端进行负载均衡算法的分配。

Ribbon负载均衡相关算法:

使用Ribbon的随机负载均衡策略,每个服务访问完全随机:

配置类RandRule:

1
2
3
4
5
6
7
8
@Configuration
public class RandRule {

@Bean
public IRule iRule(){
return new RandomRule();
}
}

启动类上要加上Ribbon注解:

1
2
3
4
@SpringBootApplication
@RibbonClients(value = {
@RibbonClient(name = "stock-service", configuration = RandRule.class)
})

使用NacosRule(基于权重的负载均衡策略)

application.yml中添加:

1
2
3
4
#基于权重的负载均衡策略
stock-service:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule

不需要@RibbonClients注解,直接在Nacos中配置:

其中权重范围为0-1,越小访问概率越低。

使用自定义负载均衡策略:

application.yml中添加:

1
2
3
4
5
6
7
8
9
10
#自定义
stock-service:
ribbon:
NFLoadBalancerRuleClassName: com.example.ribbon.rule.CustomRule

#开启饥饿加载,启动服务就加载,否则调用才初始化
ribbon:
eager-load:
enabled: true
clients: stock-service

启动类中不用其他注解

自定义类(实现随机负载均衡策略):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CustomRule extends AbstractLoadBalancerRule {
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {

}

@Override
public Server choose(Object o) {
ILoadBalancer loadBalancer = this.getLoadBalancer();
List<Server> servers = loadBalancer.getReachableServers();

int random = ThreadLocalRandom.current().nextInt(servers.size());

Server server = servers.get(random);

return server;
}
}

使用Spring Cloud自带负载均衡LoadBalancer(轮询):

需要在POM中排除Ribbon:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
不使用ribbon负载均衡
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>

加上loadbalancer:

1
2
3
4
5
6
使用Spring Cloud负载均衡
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>3.1.1</version>
</dependency>

只需要在配置文件中添加:

1
2
3
loadbalancer:
ribbon:
enabled: false

Feign的配置和使用

关于一些接口调用的方法:

Feign是Netflix开发声明式,模板化的HTTP客户端,可以优雅的调用HTTP API。Feign支持多种注解,也有自带注解。

SpringCloudOpenFeign对Feign进行了增强,使其支持Spring MVC注解,还整合了Ribbon和Nacos,使Feign的使用更加方便。

Feign可以做到使用HTTP请求远程服务时就像调用本地方法一样的体验,开发者完全感知不到这是否是远程方法或HTTP请求。

Feign使用

引入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

启动类需要加入注解@EnableFeignClients:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}

}

新建服务类,接口:

1
2
3
4
5
6
@FeignClient(name = "stock-service", path = "/stock")
public interface StockFeignService {

@RequestMapping("/deduct")
String deduct();
}

完全模仿接口的形式,Controller:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/order")
public class OrderController {

@Resource
StockFeignService service;

@GetMapping("/add")
public String add() {
return "下单成功" + service.deduct();
}
}

Feign日志配置使用

先创建一个配置类:

1
2
3
4
5
6
7
8
//@Configuration //全局配置
public class FeignConfig {

@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}

@Configuration加上就是全局配置,对所有接口都生效

不加@Configuration,则为部分配置,需要在接口中这样写:

1
2
3
4
5
6
7
//部分配置日志级别,需要去掉@Configuration
@FeignClient(name = "stock-service", path = "/stock", configuration = FeignConfig.class)
public interface StockFeignService {

@RequestMapping("/deduct")
String deduct();
}

需要加上configuration参数,配置完后不显示日志,是因为spirngboot 默认info 级别,大于debug,需要配置:

1
2
3
4
# spirngboot 默认info 级别,大于debug,不显示
logging:
level:
com.example.order.feign: debug

配置文件配置日志:

1
2
3
4
5
6
# 部分日志级别设置
feign:
client:
config:
stock-service:
loggerLevel: BASIC

其他配置

契约配置:

1
2
3
4
5
6
feign:
client:
config:
stock-service:
loggerLevel: BASIC
contract: feign.Contract.Default #使用Feign原生注解

SpringCloud在Feign的基础上做了扩展,使用SpringMVC的注解来完成Feign的功能。原生Feign是不支持SpringMVC注解的,如果你想在SpringCloud中使用原生注解的方式来定义客户端也是可以的,通过契约来改变这个配置,SpringCloud中模式是使用SpringMVC Contract。

超时时间配置:

1
2
3
4
5
6
7
8
feign:
client:
config:
stock-service:
loggerLevel: BASIC
# contract: feign.Contract.Default #使用Feign原生注解
connectTimeout: 5000 #连接超时
readTimeout: 3000 #处理超时

拦截器配置:

在客户端消费者调用提供者服务时进行拦截。

1
2
3
4
5
6
7
8
9
10
11
public class CustomFeignInterceptor implements RequestInterceptor {

Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void apply(RequestTemplate template) {
//TODO
template.header("xxx", "xxx");

logger.info("Feign Interceptor!!!!!!!!!!!!!!!!!");
}
}

全局配置(针对所有接口调用):

配置类中加上@Configuration,并且注入到Spring中:

1
2
3
4
@Bean
public CustomFeignInterceptor customFeignInterceptor(){
return new CustomFeignInterceptor();
}

局部配置(推荐):

直接在配置中声明:

1
2
3
4
5
6
7
8
9
10
feign:
client:
config:
stock-service:
loggerLevel: BASIC
# contract: feign.Contract.Default #使用Feign原生注解
connectTimeout: 5000 #连接超时
readTimeout: 3000 #处理超时
requestInterceptors[0]: #开启拦截器
com.example.order.intercptor.CustomFeignInterceptor

Nacos Config 配置中心

Nacos 提供用于存储配置和其他元数据的 key/value 存储,为分布式系统中的外部化配置提供服务器端和客户端支持。使用 Spring Cloud Alibaba Nacos Config,您可以在 Nacos Server 集中管理你 Spring Cloud 应用的外部属性配置。

主要结构图:

方便维护,统一管理,安全,时效

几个配置中心的对比:

使用配置中心

依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

必须使用 bootstrap.properties 配置文件来配置Nacos Server 地址

bootstrap.yml:

1
2
3
4
5
6
7
8
spring:
application:
name: com.example.user
cloud:
nacos:
server-addr: 192.168.2.125:8848
username: nacos
password: nacos

其中name必须和Data Id相同,否则将读取不到配置。

启动类读取配置:

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication
public class ConfigApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(ConfigApplication.class, args);
String userName = applicationContext.getEnvironment().getProperty("user.name");
String userAge = applicationContext.getEnvironment().getProperty("user.age"); //切换为dev环境,还是可以读取默认环境的配置。是互补关系,同名配置根据优先级判断
String userConfig = applicationContext.getEnvironment().getProperty("user.config");
System.out.println("UserName: " + userName);
System.out.println("UserAge: " + userAge);
System.out.println("Config: " + userConfig);
}
}

nacos-config的原理是判断文件是否修改,修改了的文件md5值会发生变化,md5发生变化就会去拉取文件。

即先在配置文件中配置profiles:

application.yml:

1
2
3
4
5
6
server:
port: 8020

spring:
profiles:
active: dev

然后配置中心新建配置:

Nacos Config也支持Namespace和Group管理配置文件,

直接在bootstrap.yml指定相对应的dev的id,group名即可

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
application:
name: com.example.user
cloud:
nacos:
server-addr: 192.168.2.125:8848
username: nacos
password: nacos
config:
file-extension: yaml
# namespace: public 不能指定public,否则有问题,读取不到,很坑、
namespace: ccc62789-3bb8-4b58-9dab-95d5814f725d # dev的id
group: example

如何使用的自定义Data Id:

方法1,shared-configs:

在bootstrap.yml配置shared-configs即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
application:
name: com.example.user
cloud:
nacos:
server-addr: 192.168.2.125:8848
username: nacos
password: nacos
config:
file-extension: yaml
# namespace: public 不能指定public,否则有问题,读取不到,很坑、
namespace: ccc62789-3bb8-4b58-9dab-95d5814f725d # dev的id
group: example
shared-configs: # 下标越大,优先级越高
- data-id: com.example.common.properties
refresh: true # 自动感知变化
- data-id: com.example.common02.properties
refresh: true # 自动感知变化

配置中心这样写:

方法2,extension-configs:

两种方法都差不多,配置bootstrap.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
application:
name: com.example.user
cloud:
nacos:
server-addr: 192.168.2.125:8848
username: nacos
password: nacos
config:
file-extension: yaml
# namespace: public 不能指定public,否则有问题,读取不到,很坑、
namespace: ccc62789-3bb8-4b58-9dab-95d5814f725d # dev的id
group: example
shared-configs: # 下标越大,优先级越高
- data-id: com.example.common.properties
refresh: true # 自动感知变化
- data-id: com.example.common02.properties
refresh: true # 自动感知变化
extension-configs:
- data-id: com.example.common.properties
refresh: true # 自动感知变化

关于这几种方法的优先级:
优先级 profile(dev,prod..) > 默认配置文件 > extension-configs > shared-configs

如何在Controller中使用配置

和直接写在application.yml中的配置使用一样,使用@Value注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/config")
@RefreshScope //加上才可以动态感知配置文件的变化
public class ConfigController {

@Value("${user.name}")
private String userName;

@GetMapping("/show")
public String showConfig(){
return "UserName: " + userName;
}
}

但是配置变化无法感知,需要加上@RefreshScope注解,自动感知。

Sentinel的配置和使用

关于Sentinel

服务雪崩:

当一个不太重要的积分服务Down了,依赖积分服务的商品服务得不到相应,大量请求堆积,导致一系列的服务Down。

出现异常的几种形式:

如何解决服务雪崩:

  1. 限流

即控制QPS(Queries Per Second),当QPS超过了设定的阈值,则进行相应的处理。

  1. 超时机制

在不做任何处理的情况下,服务提供者不可用会导致消费者请求线程强制等待,而造成系统资源耗尽。加入超时机制,一旦超时,就释放资源。由于释放资源速度较快,一定程度上可以抑制资源耗尽的问题。

  1. 隔离

原理:用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,则会进行降级处理,用户的请求不会被阻塞,至少可以看到一个执行结果(例如返回友好的提示信息),而不是无休止的等待或者看到系统崩溃。

隔离前:

  1. 服务熔断

Sentinel的使用

下载sentinel-dashboard,配置启动项:

加入依赖:

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.1</version>
</dependency>

<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.8.1</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.1</version>
</dependency>
</dependencies>

使用代码编写流控规则:

Controller:

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
private static final String RESOURCE_NAME = "Hello";
private static final String USER_RESOURCE_NAME = "Hello";
private static final String DEGRADE_RESOURCE_NAME = "degrade";

@GetMapping("/goods")
public String getGoods() {
Entry entry = null;
try {
entry = SphU.entry(RESOURCE_NAME); //Sentinel针对资源进行限制
String str = "Hello Sentinel";
log.info("字符串: " + str);
return str;
} catch (BlockException e1) {
log.info("block!!!!!!!!");
return "当前QPS过高,被流控";
} catch (Exception ex) {
//配置降级规则之类的,可以放在这里
Tracer.traceEntry(ex, entry);
} finally {
if (entry != null) {
entry.exit();
}
}
return null;
}

@PostConstruct //初始化方法注解
private static void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();

FlowRule rule = new FlowRule();
//设置受保护资源
rule.setResource(RESOURCE_NAME);
//设置流控规则为QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置QPS阈值为1
rule.setCount(1);
rules.add(rule);

FlowRule rule2 = new FlowRule();
rule2.setResource(USER_RESOURCE_NAME);
rule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule2.setCount(1);
rules.add(rule2);

FlowRuleManager.loadRules(rules);
}

可以看到,侵入性强,不方便。

推荐使用Spring的面向切片编程:

启动类需要这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
public class SentinelDemoApplication {

public static void main(String[] args) {
SpringApplication.run(SentinelDemoApplication.class, args);
}


@Bean //controller使用@sentinelresource
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}

Controller:

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
@GetMapping("/user")
@SentinelResource(value = USER_RESOURCE_NAME, blockHandler = "blockHandlerForGetUser", fallback = "fallbackHandler") //不在同一个类,加上blockHandlerClass, static
public User getUser(String id){
//int a = 0/0;
return new User("Azusa");
}

//blockHandler优先级 > fallback

/**
* 1. 必须为public
* 2. 返回值与原方法同类型
* 3. 参数相同
* @param id
* @param ex
* @return
*/
public User blockHandlerForGetUser(String id, BlockException ex){
ex.printStackTrace();
return new User("当前QPS过高,被流控!");
}

public User fallbackHandler(String id, Throwable e){
e.printStackTrace();
return new User("ERROR");
}

加上@SentinelResource注解,写blockHandler和falllback方法。

基于QPS与线程数流控对比:

熔断降级规则编写:

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
@GetMapping("/de")
@SentinelResource(value = DEGRADE_RESOURCE_NAME, entryType = EntryType.IN, blockHandler = "blockTest")
public User degradeTest(String id){
throw new RuntimeException("异常");
}

public User blockTest(String id, BlockException ex){
ex.printStackTrace();
return new User("触发熔断");
}

@PostConstruct
public void initDegradeRule(){
List<DegradeRule> degradeRules = new ArrayList<>();
DegradeRule degradeRule = new DegradeRule();
degradeRule.setResource(DEGRADE_RESOURCE_NAME);

//降级规则:异常数
degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
degradeRule.setCount(2);
//处罚降级最小异常数
degradeRule.setMinRequestAmount(2);
//统计时长
degradeRule.setStatIntervalMs(60*1000);
//1min内,执行2次,出现2次异常,触发熔断

degradeRule.setTimeWindow(10); //熔断时长:10s,结束后,半开状态,可以请求,但第一次请求出现异常直接再次触发熔断

degradeRules.add(degradeRule);
DegradeRuleManager.loadRules(degradeRules);
}

使用Sentinel控制台进行流控

依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
</dependencies>

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 8010

spring:
application:
name: order-sentinel
cloud:
nacos:
server-addr: 192.168.2.125:8848
discovery:
username: nacos
password: nacos
namespace: public
sentinel:
transport:
dashboard: 127.0.0.1:8869

控制类:

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
@RestController
@RequestMapping("/order")
public class OrderController {

@Resource
TestService testService;

@GetMapping("/add")
public String add() {
return "下单成功";
}

@GetMapping("/flow")
//@SentinelResource(value = "flow", blockHandler = "block")
public String flow() {
return "流控测试";
}

public String block(BlockException e){
return "QPS过高";
}

@GetMapping("/test1")
public String test1(){
return testService.getUser();
}

@GetMapping("/test2")
public String test2(){
return testService.getUser();
}
}

之后,访问任意一个接口,sentinel控制台都会有相应接口:

可以直接在控制台添加流控规则,不需要写代码:

降级规则也同样可以设置:

当然,默认返回的流控信息不是很方便,同样可以使用@SentinelResource注解设置blockHandler方法。

关联流控模式:

如果访问超过了2次/order/add接口,/order/get查询订单将会被限流。

链路流控模式:

配置文件需要启用链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
application:
name: order-sentinel
cloud:
nacos:
server-addr: 192.168.2.125:8848
discovery:
username: nacos
password: nacos
namespace: public
sentinel:
transport:
dashboard: 127.0.0.1:8869
web-context-unify: false # 默认收敛调用链路

需要在ServiceImpl类中实现:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class TestServiceImpl implements TestService {
@Override
@SentinelResource(value = "getUser",blockHandler = "blockHandler")
public String getUser() {
return "返回用户";
}

public String blockHandler(BlockException e){
return "被流控!!";
}
}

不然默认返回500的错误界面。

频繁访问/order/test2,被流控,但是访问/order/test1不会被流控。

统一异常处理

统一返回结果类:

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
public class Result<T> {
private Integer code;
private String msg;
private T data;

public Result(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}

public Result(Integer code, String msg) {
this.code = code;
this.msg = msg;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

public static Result error(Integer code, String msg){
return new Result(code, msg);
}
}

统一异常类:

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
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {

Logger log = LoggerFactory.getLogger(this.getClass());

@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
log.info("BlockExceptionHandler, 触发中断: " + e.getRule());

Result r = null;
if (e instanceof FlowException) {
r = Result.error(100, "接口限流");
} else if (e instanceof DegradeException) {
r = Result.error(101, "服务降级");
} else if (e instanceof ParamFlowException) {
r = Result.error(102, "热点参数限流");
} else if (e instanceof SystemBlockException) {
r = Result.error(103, "触发系统保护规则");
} else if (e instanceof AuthorityException) {
r = Result.error(104, "授权规则不通过");
}

httpServletResponse.setStatus(500);
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(httpServletResponse.getWriter(), r);
}
}

访问接口,如果限流就会同意返回JSON:

流控效果

直接失败:

超过设定值直接返回流控信息。

Warm Up:

Jmeter压测工具使用:

300个线程,在10s内启动完毕,

新建HTTP请求,访问接口,

可以在结果树中看到访问成功的线程越来越多,逐渐到达阈值,

sentinel中也类似可以看到:

排队等待:

目的是解决脉冲流量。

效果如下:

熔断降级策略设置

注意熔断的半开状态,如果熔断并且过了熔断时长后就进入了半开状态,接下来的第一个请求如果达不到设置的值,直接熔断。

慢调用比例:

异常比例:

统计10s内,最少出现100个请求,且出现异常的比例大于0.5(50个),则进入熔断。

异常数:

同理,只不过把比例换成了实际的数值。

Sentinel 整合OpenFeign

依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-openfeign</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 8010

spring:
application:
name: order-sentinel-feign
cloud:
nacos:
server-addr: 192.168.2.125:8848
discovery:
username: nacos
password: nacos
namespace: public
sentinel:
transport:
dashboard: 127.0.0.1:8869
feign:
sentinel:
enabled: true

FeignService:

1
2
3
4
5
6
7
8
9
10
11
12
13
@FeignClient(name = "stock-service", path = "/stock", fallback = StockFeignServiceFallback.class)
public interface StockFeignService {

@RequestMapping("/deduct")
String deduct();

@RequestMapping("/error")
String errorTest();

@RequestMapping("/get/{id}")
String buy(@PathVariable("id") Integer id);

}

FeignServiceFallback类,用来降级时显示信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class StockFeignServiceFallback implements StockFeignService {
@Override
public String deduct() {
return "降级!!!!";
}

@Override
public String errorTest() {
return "降级!!!!";
}

@Override
public String buy(Integer id) {
return "降级!!!!!";
}
}

Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("/order")
public class OrderController {

@Resource
StockFeignService service;

@GetMapping("/add")
public String add() {
return "下单成功" + service.errorTest();
}

@GetMapping("/buy/{id}")
@SentinelResource(value = "getById", blockHandler = "hotBlockHandler")
public String buyGoods(@PathVariable("id") Integer id){
return "购买成功," + service.buy(id);
}

public String hotBlockHandler(@PathVariable("id") Integer id, BlockException e){
return "HotBlock";
}
}

添加限流熔断的方法和上面一模一样。

热点限流:

如果大部分是热点参数,那阈值主要针对热点参数进行流控,其他就针对普通参数进行流控。

如果大部分是普通流量,那主要就对普通流量进行设置。

当id为1时,访问超过2次就会进入限流,其他参数都是正常的。

系统保护规则

相较于上面对单个服务的流控,系统保护则是对整个机器访问的保护。

具体参数设置非常简单,超过阈值就无法访问,适用于order-service的所有接口。

使用Nacos Config 规则持久化

依赖需要添加:

1
2
3
4
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

在Nacos中添加配置:

配置文件添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: order-sentinel
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8869
web-context-unify: false #默认调用链路是收敛的,没有展开,我们需要它展开
datasource:
flow-rule:
server-addr: 192.168.2.125:8848 #nacos注册中心
username: nacos
password: nacos
dataId: order-sentinel-flow-rule
rule-type: flow

访问一次接口可以在sentinel中看到配置好的规则。

Seata配置和使用

事务概念:

本地事务:一般是单体架构,只有一个数据库。

分布式事务:

一般是处理多个数据库,并且有不同的操作,这时使用@Transactional注解没有作用,需要使用分布式事务解决方案Seata。

常见的事务解决方案都基于2PC这一概念:

即一次Prepare,等待确认,另一次Commit,等待确认。

几种分布式事务解决方案的结构:

MQ消息队列:

  1. 提交预备消息。

  2. 如果成功发出确认消息,此时会真正投递消息。

  3. 如果确认信息发送失败,MQ有回查机制一定的次数(15次)。如果回查失败,就会删除确认消息,就不会进行真正消息的投递了。

  4. 当确认消息已经确认,那么MQ会告知消费者真正去扣减库存,如果MQ没有通知成功,MQ也有重试的机制。

  5. 如果MQ最终没有重试成功的通知到消费者,此时就需要人工干预了。

Seata的配置

第一阶段:

第二阶段:

第一阶段遇到异常回滚:

Seata Server 单机部署:

下载Releases:https://github.com/seata/seata/releases

配置Seata 高可用模式(DB):

修改conf/file.conf为DB模式:

新建数据库,并导入sql脚本https://github.com/seata/seata/blob/1.4.0/script/server/db/mysql.sql

配置Seata服务为Nacos模式:

修改conf/registry.conf文件:

修改script/config-center/config.txt

1
2
3
4
5
6
7
8
9
service.vgroupMapping.my_test_tx_group=default
store.mode=db

store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=root
store.db.password=root

使用Python脚本导入配置:

此时启动seata:使用seata/bin/seata-server.bat -p 8866

Seata Client配置

一共两个服务:

  1. Order服务,调用Stock
  2. Stock服务,提供接口给Order
  3. Order的一个接口/order/add,调用时在seata_order数据库中新增一条记录,并调用Stock服务,Stock服务为更新stock_seata数据库的表中减少库存数量

准备工作:

新建两个数据库,并建好表:

undo_log就是用来存储事务中间过程的一些信息,当事务开始时,也就是第一阶段,向undo_log中插入一条记录,当收到seata对commit确认后,删除undo_log中的记录,当收到rollback时,使用undolog做交易补偿,然后删除undolog。

依赖:

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
<dependencies>
<!--nacos-服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--1. 添加openfeign依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--seata的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
</dependencies>

配置文件,order:

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
# 数据源
spring:
datasource:
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource

#初始化时运行sql脚本
schema: classpath:sql/schema.sql
initialization-mode: NEVER
application:
name: alibaba-order-seata
cloud:
nacos:
discovery:
server-addr: 192.168.2.125:8848
username: nacos
password: nacos
alibaba:
seata:
tx-service-group: my_test_tx_group #配置事务分组
# tx-service-group: guangzhou #配置事务分组

#设置mybatis
mybatis:
mapper-locations: classpath:com/tulingxueyuan/order/mapper/*Mapper.xml
#config-location: classpath:mybatis-config.xml
typeAliasesPackage: com.tulingxueyuan.order.pojo
configuration:
mapUnderscoreToCamelCase: true
server:
port: 8070
seata:
registry:
type: nacos
nacos:
server-addr: 192.168.2.125:8848
application: seata-server
username: nacos
password: nacos
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: 192.168.2.125:8848
application: seata-server
username: nacos
password: nacos
group: SEATA_GROUP

注意坑点:

必须要配置tx-service-group: my_test_tx_group,否则会一直报错:

no available service ‘null‘ found, please make sure registry config correct

OrderService类中这样写:

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
@Service
public class OrderServiceImpl implements OrderService {


@Autowired
OrderMapper orderMapper;

@Autowired
StockService stockService;

/**
* 下单
* @return
*/
@Override
@GlobalTransactional
public Order create(Order order) {
// 插入能否成功?
orderMapper.insert(order);

// 扣减库存 能否成功?
stockService.reduct(order.getProductId());

// 异常
//int a=1/0;

return order;
}
}

没有异常,先运行一遍,可以看到数据库中正常执行了语句,库存也减少了:

再人为制造一个异常,在执行一遍,可以看到网页出现500异常:

order-server:

stock-service:

可以看到更新完直接回滚,

Seata中也有相应的信息:

数据库中库存没有减少:

GateWay 网关

API网关:

所谓的API网关,就是系统的统一入口,它封装了应用哦程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,如,认证,鉴权,监控,路由转发等。

总结:网关的作用可以帮我们维护一些公共的功能,节省重复造轮子的事情,结合nacos注册中心后,还可以帮我们维护服务的地址,进行统一的路由。就像小区里的门卫。

整个结构就如下图:

Spring Cloud GateWay 相关概念:

Spring Cloud GateWay 功能特性:

  • 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建
  • 动态路由:能够匹配任何请求属性
  • 支持路径重写
  • 集成 Spring Cloud 服务发现功能(Nacos、Eruka)
  • 可集成流控降级功能( Sentinel. Hystrix)
  • 可以对路由指定易于编写的Predicate(断言)和Filter(过滤器);

核心概念:

  • 路由(route)
    路由是网关中最基础的部分,路由信息包括-个ID、一个目的URI、一组断言工厂、一组ilter组成。 如果断言为真,则说明请求的URL和配的路由匹配。
  • 断言(predicates)
    Java8中的断言函数,SpringCloud Gateway中的断言函数类型是Spring5.0框架中的ServerWebExchange.断言函数允许开发者去定义匹配Http request中的任何信息,比如请求头和参数等。
  • 过滤器(Filter)
    SpringCloud Gateway中的ilter分为Gateway Fller和Global Fiter。Flter可以对请求和响应进行处理。

Spring Cloud GateWay 使用

简单使用转发功能:

新建网关模块,设置依赖:

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 8088

spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: order_route #路由的唯一标识
uri: http://localhost:8010
predicates:
- Path=/order-serv/** #路由规则的匹配 http://localhost:8088/order-serv/order/add 转发到8011,http://localhost:8010/order-serv/order/add
filters:
- StripPrefix=1 #转发前去掉中间的路径 得到http://localhost:8010/order/add

访问8088端口,自动转发到8010,成功。

GateWay整合Nacos:

上面的基础上添加Nacos依赖:

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 8088

spring:
application:
name: api-gateway
cloud:
nacos:
server-addr: 192.168.2.125:8848
discovery:
username: nacos
password: nacos
namespace: public
gateway:
routes:
- id: order_route #路由的唯一标识
uri: lb://order-service #LoadBalance
predicates:
- Path=/order-serv/** #路由规则的匹配 http://localhost:8088/order-serv/order/add 转发到8011,http://localhost:8010/order-serv/order/add
filters:
- StripPrefix=1 #转发前去掉中间的路径 得到http://localhost:8010/order/add

一种简便方法配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 8088

spring:
application:
name: api-gateway
cloud:
nacos:
server-addr: 192.168.2.125:8848
discovery:
username: nacos
password: nacos
namespace: public
gateway:
discovery:
locator:
enabled: true #自动识别Nacos服务

这样无需配置断言,过滤器之类的,直接以服务名作为断言路径。

断言工厂

尝试:

1
2
3
4
5
6
7
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order/**
- After=2020-12-31T23:59:59.789+08:00[Asia/Shanghai]

正常访问。

设置时间晚一点,出现404。Before,Between同理。

加上验证请求头的断言:

1
2
3
4
5
6
7
8
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order/**
- After=2020-12-31T23:59:59.789+08:00[Asia/Shanghai]
- Header=X-Request-Id,\d+

加上请求头正常访问,去掉请求头404。

同样,可以加入请求类型,指定请求参数的断言,操作都一样。

自定义断言工厂:

自定义路由断言工厂需要继承AbstractRoutePredicateFactory类,重写apply方法的逻辑。在apply方法中可以通过exchange getRequest()拿倒ServerHttpRequest对象,从而可以获取到请求的参数、请求方式、请求头等信息。

要求:

  1. 必须spring组件 bean
  2. 类必须加kRoutePredicateFactory作为结尾
  3. 必须继承AbstractRoutePredicateFactory
  4. 必须声明静态内部类声明属性来接收配置文件中对应的断言的信息
  5. 需要结合shortcutFieldOrder进行绑定
  6. 通过apply进行逻辑判断true就是匹配成功false匹配失败

模仿自带的断言工厂:

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
@Component
public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> {

public CheckAuthRoutePredicateFactory() {
super(CheckAuthRoutePredicateFactory.Config.class);
}

@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("name");//会跟config进行绑定
}

public Predicate<ServerWebExchange> apply(CheckAuthRoutePredicateFactory.Config config) {
return new GatewayPredicate() {
public boolean test(ServerWebExchange exchange) {
if(config.getName().equals("Azusa")){
return true;
}
return false;
}

};
}
//接收配置文件的断言信息
@Validated
public static class Config {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
}

配置文件中加上断言:

1
2
3
4
5
6
7
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order/**
- CheckAuth=Azusa #自定义断言工厂

正常访问,修改value为其他的值,404。

过滤器工厂

自带的过滤器:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#token-relay-gatewayfilter-factory

尝试以下添加请求头的过滤器:

1
2
3
4
5
6
7
8
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- AddRequestHeader=X-Request-clor, red

在Order中添加接口来查看请求头是否设置好了:

1
2
3
4
5
@RequestMapping("/header")
public String header(@RequestHeader("X-Request-color") String color){
System.out.println("header信息接口");
return color;
}

访问header:

尝试添加前缀路径的过滤器:

order-server的设置context-path:

1
2
3
4
server:
port: 8010
servlet:
context-path: /mall

order-server必须加上路径才能访问。

在GateWay中配置PrefixPath:

1
2
3
4
5
6
7
8
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- PrefixPath=/mall

不需要加上前缀也可以访问。

尝试使用重定向过滤器:

1
2
3
4
5
6
7
8
9
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- PrefixPath=/mall
- RedirectTo=302, https://www.baidu.com/

直接重定向到百度。

自定义过滤器工厂:

和自定义断言工厂一样模仿:

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
61
62
package com.example.filters;

import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

@Component
public class CheckAuthGatewayFilterFactory extends AbstractGatewayFilterFactory<CheckAuthGatewayFilterFactory.Config> {

public CheckAuthGatewayFilterFactory() {
super(CheckAuthGatewayFilterFactory.Config.class);
}

public List<String> shortcutFieldOrder() {
return Arrays.asList("value");
}
public GatewayFilter apply(CheckAuthGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//此处如果传递需要带上name这个参数
String name = exchange.getRequest().getQueryParams().getFirst("name");
if(StringUtils.isNotBlank(name)){
if(config.getValue().equals(name)){
return chain.filter(exchange);
}else{
//返回404结束
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
return exchange.getResponse().setComplete();
}
}
//正常访问
return chain.filter(exchange);
}
};
}


public static class Config {
String value;

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}

public Config() {
}

}
}

配置文件:

1
2
3
4
5
6
7
8
9
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- PrefixPath=/mall
- CheckAuth=Azusa

成功验证,当参数与配置中不相同时,404错误。

全局过滤器

局部过滤器和全局过滤器区别:
局部:局部针对某个路由,需要在路由中进行配置
全局:针对所有路由请求,一旦定义就会投入使用

写一个类,实现GlobalFilter

1
2
3
4
5
6
7
8
9
@Slf4j
@Component
public class LogFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info(exchange.getRequest().getPath().value());
return chain.filter(exchange);
}
}

使用Reactor Netty访问日志:

设置VM:-Dreactor.netty.http.server.accessLogEnabled=true

比上面更加详细

跨域相关问题

前端页面不在同一个域中发起请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>

</head>
<body>
<button onclick="testCros()">发送信息</button>
<script>
function testCros(){
$.get("http://localhost:8088/order/add",function (res){
alert(res)
})
}

</script>
</body>
</html>

有跨域错误。

添加GateWay的允许跨域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- PrefixPath=/mall
- CheckAuth=Azusa
#跨域处理
globalcors:
cors-configurations:
'[/**]': #允许跨域的资源
allowedOrigins: "*" #允许的来源
allowedMethods:
- GET
- POST

显示结果正常。

使用配置类的方法允许跨域:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter(){
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**",config);
return new CorsWebFilter(source);
}
}

也可以正常请求。

GateWay整合Sentinel

加入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<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>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
port: 8088

spring:
application:
name: api-gateway
cloud:
nacos:
server-addr: 192.168.2.125:8848
discovery:
username: nacos
password: nacos
namespace: public
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order/**
sentinel:
transport:
dashboard: 127.0.0.1:8869

打开Sentinel,随便访问一个接口,例如http://localhost:8088/order/add

流控测试:

正常。

详细功能测试:

间隔,就是Queries Per 间隔

Burst size:指的是QPS阈值可以再允许访问的次数,比如QPS阈值为2,设置了Brust size,则表示访问4次不会流控,第五次才流控。

针对请求属性:

相当于断言工厂,只有设置的这些匹配了才会限流,

测试成功。其他配置和之前sentinel一模一样。

API分组流控:

即把不同接口统一管理,进行流控。

访问http://localhost:8088/order/addhttp://localhost:8088/order/flow 都限流,访问其他接口不限流。

降级规则设置:

和之前sentinel一样

自定义异常处理(代码):

写一个配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class GatewayConfig {
@PostConstruct
public void init(){
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
System.out.print(throwable);
HashMap<String,String> map =new HashMap<>();
map.put("code", HttpStatus.TOO_MANY_REQUESTS.toString());
map.put("message","限流了");
//自定义异常处理
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}

}

自定义异常处理(配置文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring:
application:
name: api-gateway
cloud:
nacos:
server-addr: 192.168.2.125:8848
discovery:
username: nacos
password: nacos
namespace: public
gateway:
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/order/**
sentinel:
transport:
dashboard: 127.0.0.1:8869
scg:
fallback:
mode: response
response-body: '"code":"429 TOO_MANY_REQUESTS","message":"限流了..."}'

达到同样的效果。

GateWay 高可用集群

使用nginx反向代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 负载均衡,默认是轮询策略
upstream gateway {
server localhost:5000;
server localhost:5001;
}
server {
# 监听端口
listen 80;
server_name localhost;

# 根目录
location / {
proxy_pass http://gateway;
}
}

SkyWalking

因为复杂的调用关系,我们很难通过查看日志的方式去定位问题,我们需要捋清调用的链路,每个结点的访问详细信息等等。

SkyWalking 配置

主要框架结构:

SkyWalking 部署:

下载:https://skywalking.apache.org/downloads/

修改webapp端口:webapp/webapp.yml

最新的SkyWalking还需要单独下载java-agent,

接入微服务

接入单个微服务:

只需要配置启动项:

1
2
3
-javaagent:D:\Java\skywalking-agent\skywalking-agent.jar
-DSW_AGENT_NAME=api-service
-DSW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800

分别配置java-agent路径,要显示的服务名,以及skywalking的ip:端口

接入多个微服务:

直接修改启动配置就可以了。

效果:

SkyWalking 持久化

为啥持久化:

内存占用越来越多,需要经常重启

SkyWalking配置文件修改:

config/application.yml

启动SkyWalking,各种数据就保存在数据库中。

SkyWalking 自定义链路追踪

添加依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>8.9.0</version>
</dependency>

在service中添加注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Trace //加入trace的注解也会记录到链路当中
@Tag(key="getAll",value="returnObj")
public List<OrderTbl> all() throws InterruptedException {
TimeUnit.SECONDS.sleep(2);
return orderMapper.selectList(null);
}
@Trace
@Tags({
@Tag(key="getAll",value = "returnedObj"),
@Tag(key="getAll",value="arg[0]")
})
public OrderTbl get(Integer id){
return orderMapper.selectById(id);
}

controller:

1
2
3
4
5
6
7
8
@RequestMapping("/get/{id}")
public OrderTbl get(@PathVariable Integer id){
return orderService.get(id);
}
@RequestMapping("/all")
public List<OrderTbl> getAll() throws InterruptedException {
return orderService.all();
}

访问接口,可以在skywalking中看到返回值和参数。

性能剖析

一开始是没有数据的

需要新建任务:

日志

首先加入依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>8.9.0</version>
</dependency>

新建logback-spring.xml日志配置文件在resources目录下:

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
<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。
当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!--1. 输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<Pattern>-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} [%tid] %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}</Pattern>
</layout>
</encoder>
</appender>

<!--输出到SkyWalking 网页日志上-->
<appender name="grpc-log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n</Pattern>
</layout>
</encoder>
</appender>

<root level="info">
<appender-ref ref="CONSOLE" />
<appender-ref ref="grpc-log" />
</root>

<!-- 4.2 生产环境:输出到文档
<springProfile name="pro">
<root level="info">
<appender-ref ref="CONSOLE" />
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="ERROR_FILE" />
<appender-ref ref="WARN_FILE" />
</root>
</springProfile> -->

</configuration>

其中,配置了输出到控制台的日志和输出到网页上的日志

请求接口:

点击右边的追踪ID可以直接跳到追踪所显示的页面:

如果SkyWalking部署在远程,需要修改Agent的配置:

config\agent.config:

1
2
3
4
plugin.toolkit.log.grpc.reporter.server_host=${SW_GRPC_LOG_SERVER_HOST:127.0.0.1}
plugin.toolkit.log.grpc.reporter.server_port=${SW_GRPC_LOG_SERVER_PORT:11800}
plugin.toolkit.log.grpc.reporter.max_message_size=${SW_GRPC_LOG_MAX_MESSAGE_SIZE:10485760}
plugin.toolkit.log.grpc.reporter.upstream_timeout=${SW_GRPC_LOG_GRPC_UPSTREAM_TIMEOUT:30}

告警

webhooks:

出现报警自动调用这个接口,这个接口可以接受这些参数,用Server酱之类的提醒:

config/alarm-settings.yml:

1
2
3
4
5
webhooks: #出现报警自动调用这个接口,这个接口可以接受这些参数,用Server酱之类的提醒
# - http://127.0.0.1/notify/
# - http://127.0.0.1/go-wechat/
- http://127.0.0.1:8848/notify/

编写webhook接口:

按照这个类来写:

https://github.com/apache/skywalking/blob/8.9.1/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/alarm/AlarmMessage.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Setter
@Getter
@ToString
public class AlarmMessage {
private int scopeId;
private String scope;
private String name;
private String id0;
private String id1;
private String ruleName;
private String alarmMessage;
private List<Tag> tags;
private long startTime;
private transient int period;
private transient boolean onlyAsCondition;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Setter
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class Tag {
private String key;
private String value;

@Override
public String toString() {
return key + "=" + value;
}

public static class Util {
public static List<String> toStringList(List<Tag> list) {
if (CollectionUtils.isEmpty(list)) {
return Collections.emptyList();
}
return list.stream().map(Tag::toString).collect(Collectors.toList());
}
}

Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/alarm")
public class SwAlarmController {
@RequestMapping("/receive")
public void receive(@RequestBody List<AlarmMessage> alarmList) {
System.out.println("alarm receive");
log.info("告警邮件信息已经发送。。。");
StringBuffer stringBuffer = new StringBuffer();
for (AlarmMessage alarmMessage : alarmList) {
stringBuffer.append(alarmMessage);
log.info(alarmMessage.toString());
System.out.println(alarmMessage);
}
}
}

出现告警信息:

SkyWalking高可用

接下来,配置skywalking:

config/application.yml

改为nacos,再配置nacos相关ip等

然后修改webapp的配置:

webapp/webapp.yml:

添加服务器IP即可;

最后,在启动配置中用逗号分隔不同的服务器:


Spring Cloud Alibaba学习记录
https://nanami.run/2022/03/09/SpringCloudAlibaba/
作者
Nanami
发布于
2022年3月9日
许可协议