SpringCloud(一)

微服务SpringCloud的学习笔记——由黑马微服务课程知识点总结

一、介绍

微服务拆分以后碰到的各种问题都有对应的解决方案和微服务组件,而SpringCloud框架可以说是目前Java领域最全面的微服务组件的集合了。

版本:SpringCloud3

二、RestTemplate

由Spring提供,与HttpClient相似可以方便的实现HTTP请求的发送

1、将RestTemplate注册为Bean

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

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

2、注入后使用

可以看到,利用RestTemplate发送http请求与前端ajax发送请求非常相似,都包含四部分信息:

  • ① 请求方式

  • ② 请求路径

  • ③ 请求参数

  • ④ 返回值类型

1
2
3
4
5
6
7
8
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtil.join(itemIds, ","))
);
  • 调用RestTemplate的API发送请求,常见方法有:

    • getForObject:发送Get请求并返回指定类型对象
    • PostForObject:发送Post请求并返回指定类型对象
    • put:发送PUT请求
    • delete:发送Delete请求
    • exchange:发送任意类型请求,返回ResponseEntity

三、Nacos

Nacos作为注册中心框架之一。Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用

  • 服务提供者:提供接口供其它微服务访问,比如item-service

  • 服务消费者:调用其它微服务提供的接口,比如cart-service

注册中心是协调2者的中间商

1、数据库导入

2

进入链接下载https://github.com/alibaba/nacos/blob/master/distribution/conf/mysql-schema.sql

Nacos需要存放数据到数据库中。

2、docker安装Nacos

在/root/nacos/下创建Nacos配置文件:.env文件

1

文件内容,包括数据库一系列信息:

1
2
3
4
5
6
7
8
9
PREFER_HOST_MODE=hostname
MODE=standalone
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=192.168.101.128
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
MYSQL_SERVICE_PASSWORD=123
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

进入root目录:

1
2
3
4
5
6
7
8
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim

成功安装后进入Nacos: http://虚拟机地址:8848/nacos

3、服务注册

  1. 引入依赖:

    1
    2
    3
    4
    5
    <!--nacos 服务注册发现-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  2. 在application.yml文件中配置nacos

    1
    2
    3
    4
    5
    6
    spring:
    application:
    name: item-service # 服务名称
    cloud:
    nacos:
    server-addr: 192.168.150.101:8848 # nacos地址

启动实例,进入nacos查看,发现服务注册成功,我们这次启动了2个实例

3

4、服务发现

  1. 引入依赖(同上)

    1
    2
    3
    4
    5
    <!--nacos 服务注册发现-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  2. 在application.yml文件中配置nacos

    1
    2
    3
    4
    spring:
    cloud:
    nacos:
    server-addr: 192.168.150.101:8848
  3. 使用DiscoveryClient获取Nacos中的服务

    上述配置好后,我们就可以利用Spring自带的DiscoveryClient与RestTemplate使用Naco中注册的服务了

    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
    //2.1 根据服务名称获取服务的示例列表
    List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
    if(CollUtils.isEmpty(instances)){
    return;
    }
    //2.2 手写负载均衡,从实例列表中挑选一个实例
    ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
    //获取实例地址。
    URI uri = instance.getUri();


    //2.1 利用RestTemplate发起http请求,得到http的响应
    ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
    uri+"/items?ids={ids}",
    HttpMethod.GET,
    null,
    new ParameterizedTypeReference<List<ItemDTO>>() {
    },
    Map.of("ids", CollUtils.join(itemIds, ","))
    );

    //2.2 解析响应
    if(!response.getStatusCode().is2xxSuccessful()){
    //查询失败,直接结束
    return;
    }
    List<ItemDTO> items = response.getBody();

四、OpenFeign

之前我们用Spring自带的DiscoveryClient与RestTemplate实现服务的远程调用,这样太复杂了。有没有一个组件可以方便的调用远程服务呢? OpenFeign来了。

1、引入依赖

1
2
3
4
5
6
7
8
9
10
<!--openFeign-->
<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>

2、启用OpenFeign

在要使用OpenFeign的启动类上添加**@EnableFeignClients**注解

4

3、编写OpenFeign客户端

只需要声明接口,无需实现方法。

1
2
3
4
5
6
@FeignClient("item-service")
public interface ItemClient {

@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
1
2
3
4
5
- @FeignClient("item-service"):声明服务名称
- @GetMapping:声明请求方式
- @GetMapping("/items"):声明请求路径
- @RequestParam("ids") Collection<Long> ids :声明请求参数
- List<ItemDTO>:返回值类型

有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>

4、使用FeignClient

5

就跟平常调用service接口一样的用法。FeignClient会自动帮我们完成所有操作,返回数据

5、启用连接池

连接池的主要目的是提高网络请求的效率,减少创建和销毁 TCP 连接的开销。具体作用包括:

  1. 减少连接创建和销毁的开销:每次发起 HTTP 请求时,都需要通过 TCP 建立连接。建立连接是有延迟的,尤其是在高并发的情况下,频繁的创建和销毁连接会增加系统的负担。连接池可以复用已经建立的连接,避免了重复建立连接的开销。

  2. 提高性能:连接池能够在客户端与服务器之间保持多个长连接。当多个请求需要访问同一个服务器时,OpenFeign 可以复用已有的连接,而不是每次都重新建立一个新的连接,这大大提高了性能。

  3. 控制并发和资源:连接池可以通过配置最大连接数和最大空闲连接数来控制并发连接的数量,避免连接过多导致系统资源耗尽。同时,连接池可以管理空闲连接,确保不浪费资源。

  4. 避免长时间等待:如果连接池的最大连接数已经用尽,新的请求会在一定时间内等待空闲连接。如果连接池管理得当,可以避免因等待时间过长导致的超时或性能问题。

Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:

  • HttpURLConnection:默认实现,不支持连接池

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.

  1. 引入Okhttp依赖

    1
    2
    3
    4
    5
    <!--OK http 的依赖 -->
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    </dependency>
  2. 开启连接池

    application.yml配置文件中开启Feign的连接池功能:

    1
    2
    3
    feign:
    okhttp:
    enabled: true # 开启OKHttp功能

6、最佳实践

统一编写一个包管理OpenFeign的接口,要使用OpenFeign接口的,pom.xml中导入这个包即可

6

由于默认只扫描本工程的包,导入接口包后,要配置扫描client包

  1. 声明扫描包:在启动类上注明: basePackages = “com.hmall.api.client”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @EnableFeignClients(basePackages = "com.hmall.api.client",defaultConfiguration = DefaultFeignConfig.class)
    public class CartApplication {
    public static void main(String[] args) {
    SpringApplication.run(CartApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate(){
    return new RestTemplate();
    }
    }
  2. 声明要用的FeignClient: 在启动类上声明 clients = {ItemClient.class}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @MapperScan("com.hmall.cart.mapper")
    @SpringBootApplication
    @EnableFeignClients(clients = {ItemClient.class},defaultConfiguration = DefaultFeignConfig.class)
    public class CartApplication {
    public static void main(String[] args) {
    SpringApplication.run(CartApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate(){
    return new RestTemplate();
    }
    }

    7、配置日志

OpenFeign四级日志级别,默认是NONE

  • NONE:不记录任何日志信息,这是默认值。

  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间

  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息

  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

定义一个配置类

1
2
3
4
5
6
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}
  • 局部生效:在某个FeignClient中配置,只对当前FeignClient生效

1
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
  • 全局生效:在@EnableFeignClients中配置,针对所有FeignClient生效。

1
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

五、Gateway

何为网关? 可以比喻成小区的保安,外面的人想要进来找你/传话,必须得到保安的同意,并且由保安来带路/传话。

1、引入依赖

一般来说,网关也需要依赖Nacos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

2、配置路由

application.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
27
28
29
30
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.150.101:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**

六、GateWay登录检验

在微服务架构中,不可能每个微服务都部署登录验证的代码。既然网关是所有请求的路口,那么我们可以在Gateway这里做登录验证。

1、网关过滤器

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。

  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为**Filter**)。

  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。

  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。

  5. 微服务返回结果后,再倒序执行Filterpost逻辑。

  6. 最终把响应结果返回。

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.

  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

  1. Gateway内置过滤器,通过配置文件配置,添加请求头

    ①过滤器在特定路由添加请求头

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring:
    cloud:
    gateway:
    routes:
    - id: test_route
    uri: lb://test-service
    predicates:
    -Path=/test/**
    filters:
    - AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value

    ②过滤器在全部路由添加请求头

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring:
    cloud:
    gateway:
    default-filters: # default-filters下的过滤器可以作用于所有路由
    - AddRequestHeader=key, value
    routes:
    - id: test_route
    uri: lb://test-service
    predicates:
    -Path=/test/**

2、自定义过滤器进行登录检验

实现GlobalFilter接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {


private final AuthProperties authProperties;

private final JwtTool jwtTool;

private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1、获取Request
ServerHttpRequest request = exchange.getRequest();
//2、判断是否需要做登录拦截
if(isExclude(request.getPath().toString())){
//放行
return chain.filter(exchange);
}
//3、请求头中获取token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if(headers!=null && !headers.isEmpty()){
token = headers.get(0);
}

//4、校验并解析token
Long userId = null;
try{
userId = jwtTool.parseToken(token);

}catch (UnauthorizedException e){
//拦截,设置响应状态码
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}

//5、传递用户信息
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info", userInfo))
.build();
return chain.filter(swe);
}

private boolean isExclude(String path) {
for (String pathPattern : authProperties.getExcludePaths()) {
if (antPathMatcher.match(pathPattern,path)) {
return true;
}
}
return false;
}

@Override
public int getOrder() {
return 0;
}
}

3、登录后网关将用户信息传给下游微服务

我们要做的事情有:

  • 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务

    1
    2
    3
    4
    5
    6
    //5、传递用户信息
    String userInfo = userId.toString();
    ServerWebExchange swe = exchange.mutate()
    .request(builder -> builder.header("user-info", userInfo))
    .build();
    return chain.filter(swe);
  • 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行

7

UserContext是一个使用了ThreadLocal的类,保存用户id。由于基本上每个微服务都需要得到用户Id,我们写一个单独的common工程, 需要用到的微服务引用common

当然我们需要配置类添加这个拦截器:

1
2
3
4
5
6
7
8
9
@Configuration
//对网关设置不生效
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}

由于Spring自动装配原理:默认只会扫描启动类所在的路径的类。所以我们要在resources目录下的META-INF/spring.factories文件中添加:

1
2
3
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig

启用扫描别的包

4、将用户信息在不同微服务中传递

如果一个操作存在多个微服务的依次调用。那么我们就需要在多个微服务中传递UserId。 由于我们使用OpenFeign来在不同微服务中发送请求的。 我们需要配置OpenFeign拦截器,给请求头加上userID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DefaultFeignConfig {

//...
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Long userId = UserContext.getUser();
if(userId!=null)
requestTemplate.header("user-info",userId.toString());
}
};
}
//...
}

在调用微服务的拦截器中对user-info进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserInfoInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取登录用户信息
String userInfo = request.getHeader("user-info");
//2、判断是否获取了用户
if(StrUtil.isNotBlank(userInfo)){
UserContext.setUser(Long.valueOf(userInfo));
}
//3、放行

return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.removeUser();
}
}

七、配置管理

1、问题发现

  1. Gateway网关的路由表需要改变的时候,我们必须重启网关服务。这费时费力

  2. 某些业务的配置文件写死了,每次都需要重启服务

  3. 每个服务中都有重复的配置,维护成本高

解决这些就需要统一的配置管理器服务解决,Nacos不仅仅具备注册中心功能,也具备配置管理

  • 在Nacos中添加共享配置

  • 微服务拉取配置

2、Nacos中添加公用配置

8

3、微服务拉取公共配置

由于读取Nacos配置是SpringCloud上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml。

引导阶段application.yaml未读取,根本不知道Nacos地址,就没法得知Nacos公共配置。

而SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml的文件。我们就需要配置这个文件。将Nacos地址配置进去

  1. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--nacos配置管理-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!--读取bootstrap文件-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
  2. 创建bootstrap.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    spring:
    application:
    name: cart-service # 服务名称
    profiles:
    active: dev
    cloud:
    nacos:
    server-addr: 192.168.150.101 # nacos地址
    config:
    file-extension: yaml # 文件后缀名
    shared-configs: # 共享配置
    - dataId: shared-jdbc.yaml # 共享mybatis配置
    - dataId: shared-log.yaml # 共享日志配置
    - dataId: shared-swagger.yaml # 共享日志配置
  3. 修改之前的application.yaml

    将重复内容删除

4、配置热更新

Nacos配置热更新:

  • 在Nacos中添加配置

  • 在微服务读取配置

首先我们在Nacos添加配置文件。命名格式为:

1
[服务名]-[spring.active.profile].[后缀名]

9

配置热更新:

cart-service中新建一个属性读取类:

1
2
3
4
5
6
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
public Integer maxItems;
}

在需要的地方注入这个属性读取类,然后当我们在Nacos修改配置的时候,Nacos会自动推送这个修改给对应微服务,对应微服务会读取配置的修改,然后热修改配置属性。

八、动态路由

网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,所以,我们无法利用配置热更新来实现路由更新

1、手动使用

如果希望 Nacos 推送配置变更,可以使用 Nacos 动态监听配置接口来实现。

1
public void addListener(String dataId, String group, Listener listener)

请求参数说明:

参数名 参数类型 描述
dataId string 配置 ID,保证全局唯一性,只允许英文字符和 4 种特殊字符(“.”、“:”、“-”、“_”)。不超过 256 字节。
group string 配置分组,一般是默认的DEFAULT_GROUP。
listener Listener 监听器,配置变更进入监听器的回调函数。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
// 1.创建ConfigService,连接Nacos
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2.读取配置
String content = configService.getConfig(dataId, group, 5000);
// 3.添加配置监听器
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
// 配置变更的通知处理
System.out.println("recieve1:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});

核心的步骤:

  • 创建ConfigService,目的是连接到Nacos

  • 添加配置监听器,编写配置变更的通知处理逻辑

2、自动使用(更新路由)

第一步spring-cloud-starter-alibaba-nacos-config自动装配,因ConfigService已经在com.alibaba.cloud.nacos.NacosConfigAutoConfiguration中自动创建好了

因此,只要我们拿到NacosConfigManager就等于拿到了ConfigService

第二步,编写监听器。虽然官方提供的SDK是ConfigService中的addListener,不过项目第一次启动时不仅仅需要添加监听器,也需要读取配置,因此建议使用的API是这个:

1
2
3
4
5
6
String getConfigAndSignListener(
String dataId, // 配置文件id
String group, // 配置组,走默认
long timeoutMs, // 读取配置的超时时间
Listener listener // 监听器
) throws NacosException;

更新路由要用到org.springframework.cloud.gateway.route.RouteDefinitionWriter这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.springframework.cloud.gateway.route;

import reactor.core.publisher.Mono;

/**
* @author Spencer Gibb
*/
public interface RouteDefinitionWriter {
/**
* 更新路由到路由表,如果路由id重复,则会覆盖旧的路由
*/
Mono<Void> save(Mono<RouteDefinition> route);
/**
* 根据路由id删除某个路由
*/
Mono<Void> delete(Mono<String> routeId);

}

这里更新的路由,也就是RouteDefinition,包含下列常见字段:

  • id:路由id

  • predicates:路由匹配规则

  • filters:路由过滤器

  • uri:路由目的地

格式:

1
2
3
4
5
6
7
8
9
{
"id": "item",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
}],
"filters": [],
"uri": "lb://item-service"
}

以上JSON配置就等同于:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**,/search/**
  1. 添加nacos-config、bootstrap依赖

  2. 创建并配置好bootstrap.yaml

  3. 配置路由监听器

    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
    @Slf4j
    @Component
    @RequiredArgsConstructor
    public class DynamicRouteLoader {

    private final RouteDefinitionWriter writer;
    private final NacosConfigManager nacosConfigManager;

    // 路由配置文件的id和分组
    private final String dataId = "gateway-routes.json";
    private final String group = "DEFAULT_GROUP";
    // 保存更新过的路由id
    private final Set<String> routeIds = new HashSet<>();

    @PostConstruct
    public void initRouteConfigListener() throws NacosException {
    // 1.注册监听器并首次拉取配置
    String configInfo = nacosConfigManager.getConfigService()
    .getConfigAndSignListener(dataId, group, 5000, new Listener() {
    @Override
    public Executor getExecutor() {
    return null;
    }

    @Override
    public void receiveConfigInfo(String configInfo) {
    updateConfigInfo(configInfo);
    }
    });
    // 2.首次启动时,更新一次配置
    updateConfigInfo(configInfo);
    }

    private void updateConfigInfo(String configInfo) {
    log.debug("监听到路由配置变更,{}", configInfo);
    // 1.反序列化
    List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
    // 2.更新前先清空旧路由
    // 2.1.清除旧路由
    for (String routeId : routeIds) {
    writer.delete(Mono.just(routeId)).subscribe();
    }
    routeIds.clear();
    // 2.2.判断是否有新的路由要更新
    if (CollUtils.isEmpty(routeDefinitions)) {
    // 无新路由配置,直接结束
    return;
    }
    // 3.更新路由
    routeDefinitions.forEach(routeDefinition -> {
    // 3.1.更新路由
    writer.save(Mono.just(routeDefinition)).subscribe();
    // 3.2.记录路由id,方便将来删除
    routeIds.add(routeDefinition.getId());
    });
    }
    }

    使用NacosConfigManager与nacos连接,RouteDefinitionWriter提供的接口删除和更新路由表

九、Sentinel

1、问题发现

有时候我们的微服务,会因为突然爆发式的请求导致微服务瘫痪无法访问,进一步导致使用此微服务的全部服务都是异常的,为了解决这个问题。我们引入了服务保护:Sentinel

微服务保护:

  1. 请求限流:服务故障最重要原因,就是并发太高。所以我们可以限制并发,也就是请求限流

  2. 线程隔离:当一个接口响应时间过长,会积压请求,耗尽Tomcat资源,导致同微服务的其他业务接口也无法响应。

  3. 服务熔断:线程虽然隔离了。但是接口响应时间过长或者直接崩溃还是会导致请求方卡顿,我们需要服务熔断,编写降级逻辑。

2、安装

  1. 下载sentinel

    https://github.com/alibaba/Sentinel/releases

  2. 下载完后得到jar包,cmd运行

    1
    java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

    10

    1. 进入Sentinel Dashboard 默认账号密码都是sentinel

3、微服务使用

1、引入依赖

1
2
3
4
5
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

2、配置控制台

修改application.yaml文件

1
2
3
4
5
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090 #sentinel 控制台地址

3、访问微服务的一个接口

sentinel页面就会显示这个接口

11

点击簇点资源,如果我们的接口是Restful风格: 接口路径都是统一路径,以请求方式决定进入的方法。

那么我们需要配置

1
2
3
4
5
6
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090
http-method-specify: true # 开启请求方式前缀

12

4、请求限流

在要限流的接口处点击流控,然后配置流控规则

14

  • 线程隔离 聚焦于确保线程之间的独立性,避免资源和数据的竞争和冲突。

  • 请求限流 关注于限制线程执行的速率,避免过多线程同时执行导致资源枯竭或系统过载。

5、线程隔离

限流可以降低服务器压力,尽量减少因并发流量引起的服务故障的概率,但并不能完全避免服务故障。一旦某个服务出现故障,我们必须隔离对这个服务的调用,避免发生雪崩。

有可能限流了,但是故障的接口依然用到了很多的线程,会影响其他接口的进行。这时候就需要线程隔离了。

1、OpenFeign整合Sentinel

之前都是直接对接口进行限制,现在需要对OpenFeign远程请求的接口进行限制。

修改application.yml文件,开启Feign的sentinel功能:

1
2
3
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持

修改Tomcat连接数

默认情况下SpringBoot项目的tomcat最大线程数是200,允许的最大连接是8492,单机测试很难打满。就很难测试线程隔离效果

1
2
3
4
5
6
7
server:
port: 8082
tomcat:
threads:
max: 50 # 允许的最大线程数
accept-count: 50 # 最大排队等待数量
max-connections: 100 # 允许的最大连接

2、配置

15

6、服务熔断

  1. 之前我们用线程隔离对请求进行了隔离。但是当我们对请求隔离时,返回请求失败信息。这未免有点不友好。我们希望在请求失败的时候(服务崩溃),走降级逻辑,也能返回一些假数据给用户。这样用户体验更好。

  2. 其次,如果有接口请求时间过长,我们也应该直接走配置的熔断策略。将接口熔断,走降级逻辑。

1、降级逻辑

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
@Slf4j
public class ItemClientFallbackFactory implements FallbackFactory<ItemClient> {
@Override
public ItemClient create(Throwable cause) {
return new ItemClient() {

@Override
public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
log.error("查询商品失败!",cause);
return Collections.emptyList();
}

@Override
public void deductStock(List<OrderDetailDTO> items) {
log.error("扣减商品库存失败!",cause);
throw new RuntimeException(cause);
}

@Override
public void addStock(List<OrderDetailDTO> items) {
log.error("增加商品库存失败!",cause);
throw new RuntimeException(cause);
}
};
}
}

在FeignConfig中注册成Bean

1
2
3
4
5
6
7
8
public class DefaultFeignConfig {
//...
@Bean
public ItemClientFallbackFactory itemClientFallbackFactory(){
return new ItemClientFallbackFactory();
}
//...
}

在api接口中直接使用降级逻辑: fallbackFactory = ItemClientFallbackFactory.class

1
2
3
4
5
6
7
8
9
10
@FeignClient(value = "item-service", fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
@PutMapping("/items/stock/deduct")
void deductStock(@RequestBody List<OrderDetailDTO> items);

@PutMapping("/items/stock/add")
void addStock(@RequestBody List<OrderDetailDTO> items);
}

2、服务熔断

请求服务时间较高的时候,我们可以对服务进行熔断,等到时间降低后,再关闭熔断

16

  • RT超过200毫秒的请求调用就是慢调用

  • 统计最近1000ms内的最少5次请求,如果慢调用比例不低于0.5,则触发熔断

  • 熔断持续时长20s

可以发现,每隔5秒会发送一个请求看看服务时间是否减少了。如果还是没减少,那就直接触发熔断

17

十、Seata

与数据库事务类似,分布式的事务必须保证一个事务中的业务同时成功或者失败。而Seata就是解决分布式事务的问题

  • **TC (Transaction Coordinator) -事务协调者:**维护全局和分支事务的状态,协调全局事务提交或回滚。

  • TM (Transaction Manager) - **事务管理器:**定义全局事务的范围、开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - **资源管理器:**管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

Seata的工作架构如图所示

18

可以这么理解,RM管理分支事务,TM管理全局事务,TC负责和RM和TM通讯,判断是否全局事务提交或回滚。

TM和RM是Seata客户端部分,TC则是独立的微服务,需要部署

1、部署TC服务

1、导入数据库表

19

2、准备配置文件

20

3、执行下列命令

1
2
3
4
5
6
7
8
9
docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.101.128 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
-d \
seataio/seata-server:1.5.2

2、微服务集成Seata

1、引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--统一配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

2、Nacos配置Seata共享配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
username: nacos
password: nacos
tx-service-group: hmall # 事务组名称
service:
vgroup-mapping: # 事务组与tc集群的映射关系
hmall: "default"

3、导入数据库表

导入Seata-at.sql得到undo_log表即可

3、使用分布式事务

21

在总方法头加上@GlobalTransactional注解,标记全局事务起点,将来TM就会基于这个方法判断全局事务,初始化全局事务。

4、XA模式

2

一阶段:

  • 事务协调者通知每个事务参与者执行本地事务

  • 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁

二阶段:

  • 事务协调者基于一阶段的报告来判断下一步操作

  • 如果一阶段都成功,则通知所有事务参与者,提交事务

  • 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

XA模式的优点是什么?

  • 事务的强一致性,满足ACID原则

  • 常用数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差

  • 依赖关系型数据库实现事务

24

实现步骤:

1
2
seata:
data-source-proxy-mode: XA

5、AT模式

缺弥补了XA模型中资源锁定周期过长的缺陷

23

阶段一RM的工作:

  • 注册分支事务

  • 记录undo-log(数据快照)

  • 执行业务sql并提交

  • 报告事务状态

阶段二提交时RM的工作:

  • 删除undo-log即可

阶段二回滚时RM的工作:

  • 根据undo-log恢复数据到更新前

6、总结

简述AT模式与XA模式最大的区别是什么?

  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。

  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。

  • XA模式强一致;AT模式最终一致

可见,AT模式使用起来更加简单,无业务侵入,性能更好。因此企业90%的分布式事务都可以用AT模式来解决。