Bootstrap

SpringCloud完整教程

一下内容为本人在听黑马程序员的课程时整理的

  • 微服务技术栈在这里插入图片描述

在这里插入图片描述

⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝

1、微服务框架

1.1、认识微服务

1.1.1、服务架构演变

**单体架构:**将业务的所有功能集中在一个项目中开发,打包成一个包部署

优点:

  • 架构简单
  • 部署成本低

缺点:

  • 耦合度高

**分布式架构:**根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务

优点:

  • 降低服务耦合
  • 有利于服务升级扩展

服务治理:

分布式架构要考虑的问题:

  • 服务拆分力度如何?
  • 服务集群地址如何维护?
  • 服务之间如何实现远程调用?
  • 服务健康如何感知?

微服务

微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
  • 面向服务:微服务对外暴露业务接口
  • 自治:团队独立、技术独立、数据独立、部署独立
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

微服务结构:

微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo

在这里插入图片描述

在这里插入图片描述

1.1.2、SpringCloud

SpringCloud

  • SpringCloud是目前国内使用最广泛的微服务框架
  • SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验

在这里插入图片描述

  • SpringCloud与SpringBoot的版本兼容关系如下:

在这里插入图片描述

1.2、服务拆分即远程调用

服务拆分注意事项:

  1. 不同微服务,不要重复开发相同业务
  2. 微服务数据独立,不要访问其它微服务的数据库
  3. 微服务可以将自己的业务暴露为接口,供其它服务调用

工程结构有两种:

  • 独立Project
  • Maven聚合

案例:拆分服务

  • 将hm-service中与商品管理相关功能拆分到一个微服务module中,命名为item-service
  • 将hm-service中与购物车有关的功能拆分到一个微服务module中,命名为cart-service

远程调用

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=C%3A%5CUsers%5C13478%5CDesktop%5C%E8%87%AA%E5%AD%A6%E6%88%90%E6%89%8D%5CJavaWeb%5C%E5%BE%AE%E6%9C%8D%E5%8A%A1%E5%BC%在这里插入图片描述
Spring给我们提供了一个RestTemplate工具,可以方便的实现Http请求的发送。使用步骤如下:

1、注入RestTemplate到Spring容器

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

2、发起远程调用

public <T> ResponseEntity<T> exchange(
	Sring url, //请求路径
    HttpMethod method, //请求方式
    @Nullable HttpEntity<?> requestEntity, //请求实体,可以为空
    Class<T> responseType, //返回值类型
    Map<String,?> urlVariables //请求参数
)

1.3、服务治理

1.3.1、注册中心

服务治理中的三个角色分别是什么?

  • 服务提供者:暴露服务接口,供其它服务调用
  • 服务消费者:调用其他服务提供的接口
  • 注册中心:记录并监控微服务各实例状态,推送服务变更信息

在这里插入图片描述

消费者如何知道提供者的地址号?

  • 服务提供者会在启动时注册自己信息到注册中心,消费者可以从注册中心订阅和拉取服务信息

消费者如何得知服务状态变更?

  • 服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者

当提供者有多个实例时,消费者应该选择哪一个?

  • 消费者可以通过负载均衡算法,从多个实例中选择一个
1.3.2、Nacos注册中心

Nacos是目前国内企业中占比最多的注册中心组件。它是阿里巴巴的产品,目前已经加入SpringCloudAlibaba中

在这里插入图片描述

部署Nacos:‍‌‬⁠⁠‌‌⁠‍⁠‍‌‌‍‌‬‌day03-微服务01 - 飞书云文档 (feishu.cn)

1.3.3、服务注册

服务注册步骤:

1、引入nacos discovery依赖:

<!--nacos 服务注册发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2、配置nacos

cloud:
nacos:
  discovery:
    server-addr: 192.168.88.132:8848
1.3.4、服务发现

消费者需要连接nacos以拉取和订阅服务,因此服务发现的前两步与服务注册时一样的,后面再加上服务调用即可:

  1. 引入nacos discovery依赖
  2. 配置nacos地址
  3. 服务发现
private final DiscoveryClient discoveryClient;

private void handleCartItems(List<CartVO> vos){
    //1.根据服务名称,拉取服务的实例列表
    List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
    //2.负载均衡,挑选一个实例
    ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
    //3.获取实例的IP和端口
    URU uri = instance.getUri();
    //....
}

1.4、OpenFeign

1.4.1、快速入 门

我们利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了:

而且这种调用方式,与原本的本地方法调用差异太大,编程时的体验也不统一,一会儿远程调用,一会儿本地调用。

因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单。而这就要用到OpenFeign组件了。

其实远程调用的关键点就在于四个:

  • 请求方式
  • 请求路径
  • 请求参数
  • 返回值类型

OpenFeign是一个声明式的http客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来的

其作用就是基于SpringMVC的常见注解,帮我们优雅的实现http请求的发送

OpenFeign已经被SpringCloud自动装配,实现非常简单

1、引入依赖

  <!--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、通过@EnableFeignClients注解,启用OpenFeign功能

@EnabeleFeignClients
@SpringBootApplication
public class CartApplication{  /....}

3、编写FeignClient

@FeignClient("item-service")
public interface ItemClient {

    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}

4、使用FeignClient,实现远程调用

   private void handleCartItems(List<CartVO> vos) {
        // 1.获取商品id
        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        //2.查询商品
        List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
        if (CollUtils.isEmpty(items)){
            return;
        }
        // 3.转为 id 到 item的map
        Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
        // 4.写入vo
        for (CartVO v : vos) {
            ItemDTO item = itemMap.get(v.getItemId());
            if (item == null) {
                continue;
            }
            v.setNewPrice(item.getPrice());
            v.setStatus(item.getStatus());
            v.setStock(item.getStock());
        }
    }
1.4.2、连接池

OpenFeign对Http请求做了优雅的伪装,不过其底层发起http请求,依赖于其他的框架。这些框架可以自己选择,包括一下三种:

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

OpenFeign整合OKHttp的步骤:

1、引入依赖

<!--OK http 的依赖 -->
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
</dependency>

2、开启连接池功能

feign:
  okhttp:
    enabled: true # 开启OKHttp功能
1.4.3、最佳实践

当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种解决方法:

方式一:指定FeignClient所在的包

@EnableFeignClients(basePackages = "com.hmall.api.client")

方式二:指定FeignClient字码节

@EnableFeignClients(clients={UserClient.class})
1.4.4、日志

OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:

  • NONE:不记录任何日志信息,这是默认值
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据

由于Feign默认的日志级别就是NONE,所以默认我们看不到请求日志

要自定义级别需要声明一个类型为Logger.Level的Bean,在其中定义日志级别:

public class DefaultFeignConfig{
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.FULL;
    }
}

但此时这个Bean并未生效,想要配置某个FeignClient的日志,可以在@FeignClient注解中声明

@FeignClient(value = "item-service",configuration = DefaultFeignConfig.class)

如果想要全局配置,让所有的FeignClient都按照这个日志配置,则需要再@EnableFeignClients注解中声明:

@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

2、微服务-网关及配置管理

2.1、网关

网关就是网络的开关,负责请求的路由、转发、身份校验

在这里插入图片描述

在SpringCloud中网关的实现包括两种:

1、Spring Cloud Gateway

  • Spring官方出品
  • 基于WebFlux响应式编程
  • 无需调优即可获得优异性能

2、Netfilx Zuul

  • Netfilx出品
  • 基于Servlet的阻塞式编程
  • 需要调优才能获得与SpringCloudGateway类似的性能

2.2、网关路由

2.2.1、快速入门

1、创建新模块

2、引入网关依赖

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hmall</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>hm-gateway</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--网关-->
        <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>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3、编写启动类

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

4、配置路由规则

在这里插入图片描述

server:
  port: 8080
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.88.132: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/**

2.2.2、路由属性

网关路由对应的Java类型是RouteDefinition,其中常见的属性有:

  • id:路由唯一标示
  • uri:路由目标地址
  • predicates:路由断言,判断请求是否符合当前路由
  • filters:路由过滤器,对请求或响应做特殊处理

Spring提供了12种基本的RoutePredicateFactory实现:

路由过滤器

名称说明示例
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权重处理

2.3、网关登录校验

在这里插入图片描述

网关请求处理流程

在这里插入图片描述

2.3.1、自定义过滤器

网关过滤器有两种,分别是:

  • GatewayFilter:路由过滤器,作用于任意指定的路由;默认不生效,要配置到路由后生效
  • GlobalFilter:全局过滤器,作用范围是所有路由;声明后自动生效

两种过滤器的过滤方法签名完全一致

GlobalFilter

自定义GlobalFilter比较简单,直接实现GlobalFilter接口即可

@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // TODO 模拟登录校验逻辑
        ServerHttpRequest request = (ServerHttpRequest) exchange.getRequest();
        HttpHeaders headers = request.getHeaders();
        System.out.println("headers =" + headers);
        //放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
2.3.2、实现登录校验

需求:在网关中基于过滤器实现登录校验功能

package com.hmall.gateway.filters;

import com.hmall.common.exception.UnauthorizedException;
import com.hmall.common.utils.CollUtils;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private final JwtTool jwtTool;

    private final AuthProperties authProperties;

    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 (!CollUtils.isEmpty(headers)) {
            token = headers.get(0);
        }
        // 4.校验并解析token
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            // 如果无效,拦截
            ServerHttpResponse response = exchange.getResponse();
            response.setRawStatusCode(401);
            return response.setComplete();
        }

        // TODO 5.如果有效,传递用户信息
        System.out.println("userId = " + userId);
        // 6.放行
        return chain.filter(exchange);
    }

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

    @Override
    public int getOrder() {
        return 0;
    }
}
2.3.3、网关传递用户

在这里插入图片描述

需求:修改gateway模块中的登录校验拦截器,在校验成功后保存到下游请求头中。

提示:要修改转发到微服务的请求,需要用到ServerWebExchange类提供的API,示例如下:

exchange.mutate() //mutate就是对下游请求做更改
    .request(builder->builder.header("user-info",userInfo))
    .build();

需求:由于每个微服务都可能有获取登录用户的需求,因此我们直接在hm-common模块定义拦截器,这样微服务只需要引入依赖即可生效,无需重复编写

首先,修改登录校验拦截器的处理逻辑,保存用户信息到请求头中:

// TODO 5.如果有效,传递用户信息
String userInfo = userId.toString();
ServerWebExchange ex = exchange.mutate()
        .request(builder -> builder.header("user-info",userInfo))
        .build();

UserInfoInterceptor

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、判断是否获取了用户,如果有,存入ThreadLocal
        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();
    }
}

MvcConfig

@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是com.hmall.common.config,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。

基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件中:

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

之前我们无法获取登录用户,所以把购物车服务的登录用户写死了,现在需要恢复到原来的样子。

找到cart-service模块的com.hmall.cart.service.impl.CartServiceImpl

    @Override
    public List<CartVO> queryMyCarts() {
        // 1.查询我的购物车列表
        List<Cart> carts = lambdaQuery().eq(Cart::getUserId, UserContext.getUser()).list();
        if (CollUtils.isEmpty(carts)) {
            return CollUtils.emptyList();
        }

        // 2.转换VO
        List<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);

        // 3.处理VO中的商品信息
        handleCartItems(vos);

        // 4.返回
        return vos;
    }
2.3.4、OpenFeign传递用户

微服务项目中的很多业务要多个微服务共同合作完成,而这个过程也需要传递登录用户信息,例如:

在这里插入图片描述

OPenFeign中提供了一个拦截器接口,所有有OPenFeign发起的请求都会先调用拦截器处理请求

public class DefaultFeignConfig {

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

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

}

2.4、配置管理

  • 微服务重复配置过多,维护成本高
  • 业务配置经常变动,每次都要重启服务
  • 网关路由配置写死,如果变更要重启网关

在这里插入图片描述

2.4.1、配置共享

添加一些共享配置到Nacos中,包括Jdbc,MybatisPlus、日志、Swagger、OPenFeign等配置

在这里插入图片描述

2.4.2、拉取共享配置

基于NacosConfig拉取共享配置代替微服务的本地配置

在这里插入图片描述

1、引入依赖

  <!--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

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 # 共享日志配置
2.4.3、配置热更新

配置热更新:当修改配置文件中的配置时,微服务无需重启即可使配置生效

前提条件:

1、nacos中要有一个与微服务名有关的配置文件

在这里插入图片描述

2、微服务中要以特定方式读取需要热更新的配置属性(推荐第一种)

在这里插入图片描述

案例:实现购物车添加商品上限的配置热部署

需求:购物车的限定数量目前是写死在业务中的,将其改为读取配置文件属性,并将属性交给Nacos管理,实现热更新

@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
    private Integer maxItems;
}

CartServiceImpl


private CartProperties cartProperties;

private void checkCartsFull(Long userId) {
        int count = Math.toIntExact(lambdaQuery().eq(Cart::getUserId, userId).count());
        if (count >= cartProperties.getMaxItems()) {
            throw new BizIllegalException(
                    StrUtil.format("用户购物车课程不能超过{}", cartProperties.getMaxItems()));
        }
    }

在这里插入图片描述

测试:购物车中只能添加一个商品

在这里插入图片描述

2.4.4、动态路由

要实现动态路由首先要将路由配置保存到Nacos,当Nacos中的路由配置变更时,推送最新配置到网关,实时更新网关中的路由信息

我们要完成两件事情:

1、监听Nacos配置变更的消息

在Nacos管网中给出了手动监听Nacos配置变更的SDK:Java SDK (nacos.io)

   private final NacosConfigManager nacosConfigManager;

    @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 s) {
                //2.监听到配置变更,需要去更新路由表
            }
        });
        //3.第一次读取到配置,也需要更新到路由表
        updateConfigInfo(configInfo);
    }

2、当配置变更时,将最新的路由信息更新到网关路由表

监听到路由信息后,可以利用RouteDefinitionWriter来更新路由表

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

路由配置语法

为了方便解析从Nacos读取到底路由配置,推荐使用json格式的路由配置,模块如下:

在这里插入图片描述

public void updateConfigInfo(String configInfo){
    log.debug("监听到路由配置信息"+configInfo);
    //1.解析配置信息,转为RouteDefinition
    List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
    //2.更新前先删除旧的路由表
    for (String routeId : routeIds) {
        writer.delete(Mono.just(routeId)).subscribe();
    }
    routeIds.clear();
    //3.判断是否有新的路由要更新
    if (CollUtils.isEmpty(routeDefinitions)){
        //无新路由配置,直接结束
        return;
    }
    //4.更新路由
    routeDefinitions.forEach(routeDefinition -> {
        //更新路由
        writer.save(Mono.just(routeDefinition)).subscribe();
        //记录路由id,方便将来删除
        routeIds.add(routeDefinition.getId());
    });
}

在Nacos中新增配置

在这里插入图片描述

[
    {
        "id": "item",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
        }],
        "filters": [],
        "uri": "lb://item-service"
    },
    {
        "id": "cart",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/carts/**"}
        }],
        "filters": [],
        "uri": "lb://cart-service"
    },
    {
        "id": "user",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}
        }],
        "filters": [],
        "uri": "lb://user-service"
    },
    {
        "id": "trade",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/orders/**"}
        }],
        "filters": [],
        "uri": "lb://trade-service"
    },
    {
        "id": "pay",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/pay-orders/**"}
        }],
        "filters": [],
        "uri": "lb://pay-service"
    }
]

路由表的更新有一定的延迟

3、服务保护和分布式事务

3.1、雪崩问题

微服务调用链路中的某个服务故障,引起整个链路中所有微服务不可用,这就是雪崩

雪崩问题产生的原因是什么?

  • 微服务互相调用,服务提供者出现故障或阻塞
  • 服务调用者没有做好异常处理,导致自身故障
  • 调用链中的所有服务级联失败,导致整个集群故障

解决问题的思路?

  • 尽量避免服务出现故障或阻塞
    • 保证代码的健壮性
    • 保证网络畅通
    • 能应对较高的并发需求

3.2、解决方案

3.2.1、请求限流

限制访问微服务的请求的并发量,避免服务因流量激增出现故障

在这里插入图片描述

3.2.2、线程隔离

线程隔离:也叫做舱壁模式,模拟船舱隔板的防水原理。通过限制每个业务能使用的线程数量而将故障业务隔离,避免故障扩展。

在这里插入图片描述

3.2.3、服务断熔

服务断熔:由断路器统计请求的异常比例或慢调用比例,如果超出阈值则会熔断该业务,则拦截该接口的请求

熔断期间,所有的请求快速失败,全都做fallback逻辑

在这里插入图片描述

3.3.4、服务保护技术
** **SentinelHystrix
线程隔离信号量隔离线程池隔离/信号量隔离
熔断策略基于慢调用比例或异常比例基于异常比率
限流基于 QPS,支持流量整形有限的支持
Fallback支持支持
控制台开箱即用,可配置规则、查看秒级监控、机器发现等不完善
配置方式基于控制台,重启后失效基于注解或配置文件,永久生效

3.3、Sentinel

3.3.1、初识Sentinel

Sentinel是阿里巴巴开源的一款微服务流量控制组件

官网:https://b11et3un53m.feishu.cn/wiki/QfVrw3sZvihmnPkmALYcUHIDnff#YRqVd7bn8odK9mx5F1tccqcrn2l

使用步骤:

1、下载jar包

在这里插入图片描述

2、运行

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

在浏览器中输入localhost:8090
在这里插入图片描述

3.3.2、微服务整合

我们子啊cart-service模块中整合sentinel,连接sentinel-dashboard控制台,步骤如下:

1、引入sentinel依赖

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

2、配置控制台

修改application.yaml文件,添加下面内容:

spring:
  cloud: 
    sentinel:
      transport:
        dashboard: localhost:8090

重启服务,建立与Sentinel的连接,在黑马商城中访问购物车的相关业务
在这里插入图片描述

3.3.3、簇点链路

簇点链路,就是单机调用链路。是一次请求进入服务后经过的每一个被Sentinel监控的资源链。默认Sentinel会监控SpringMVC的每一个EndPoint(http接口)。限流、熔断等都是针对簇点链路中的资源设置的。而资源名默认就是接口的请求路径:

在这里插入图片描述

RestFul风格的API请求一般都相同,这会导致簇点资源名称重复。因此我们要修改配置,把请求方式+请求路径作为簇点资源名称:

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090 #Sentinel的控制台地址
      http-method-specify: true #开启请求方式前缀

在这里插入图片描述

在这里插入图片描述

3.3.4、请求限流

在簇点链路后面点击流控按钮,即可对其做限流配置

Cloud.assets%5Cimage-20240808211321541.png&pos_id=img-2cbB1qiD-1723212019655)

在Jmeter中进行测试

在这里插入图片描述

如图,出现429代表实现限流
在这里插入图片描述

3.3.5、线程隔离

当商品服务出现阻塞或故障时,调用商品服务的购物车服务可能因此而被拖慢,甚至资源耗尽。所有必须限制购物车服务中查询商品这个业务的可用线程数,实现线程隔离

在Sentinel控制台中,会出现Feign接口的簇点资源,点击后面的流控按钮,即可配置线程隔离:

在这里插入图片描述

在ItemController中模拟业务延迟

 @ApiOperation("根据id批量查询商品")
    @GetMapping
    public List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids){
        //模拟业务延迟
        ThreadUtil.sleep(500);
        return itemService.queryItemByIds(ids);
    }

限制购物车模块的tomcat线程数

server:
  port: 8082
  tomcat:
    threads:
      max: 25
    accept-count: 25
    max-connections: 100

在这里插入图片描述
在这里插入图片描述

可以看出大部分都异常了

3.3.6、Fallback

1、将FeignClient作为Sentinel的簇点资源:

feign:
  sentinel:
  	enabled: true

2、FeignClient的FallBack有两种配置方法:

  • 方式一:FallbackClass,无法对远程调用的异常做处理
  • 方式二:FallbackFactory,可以对远程调用的异常做处理,通常都会选这种
@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 CollUtils.emptyList();
            }

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

DefaultFeignConfig

  @Bean
    public ItemClientFallbackFactory itemClientFallbackFactory(){
        return new ItemClientFallbackFactory();
    }

ItemClient

@FeignClient(value = "item-service",fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {

}

在这里插入图片描述

在这里插入图片描述

3.3.7、服务熔断

​ 熔断是解决雪崩问题的主要手段。思路是由断路器统计服务调用异常的比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求,而当服务恢复时,断路器会放行访问该服务的请求

在这里插入图片描述

在这里插入图片描述

3.4、分布式事务

​ 在分布式系统中,如果一个业务需要多个服务合作完成,而且每一个服务都有事务,多个事务必须同时成功或失败,这样的事务就是分布式事务。其中的每个服务就是一个分支事务。整个业务成为全局事务

下单业务,前端请求首先进入订单服务,创建订单并写入数据库。然后订单服务调用购物车服务和库存服务:

  • 购物车服务负责清理购物车信息
  • 库存服务负责扣减商品库存

在这里插入图片描述

3.4.1、初识Seata

Seata是2019年1月蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案

在这里插入图片描述

在这里插入图片描述
分布式事务解决思路:

在这里插入图片描述

Seata架构

Seata事务管理中有三个重要的角色:

  • TC(Transaction Cooridinator)-事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚
  • TM(Tansaction Manager)-事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务
  • RM(Resource Manager)-资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态
3.4.2、部署TC服务

1、准备数据库表

导入数据库表seata

2、准备配置文件

导入seata目录到虚拟机

将nacos连接到网络

docker network connect hm-net nacos

3、Docker部署

docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.88.130 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
-d \
seataio/seata-server:1.5.2

在浏览器中输入你的端口号:7099进入seata

在这里插入图片描述

3.4.3、微服务集成Seata

1、首先,要在项目中引入Seata依赖:

<!--统一配置管理-->
  <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、然后,在application.yml中添加配置,让微服务找到TC服务地址

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 192.168.88.130: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"

在这里插入图片描述
如果jdk是11以上版本要在启动类上修改

在这里插入图片描述

运行成功:
在这里插入图片描述

3.4.4、XA模式

XA模范是X/Open组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA规范描述了全局的TM与局部的RM之间的接口,几乎所有主流的关系型数据库都对XA规范提供了支持。Seata的XA模式如下:

一阶段工作:

  1. RM注册分支事务到TC
  2. RM执行分支业务sql但不提交
  3. RM报告执行状态到TC

二阶段工作:

  • TC检测各分支事务执行状态
    • a、如果都成功,通知所有RM提交事务
    • b、如果有失败,通知所有RM回滚事务
  • RM接收TC指令,提交或回滚事务

在这里插入图片描述

XA模式的有优点是什么?

  • 事务的强一致性,满足ACID原则
  • 常用数据库都支持,显示简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

实现XA模式

Seata的starter已经完成了XA模式的自动配置,实现非常简单,步骤如下:

1、修改application.yml文件(每个参与事务的微服务),开启XA模式:

seata:
  data-source-proxy-mode: XA

2、给发起全局事务的入口方法添加@GlobalTransaction注解,本例中是OrderServiceImpl中的create方法:

在这里插入图片描述

3、重启服务并测试

3.4.5、AT模式

Seata主推的是AT模式,AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模式中组员锁定周期过长的缺陷

阶段一RM的工作:

  • 注册分支事务
  • 记录undo-log(数据快照)
  • 执行业务sql并提交
  • 报告事务状态

阶段二提交时RM的工作:

  • 删除undo-log即可

阶段二回滚时RM的工作:

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

在这里插入图片描述

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

  • XA模式一阶段不提交事物,锁定资源;AT模式一阶段直接提交,不锁定资源。
  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚
  • XA模式强一致;AT模式最终

实现AT模式:

首先,添加资料汇总的Seata-at.sql到微服务对应的数据库中:

然后,修改application.yml文件,将事务模式修改为AT模式:

seata:
  data-source-proxy-mode: AT

913253" style=“zoom:50%;” />

如果jdk是11以上版本要在启动类上修改

[外链图片转存中…(img-QC5g9MwU-1723212019658)]

运行成功:

[外链图片转存中…(img-NEsbuGCP-1723212019658)]

3.4.4、XA模式

XA模范是X/Open组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA规范描述了全局的TM与局部的RM之间的接口,几乎所有主流的关系型数据库都对XA规范提供了支持。Seata的XA模式如下:

一阶段工作:

  1. RM注册分支事务到TC
  2. RM执行分支业务sql但不提交
  3. RM报告执行状态到TC

二阶段工作:

  • TC检测各分支事务执行状态
    • a、如果都成功,通知所有RM提交事务
    • b、如果有失败,通知所有RM回滚事务
  • RM接收TC指令,提交或回滚事务

[外链图片转存中…(img-R3xeQSL1-1723212019658)]

XA模式的有优点是什么?

  • 事务的强一致性,满足ACID原则
  • 常用数据库都支持,显示简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

实现XA模式

Seata的starter已经完成了XA模式的自动配置,实现非常简单,步骤如下:

1、修改application.yml文件(每个参与事务的微服务),开启XA模式:

seata:
  data-source-proxy-mode: XA

2、给发起全局事务的入口方法添加@GlobalTransaction注解,本例中是OrderServiceImpl中的create方法:
在这里插入图片描述

3、重启服务并测试

3.4.5、AT模式

Seata主推的是AT模式,AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模式中组员锁定周期过长的缺陷

阶段一RM的工作:

  • 注册分支事务
  • 记录undo-log(数据快照)
  • 执行业务sql并提交
  • 报告事务状态

阶段二提交时RM的工作:

  • 删除undo-log即可

阶段二回滚时RM的工作:

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

[外链图片转存中…(img-y05XX17y-1723212019659)]

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

  • XA模式一阶段不提交事物,锁定资源;AT模式一阶段直接提交,不锁定资源。
  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚
  • XA模式强一致;AT模式最终

实现AT模式:

首先,添加资料汇总的Seata-at.sql到微服务对应的数据库中:

然后,修改application.yml文件,将事务模式修改为AT模式:

seata:
  data-source-proxy-mode: AT
;