Spring Cloud
Spring Cloud
RestTemplate接口调用
Spring 提供的一个用于发送 HTTP 请求的客户端工具类,在 Java 代码中方便地调用其他 HTTP 服务
示例
1.注册RestTemplate实例
1 | |
2.通过restTemplate发送http来远程调用
1 | |
Eureka注册中心
注册中心:在微服务架构中往往会有一个注册中心,每个微服务都会向注册中心去注册自己的地址及端口信息,注册中心维护着服务名称与服务实例的对应关系。每个微服务都会定时从注册中心获取服务列表,同时汇报自己的运行情况,这样当有的服务需要调用其他服务时,就可以从自己获取到的服务列表中获取实例地址进行调用
Eureka架构中的三个角色
- 服务注册中心
Eureka的服务端应用,提供服务注册和发现功能
- 服务提供者
提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。
- 服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。
搭建Eureka
创建服务端
导入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.6</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<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>在启动类上加@EnableEurekaServer注解
1
2
3
4
5
6
7@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}编写配置
1
2
3
4
5
6
7
8
9
10
11
12
13server:
port: 8081
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost #指定主机地址
client:
fetch-registry: false #指定是否要从注册中心获取服务(注册中心不需要开启)
register-with-eureka: false #指定是否要注册到注册中心(注册中心不需要开启)
server:
enable-self-preservation: false #关闭保护模式通过
localhost:8081访问注册中心界面
创建客户端
导入依赖
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<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.6</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<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>在启动类上加@EnableEurekaClient注解
编写配置
1
2
3
4
5
6
7
8
9
10
11server:
port: 8082 #运行端口号
spring:
application:
name: eureka-client #服务名称
eureka:
client:
register-with-eureka: true #注册到Eureka的注册中心
fetch-registry: true #获取注册实例列表
service-url:
defaultZone: http://localhost:8081/eureka/ #配置注册中心地址
服务发现
在注册RestTemplate上加上@LoadTemplate注解
1
2
3
4
5@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}请求的url中用服务名称替换ip
1
2
3
4
5
6
7
8
9
10
11
12
13@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//用restTemplate发送http请求查询user信息
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
// 4.返回
return order;
}
常用配置
1 | |
Ribbon负载均衡
负载均衡可以增加系统的可用性和扩展性
在注入RestTemplate时加上@LoadBalanced注解开启负载均衡


流程
- LoadBalanceInterceptor拦截器获取请求,RibbonLoadBalancerClient从url中获取服务名称
- DynamicServerListLoadBalancer根据服务名称拉取eureka服务列表
- IRule根据负载均衡策略选取一个
- RibbonLoadBalancerClient将url中的服务名称替换为ip端口,再发起请求
自定义负载均衡策略
一般使用默认策略(ZoneAvoidanceRule)
方式一(全局)
- 导入坐标
1 | |
- 注册IRule
1 | |
方式二(局部)
- 在application.yml中添加配置
1 | |
开启饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时
1 | |
常用配置
此为全局配置,局部配置要
服务名:ribbon下配置
1 | |
Gateway网关
核心功能
- 请求路由和负载均衡:
- 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
- 限流: 当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大
- Predicate(断言):指的是Java 8 的 Function Predicate。 输入类型是Spring框架中的ServerWebExchange。 这使开发人员可以匹配HTTP请求中的所有内容,例如请求头或请求参数。如果请求与断言相匹配,则进行路由;
- Filter(过滤器):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前后对请求进行修改。
基本使用
创建gateway模块

导入依赖坐标
基于
spring-boot-starter-webflux(非阻塞)。不支持spring-boot-starter-web(阻塞),不能在依赖中加入1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- nacos服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- gateway网关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 负载均衡如果没有生效,或者从网关没法访问其他服务,说明当前版本的Cloud没有包含这个依赖,需要添加上 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>编写路由配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20server:
port: 10010 #网关地址
spring:
application:
name: gateway #服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos地址
gateway:
routes: #路由配置
- id: user-service #路由id,可自取保证唯一即可
uri: lb://userservice #路由目标地址,服务注册的名字(lb: loadBalance)
predicates: #断言,根据这些路径来匹配
- Path=/user/**
- id: order-service #路由id,可自取
uri: lb://orderservice #路由目标地址(lb: loadBalance)
predicates: #断言,根据这些路径来匹配
- Path=/order/**通过网关地址访问服务
1
http://localhost:10010/user/1
跨域问题
浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决跨域问题
在gateway服务的application.yml文件中,添加下面的配置:
1 | |
流控鉴权
通过过滤器实现流量控制和登录鉴权
过滤器工厂提供的过滤器的作用都是固定的,可以通过全局过滤器来自定义过滤逻辑
限流,使用redis实现,以下为参考代码,如果并发量大,可以改用 Redis Lua 脚本,确保计数与过期操作原子性
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98import com.google.gson.Gson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
@Component
@Order(0) // 优先级高于 SecurityFilterToken(-1),防止恶意请求先打满流量
public class IPLimitFilter implements GlobalFilter {
/** 连续请求上限 */
private static final int CONTINUE_COUNTS = 3;
/** 统计时间窗口(秒) */
private static final int TIME_INTERVAL = 20;
/** 黑名单持续时间(秒) */
private static final int LIMIT_DURATION = 30;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return doLimit(exchange, chain);
}
private Mono<Void> doLimit(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求 IP
ServerHttpRequest request = exchange.getRequest();
String ip = IPUtil.getIP(request);
// 2. 拼接 Redis key
String ipCountKey = "gateway:ip:count:" + ip;
String ipBanKey = "gateway:ip:ban:" + ip;
// 3. 检查是否已被拉黑
Long ttl = redisTemplate.getExpire(ipBanKey, TimeUnit.SECONDS);
if (ttl != null && ttl > 0) {
// 在封禁期内,直接拦截
return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
}
// 4. 自增访问计数
Long count = redisTemplate.opsForValue().increment(ipCountKey);
// 5. 第一次访问时,设置过期时间窗口
if (count != null && count == 1L) {
redisTemplate.expire(ipCountKey, TIME_INTERVAL, TimeUnit.SECONDS);
}
// 6. 超出访问次数阈值,加入黑名单
if (count != null && count > CONTINUE_COUNTS) {
redisTemplate.opsForValue().set(ipBanKey, "1", LIMIT_DURATION, TimeUnit.SECONDS);
return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
}
// 7. 放行请求
return chain.filter(exchange);
}
/**
* 统一错误响应构造
*/
private Mono<Void> renderErrorMsg(ServerWebExchange exchange,
ResponseStatusEnum statusEnum) {
ServerHttpResponse response = exchange.getResponse();
// 设置响应头为 JSON
response.getHeaders().set("Content-Type", MimeTypeUtils.APPLICATION_JSON_VALUE);
// 状态码使用 429 Too Many Requests 更符合限流语义
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
// 构建返回对象
GraceJSONResult result = GraceJSONResult.exception(statusEnum);
String json = new Gson().toJson(result);
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
return response.writeWith(Mono.defer(() ->
Mono.just(response.bufferFactory().wrap(bytes)))
);
}
}登录鉴权过滤,以下为参考代码,使用redis实现,只检查了是否有token
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
84
85
86
87
88
89
90
91
92@Component
@Order(-1) // 优先级高,越小越先执行
@RefreshScope // 支持 Nacos / Config 动态刷新配置
public class SecurityFilterToken implements GlobalFilter {
/**
* 注入 RedisTemplate,用于访问 Redis
*/
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 注入免登录 URL 配置
*/
@Resource
private ExcludeUrlProperties excludeUrlProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求路径
String url = exchange.getRequest().getURI().getPath();
// 2. 获取免验证路径列表
List<String> excludeList = excludeUrlProperties.getUrls();
// 3. 检查是否命中免验证路径
if (excludeList != null && !excludeList.isEmpty()) {
for (String excludeUrl : excludeList) {
// 使用 match() 而不是 matchStart(),避免 /login123 被误放行
if (antPathMatcher.match(excludeUrl, url)) {
return chain.filter(exchange); // 放行
}
}
}
// 4. 从请求头中获取用户 ID 和 Token
String userId = exchange.getRequest().getHeaders().getFirst(HEADER_USER_ID);
String userToken = exchange.getRequest().getHeaders().getFirst(HEADER_USER_TOKEN);
// 5. 校验 Token 是否存在
if (StringUtils.isNotBlank(userId) && StringUtils.isNotBlank(userToken)) {
// 拼接 Redis 中的键
String redisKey = REDIS_USER_TOKEN + ":" + userId;
// 从 Redis 获取 Token(使用 RedisTemplate)
String redisToken = redisTemplate.opsForValue().get(redisKey);
// 6. 校验 Token 是否匹配
if (StringUtils.isNotBlank(redisToken) && redisToken.equals(userToken)) {
// 校验成功,放行
return chain.filter(exchange);
}
}
// 7. 未登录或 Token 不匹配,返回错误响应
return renderErrorMsg(exchange, ResponseStatusEnum.UN_LOGIN);
}
/**
* 构造统一错误响应
*
* @param exchange 当前请求交换器
* @param statusEnum 状态码枚举(自定义枚举类)
* @return Mono<Void> 异步返回
*/
public Mono<Void> renderErrorMsg(ServerWebExchange exchange,
ResponseStatusEnum statusEnum) {
// 1. 获取响应对象
ServerHttpResponse response = exchange.getResponse();
// 2. 设置响应头为 JSON 类型(强制覆盖)
response.getHeaders().set("Content-Type", MimeTypeUtils.APPLICATION_JSON_VALUE);
// 3. 设置 HTTP 状态码为 401(未授权)
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 4. 封装返回的 JSON 对象
GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum);
String resultJson = new Gson().toJson(jsonResult);
// 5. 写入响应内容
byte[] bytes = resultJson.getBytes(StandardCharsets.UTF_8);
return response.writeWith(Mono.defer(() ->
Mono.just(response.bufferFactory().wrap(bytes)))
);
}
}
过滤器执行顺序:
- order值越小,优先级越高
- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
- 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。
访问条件
通过断言来设置访问条件
通过predicates:后加断言来设置访问条件
| 名称 | 说明 | 示例 |
|---|---|---|
| After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
| Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
| Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
| Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
| Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
| Host | 请求必须是访问某个host(域名) | - Host=.somehost.org,.anotherhost.org |
| Method | 请求方式必须是指定方式 | - Method=GET,POST |
| Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
| Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
| RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
| Weight | 权重处理 |
修改请求/响应
通过过滤器工厂,路由过滤器允许以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应
添加过滤器(针对某个服务)
1
2
3
4
5
6
7
8
9
10
11
12gateway:
routes: #路由配置
- id: user-service #路由id
uri: lb://userservice #路由目标地址(lb: loadBalance)
predicates: #断言,根据这些路径来匹配
- Path=/user/**
filters:
- AddRequestHeader=Truth, aaa # 添加请求头
- id: order-service #路由id
uri: lb://orderservice #路由目标地址(lb: loadBalance)
predicates: #断言,根据这些路径来匹配
- Path=/order/**添加默认过滤器(对所有服务都有效)
1
2
3
4
5
6
7
8
9
10
11
12
13
14gateway:
routes: #路由配置
- id: user-service #路由id
uri: lb://userservice #路由目标地址(lb: loadBalance)
predicates: #断言,根据这些路径来匹配
- Path=/user/**
filters:
- AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头
- id: order-service #路由id
uri: lb://orderservice #路由目标地址(lb: loadBalance)
predicates: #断言,根据这些路径来匹配
- Path=/order/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, aaa
Spring Cloud Alibaba
Nacos服务管理
版本: v3.1.0
官网:https://nacos.io/docs/latest/quickstart/quick-start
安装
Windows
要求本地JDK 17+
从github仓库下载压缩包
nacos-server-$version.zip,解压bin目录下运行
startup.cmd,单机模式非集群1
startup.cmd -m standalone第一次启动会要求设置
也可在
conf/application.properties中设置1
2
3
4
5# Server端之间 Inner API的身份标识
nacos.core.auth.server.identity.key
nacos.core.auth.server.identity.value
# 用于生成JWT Token的密钥,使用长度大于32字符的字符串,再经过Base64编码
nacos.core.auth.plugin.nacos.token.secret.key访问
http://127.0.0.1:8080/index.html,会要求设置密码通过双击
shutdown.cmd关闭nacos
Linux
启动命令为sh startup.sh -m standalone,其他与Windows类似
Docker
从docker hub下载镜像
执行命令,使用docker desktop的话改成全在一行
1
2
3
4
5
6
7
8
9docker run --name nacos-standalone-derby \
-e MODE=standalone \
-e NACOS_AUTH_TOKEN=${your_nacos_auth_secret_token} \
-e NACOS_AUTH_IDENTITY_KEY=${your_nacos_server_identity_key} \
-e NACOS_AUTH_IDENTITY_VALUE=${your_nacos_server_identity_value} \
-p 8080:8080 \
-p 8848:8848 \
-p 9848:9848 \
-d nacos/nacos-server:latest- 8080: Nacos Web 控制台
- 8848: Nacos Server 与客户端交互的主入口
- 9848: gRPC 通信端口
访问
http://127.0.0.1:8080/index.html,会要求设置密码
配置管理
基本使用
在nacos中添加配置
项目的核心配置,需要热更新的配置才有放到nacos管理的必要


从nacos中拉取配置
父模块导入依赖
查看SpringCloud和SpringBoot对应的版本:https://spring.io/projects/spring-cloud
1
2
3
4
5
6
7
8
9
10
11
12
13<!-- 依赖管理 -->
<!-- scope=import 的作用不是引入具体的 JAR 包,而是引入这个 POM 中的 <dependencyManagement> 配置。 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2023.0.3.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>子模块导入配置管理坐标
1
2
3
4
5<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>往
application.yml添加配置信息bootstrap.yml以及bootstrap.properties已不推荐使用,请使用application.yml或者application.properties1
2
3
4
5
6
7
8
9
10
11
12
13
14
15spring:
cloud:
nacos:
serverAddr: 127.0.0.1:8848 # 注意访问控制台时端口是8080,这里是8848
config:
import:
- nacos:nacos-config-example.yml?refreshEnabled=true
server:
port: 18084
management:
endpoints:
web:
exposure:
include: "*"- nacos-config-example.yml为配置的Data Id
通过@Value注解来读取nacos中的配置信息
1
2@Value("${pattern.dataformat}")
private String dataformat;
读取配置方式
Nacos中配置nacos-config-example.yml为
1 | |
通过
@Value注解读取在类上加
@RefreshScope可以热更新1
2@Value("${pattern.dataformat}")
private String pattern;通过配置类读取
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@Configuration
@RefreshScope // 热更新
@ConfigurationProperties(prefix = "example")
public class ExampleConfig {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "ExampleConfig{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
多环境配置
显示添加配置
1 | |
只要修改spring.profiles.active的值,就能切换不同环境的配置
如上面的配置就会加载basic-service-develop.yml
命名空间
Namespace 的常用场景之一是不同环境下配置的区分隔离
使用
创建命名空间并添加配置
在配置文件中指定命名空间id,就能读取到不同命名空间下的配置
1
2
3
4
5
6spring:
cloud:
nacos:
config:
# 注意是命名空间的id,默认值为public
namespace: f9983feb-a567-4113-8f6c-2ab242c3223f
服务注册与发现
服务注册
在 Spring Cloud 应用的启动阶段,监听了 WebServerInitializedEvent 事件,当 Web 容器初始化完成后,即收到 WebServerInitializedEvent 事件后,会触发注册的动作,调用 ServiceRegistry 的 register 方法,将服务注册到 Nacos Server。
项目中导入依赖
1
2
3
4
5<!--nacos服务发现-->
<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
20spring:
application:
name: basic-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
config:
import:
- nacos:nacos-config-example.yml?refreshEnabled=true
server:
port: 18084
management:
endpoints:
web:
exposure:
include: "*"在启动类上加注解
@EnableDiscoveryClient
Nacos搭建集群
通过nginx反向代理多个nacos实现负载均衡

步骤:
- 搭建数据库
- 配置nacos
- 配置nginx反向代理
配置nacos
进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf
添加nacos集群的ip和端口
1
2
3127.0.0.1:8845
127.0.0.1:8846
127.0.0.1:8847修改配置文件application.properties,添加数据库信息
1
2
3
4
5
6
7
8spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123在application.properties中修改每个nacos的端口
分别启动nacos
1
startup.cmd
配置nginx反向代理
下载解压nginx到非中文目录下
修改conf/nginx.conf文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14upstream nacos-cluster {
server 127.0.0.1:8845; #nacos的ip和端口
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}
server {
listen 80;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}代码中nacos的地址要改为nginx的
1
2
3
4spring:
cloud:
nacos:
server-addr: localhost:80 # Nacos地址
配置集群

修改配置
1
2
3
4
5
6spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
同集群优先的负载均衡
Nacos中提供了一个
NacosRule的实现,可以优先从同集群中挑选实例
修改配置文件application.yml
1
2
3userservice: #服务名称
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
权重优先的负载均衡
权重越高,优先级越高

- 通过编辑来设置权重
设置实例类型
实例分为临时实例和非临时实例
临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型
非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例
设置为非临时实例
1 | |
Feign服务消费
Feign是声明式的服务调用工具,我们只需创建一个接口并用注解的方式来配置它,就可以实现对某个服务接口的调用,简化了直接使用RestTemplate来调用服务接口的开发量
实例
服务提供方
导入坐标
1
2
3
4<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>配置
1
2
3
4spring:
application:
name: user-service # 调用方会通过服务名来调用
# ...Controller类
1
2
3
4
5
6
7
8
9@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/hello/{name}")
public String hello(@PathVariable("name") String name) {
return "hello " + name;
}
}
服务调用方
导入坐标
1
2
3
4
5
6
7
8
9
10
11<!-- feign client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>启动类上加入@EnableFeignClients注解开启Feign的功能
1
@EnableFeignClients配置一个Feign Client
1
2
3
4
5@FeignClient(name = "user-service")
public interface FeignService {
@GetMapping(value = "/user/hello/{name}")
String hello(@PathVariable("name") String name);
}注入Feign Client来使用
1
2
3
4
5
6
7
8
9
10
11@RestController
public class BasicServiceController {
@Resource
private FeignService feignService;
@GetMapping("/test/{str}")
public String test(@PathVariable String str) {
return feignService.hello(str);
}
}
自定义配置
| 类型 | 作用 | 说明 |
|---|---|---|
| feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
| feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
| feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
| feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
| feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
配置方式:
配置文件方式
1
2
3
4
5feign:
client:
config:
userservice: # 针对某个服务, 不加针对所有服务
loggerLevel: FULL # 日志等级代码方式
1
2
3
4
5
6
7public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLevel() {
return Logger.Level.BASIC;
}
}通过注入Bean的方式来自定义配置
1
2
3
4
5
6//如果要所有服务生效,则加在@EnableFeignClient上
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
//如果只要某个服务生效,则加在指定的feignClient上
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)
服务降级
降级指系统将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能。降级的核心思想就是丢车保帅,优先保证核心业务
优化性能
可以通过一下两点优化性能:
- 日志级别尽量用basic及以下
- 使用HttpClient或OKHttp代替URLConnection
替换连接池
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
•URLConnection:默认实现,不支持连接池
•Apache HttpClient :支持连接池
•OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。
导入依赖坐标
1
2
3
4
5<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>配置使用的连接池
1
2
3
4
5
6
7
8
9feign:
client:
config:
userservice: # 针对某个服务
loggerLevel: BASIC # 日志等级
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
抽取Feign客户端
将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
降低代码冗余

创建新模块feign-api
将FeignClient及feign配置类、实体类移到feign-api中

在需要feign的类中导入feign-api模块

在@EnableFeignClients注解中说明feign所在的包或类
1
2
3
4
5@EnableFeignClients(clients = {UserClient.class})
//或者
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
常用配置
1 | |
SchedulingTasks分布式定时任务
防止定时任务被多个节点重复执行
使用MySQL作为分布式锁
建表
1
2
3
4
5
6
7CREATE TABLE `shedlock` (
`NAME` varchar(64) NOT NULL,
`lock_until` timestamp(3) NOT NULL,
`locked_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`locked_by` varchar(255) NOT NULL,
PRIMARY KEY (`NAME`)
);添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<!-- 分布式任务需要的锁 -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>6.9.2</version>
</dependency>
<!-- shedlock的JDBC实现方式 -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>6.9.2</version>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>创建
lockProvider的Bean1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* 分布式定时任务锁
*/
@Configuration
public class ScheduledLockConfig {
// 数据库连接池对象
@Resource
private DataSource dataSource;
@Bean
public LockProvider lockProvider() {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
// 指定锁的持久化方式是通过 JdbcTemplate 操作数据库
.withJdbcTemplate(new JdbcTemplate(dataSource))
// 指定数据库中存储锁时间使用 UTC 时间,防止时区差异导致锁失效时间计算错误
.withTimeZone(TimeZone.getTimeZone("UTC"))
.build()
);
}
}创建定时任务
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 SpringJob {
/**
* 每5分钟跑一次
*
* cron表达式:秒、分、时、日、月、星期、年
* name: 锁的唯一名称
* lockAtMostFor: 最长持锁时间。如果任务卡死没释放锁,ShedLock会在2分钟后自动释放,避免锁永远卡死
* lockAtLeastFor: 最短持锁时间。即使任务很快执行完,也至少保留锁1分钟,防止任务立即被另一个实例重复触发
*/
@Scheduled(cron = "0 */5 * * * ?")
@SchedulerLock(name = "SpringJob.job1", lockAtMostFor = "2m", lockAtLeastFor = "1m")
public void job1() {
System.out.println("time=" + DateTime.now().toString("YYYY-MM-dd HH:mm:ss") + " do job1...");
}
/**
* 每5秒跑一次
*/
@Scheduled(fixedRate = 5000)
@SchedulerLock(name = "SpringJob.job2", lockAtMostFor = "4s", lockAtLeastFor = "4s")
public void job2() {
System.out.println("time=" + DateTime.now().toString("YYYY-MM-dd HH:mm:ss") + " do job2...");
}
/**
* 上次跑完之后隔5秒再跑
* @throws InterruptedException
*/
@Scheduled(fixedDelay = 5000)
@SchedulerLock(name = "SpringJob.job3", lockAtMostFor = "4s", lockAtLeastFor = "4s")
public void job3() throws InterruptedException {
System.out.println("time=" + DateTime.now().toString("YYYY-MM-dd HH:mm:ss") + " do job3...");
Thread.sleep(10000);
}
}开启定时任务
1
2
3
4
5
6
7
8
9@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
// 开启定时任务
@EnableScheduling
// 开启分布式定时任务锁
@EnableSchedulerLock(defaultLockAtMostFor = "3m")
public class BasicServiceApplication {
...
Sentinel限流
Sentinel控制台
下载运行
JDK版本:1.8及以上
运行jar包
1
java -Dserver.port=18080 -Dcsp.sentinel.dashboard.server=localhost:18080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar-Dserver.port:应用启动的端口号-Dcsp.sentinel.dashboard.server=localhost:Sentinel 客户端要连接的控制台服务器地址-Dproject.name:当前运行项目的名称-Dsentinel.dashboard.auth.username:控制台账号-Dsentinel.dashboard.auth.password:控制台密码
账号密码默认为
sentinel
基本使用
引入依赖
1
2
3
4<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>添加配置
1
2
3
4
5
6
7
8spring:
cloud:
sentinel:
transport:
# 会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互
port: 8720
# sentinel控制台ip端口
dashboard: 127.0.0.1:18080定义资源
1
2
3
4
5
6
7
8@Service
public class TestService {
@SentinelResource(value = "sayHello")
public String sayHello(String name) {
return "Hello, " + name;
}
}使用
1
2
3
4
5
6
7
8
9
10
11@RestController
public class TestController {
@Autowired
private TestService service;
@GetMapping(value = "/hello/{name}")
public String apiHello(@PathVariable String name) {
return service.sayHello(name);
}
}需要访问资源后,才可在Sentinel控制台查看到
定义资源
通过@SentinelResource注解来定义资源
参数:
value:资源名blockHandler:被限流时调用的方法。方法必须为public,和原函数在同一个类,返回值和参数与原函数一致,还有多一个BlockException类型的变量fallback:抛出异常时被调用的方法。方法必须为public,和原函数在同一个类,返回值和参数与原函数一致,还有多一个Throwable类型的变量exceptionsToIgnore(since 1.6.0):指定哪些方法不会触发fallback
例子
1 | |
流量控制
实时统计信息
通过http://localhost:服务端口/cnode?id=资源名查看实时统计信息
- thread: 代表当前处理该资源的线程数;
- pass: 代表一秒内到来到的请求;
- blocked: 代表一秒内被流量控制的请求数量;
- success: 代表一秒内成功处理完的请求;
- total: 代表到一秒内到来的请求以及被阻止的请求总和;
- RT: 代表一秒内该资源的平均响应时间;
- 1m-pass: 一分钟内到来的请求;
- 1m-block: 一分钟内被阻止的请求;
- 1m-all: 一分钟内到来的请求和被阻止的请求的总和;
- exception: 一秒内业务本身异常的总和。
控制方式
并发线程数
用于保护业务线程数不被耗尽。流控效果只有直接拒接请求
QPS
QPS 超过某个阈值的时候,则采取措施进行流量控制。
流控效果有:
- 直接拒绝:默认的流量控制方式。当QPS超过任意规则的阈值后,新的请求就会被立即拒绝
- Warm Up(冷启动):让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。
- 排队等待:控制了请求通过的间隔时间,让请求以均匀的速度通过。主要用于处理间隔性突发的流量
熔断降级
如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
作用是异常达到设置的阈值后,该资源变得不可调用,防止调用链路中某个不稳定的服务,导致整个链路不可用
熔断策略
黑体为可自己设置的部分
因为流量控制产生的异常不算在内
- 慢调用比例:需要设置RT(最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断
- 异常比例:当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求成功完成则结束熔断
- 异常数 :当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求成功完成则结束熔断
热点规则
看做特殊的流量控制,仅对包含热点参数的资源生效。
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流
可设置参数索引、单机阈值及统计窗口时长,和流量控制的使用类似
配置持久化
如果在Sentinel DashBoard中添加的配置,每当Sentinel或服务重启后,配置都会消失
因此若要持久化,则需要通过外部来保存配置
基于Nacos
添加依赖
1
2
3
4
5<!-- 以Nacos作为Sentinel配置的数据源 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>添加配置
1
2
3
4
5
6
7
8
9
10
11spring:
cloud:
sentinel:
datasource:
nacos-flow-rule: # 自定义数据源名称
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
namespace: f9983feb-a567-4113-8f6c-2ab242c3223f # 命名空间id
dataId: sentinel-flow-sayHello-rule # nacos创建的配置DataId
data-type: json # 数据类型
rule-type: flow # 规则类型- rule-type取值
- flow:限流规则
- degrade:熔断降级规则
- authority:授权规则
- param-flow:热点参数限流
- system:系统保护规则
- gw-flow:网关限流规则
- gw-api-group:网关自定义分组规则
- rule-type取值
nacos中添加配置
Data ID:sentinel-flow-sayHello-rule
1
2
3
4
5
6
7
8
9
10[
{
"resource": "sayHello",
"controlBehavior": 0,
"count": 300,
"grade": 1,
"limitApp": "default",
"strategy": 0
}
]resource:资源名,即限流规则的作用对象controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)count: 限流阈值grade: 限流阈值类型(QPS 或并发线程数)limitApp: 流控针对的调用来源,若为default则不区分调用来源strategy: 调用关系限流策略
要找配置项,可以去官网找对应的名字
在Sentinel DashBoard中可以看到同步的配置。修改Nacos中的配置可以同步修改控制台里的配置,但是从控制台修改不会同步到Nacos