【微服务架构V2】

文章目录


一、微服务网关


1、概念

1.一款基于 SpringFramework5,Project Reactor 和SpringBoot 2 之上开发的网关
2.第一代网关 Zuul 不同的是:gateway 是异步非阻塞的(netty+webflux 实现);zuul是同步阻塞请求的
3.三大组成部分

  • Route路由(ID必须唯一、URI)
  • Predicate断言:路由的匹配条件
  • Filter过滤器
    在这里插入图片描述

4、网关是微服务工程架构下的唯一入口(客户端 )
5、Gateway 提供了统一的路由方式,基于Filter 链的方式提供了网关的基本功能在这里插入图片描述

工作模型
在这里插入图片描述

解读
第一步:请求发送到网关,经由分发器将请求匹配到相应的 HandlerMapping,第一个分析匹配的组件,匹配相应的路由配置。
第二步:请求和处理器之间有一个映射,路由到网关处理程序,即 Web Handler,读取路由配置上的所有过滤器,构造过滤器链,往过滤器上传递。
第三步:执行特定的请求过滤器链,最终到达代理的微服务
第四步:响应逆向返回客户端


2、谓词 Predicate 的原理与应用

一、概念与原理

使用的java8的Predicate(位于 java.util.function 包中,是一个 FunctionalInterface(函数式接口)),其中的接口通常用在steam的filter中实现,表示是否满足条件。

/**
 * <h1>Java8 Predicate 使用方法与思想</h1>
 * */
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class PredicateTest {

    public static List<String> MICRO_SERVICE = Arrays.asList(
            "nacos",  "gateway", "ribbon", "feign", "hystrix"
    );

    /**
     * test 方法主要用于参数符不符合规则, 返回值是 boolean
     * */
    @Test
    public void testPredicateTest() {

        Predicate<String> letterLengthLimit = s -> s.length() > 5;
        MICRO_SERVICE.stream().filter(letterLengthLimit).forEach(System.out::println);
        //打印出 gateway、ribbon、hystrix
    }

    /**
     * and 方法等同于我们的逻辑与 &&, 存在短路特性, 需要所有的条件都满足
     * */
    @Test
    public void testPredicateAnd() {

        Predicate<String> letterLengthLimit = s -> s.length() > 5;
        Predicate<String> letterStartWith = s -> s.startsWith("gate");

        MICRO_SERVICE.stream().filter(
                letterLengthLimit.and(letterStartWith)
        ).forEach(System.out::println);
        //打印出 gateway
    }

    /**
     * or 等同于我们的逻辑或 ||, 多个条件主要一个满足即可
     * */
    @Test
    public void testPredicateOr() {

        Predicate<String> letterLengthLimit = s -> s.length() > 5;
        Predicate<String> letterStartWith = s -> s.startsWith("gate");

        MICRO_SERVICE.stream().filter(
                letterLengthLimit.or(letterStartWith)
        ).forEach(System.out::println);
        //打印出 gateway、ribbon、hystrix
    }

    /**
     * negate 等同于我们的逻辑非 !
     * */
    @Test
    public void testPredicateNegate() {

        Predicate<String> letterStartWith = s -> s.startsWith("gate");
        MICRO_SERVICE.stream().filter(letterStartWith.negate()).forEach(System.out::println);
        //打印出 nacos、ribbon、hystrix、feign
    }

    /**
     * isEqual 类似于 equals(), 区别在于: 先判断对象是否为 NULL,
     * 不为 NULL 再使用 equals 进行比较
     * */
    @Test
    public void testPredicateIsEqual() {

        Predicate<String> equalGateway = s -> Predicate.isEqual("gateway").test(s);
        MICRO_SERVICE.stream().filter(equalGateway).forEach(System.out::println);
        //打印出gateway
    }
}

二、Gateway中的应用

1、源码

public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<PathRoutePredicateFactory.Config> {
 
    public PathRoutePredicateFactory() {
        super(PathRoutePredicateFactory.Config.class);
    }
 
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("path");
    }
 
    @Override
    public Predicate<ServerWebExchange> apply(PathRoutePredicateFactory.Config config) {
        return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange exchange) {
                PathContainer path = exchange.getRequest().getPath();
                for (String p : config.getPath()) {
                    if (path.match(TextUtils.urlDecode(p))) {
                        return true;
                    }
                }
                return false;
            }
 
            @Override
            public String toString() {
                return String.format("Paths: %s", config.getPath());
            }
        };
    }
 
    public static class Config {
        private List<String> path = new ArrayList<>();
 
        public List<String> getPath() {
            return path;
        }
 
        public void setPath(List<String> path) {
            this.path = path;
        }
    }
}

解读:用于根据请求的路径进行路由判断和过滤。当请求的路径匹配指定的条件时,路由会被转发到相应的服务 例如:

    gateway:
      routes:
        - id: path_route_example
          uri: http://httptest.org
          predicates:
            - Path=/user/**  # 匹配 /user/ 开头的路径
#此配置将匹配所有以 /user/ 开头的请求路径,
#如 /user/123 或 /user/abc,并将它们转发到 http://httptest.org/user/** ```

3、集成 Alibaba Nacos 实现路由配置

一、静态路由配置

(1)、yml文件配置

1、静态路由配置写在配置文件中(yml或者 properties 文件中),端点是:spring.cloud.gateway
2、每次改动都需要网关模块重新部署

    # 静态路由
      gateway:
        routes:
          - id: path_route # 路由的ID
            uri: 127.0.0.1:8080/user/{id} # 匹配后路由地址
            predicates: # 断言, 路径相匹配的进行路由
              - Path=/user/{id}
(2)、代码配置

用于登录注册转发微服务

@Configuration
public class RouteLocatorConfig {

    /**
     * <h2>使用代码定义路由规则, 在网关层面拦截下登录和注册接口</h2>
     * */
    @Bean
    public RouteLocator loginRouteLocator(RouteLocatorBuilder builder) {

        // 手动定义 Gateway 路由规则需要指定 id、path 和 uri
        return builder.routes()
                .route(
                        "e_commerce_authority",
                        r -> r.path(
                        		//配置项中的predicates
                                "/whqn/e-commerce/login",
                                "/whqn/e-commerce/register"
                                ).uri("http://localhost:9001/")
                ).build();
    }
}

二、动态路由配置

在这里插入图片描述
1、修改nacos配置
在这里插入图片描述
在这里插入图片描述

[
  {
    "id": "e-commerce-nacos-client",
    "predicates": [
      {
        "args": {
          "pattern": "/whqn/ecommerce-nacos-client/**"
        },
        "name": "Path"
      }
    ],
    "uri": "lb://e-commerce-nacos-client",#微服务id
    "filters": [
      {
        "name": "HeaderToken"
      },
      {
        "name": "StripPrefix",
        "args": {
          "parts": "1"
        }
      }
    ]
  }
]

2、创建工程
【pom文件】

<?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>ecommerce-springcloud</artifactId>
        <groupId>com.whqn.ecommerce</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>e-commerce-gateway</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>e-commerce-gateway</name>
    <description>Spring Cloud Gateway</description>

    <dependencies>
        <!-- spring cloud alibaba nacos discovery 依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>2.5.0.RELEASE</version>
        </dependency>
        <!-- 不能引入mvc依赖,gateway使用的不是tomcat-->
        <dependency>
            <groupId>com.whqn.ecommerce</groupId>
            <artifactId>e-commerce-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <!--
        SpringBoot的Maven插件, 能够以Maven的方式为应用提供SpringBoot的支持,可以将
        SpringBoot应用打包为可执行的jar或war文件, 然后以通常的方式运行SpringBoot应用
     -->
    <build>
        <finalName>${artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

【yml】

server:
  port: 9001
  servlet:
    context-path: /whqn

spring:
  application:
    name: e-commerce-gateway
  cloud:
    nacos:
      discovery:
        enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        server-addr: 127.0.0.1:8848 # Nacos 服务器地址
        namespace: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator
    # 静态路由
  #    gateway:
  #      routes:
  #        - id: path_route # 路由的ID
  #          uri: 127.0.0.1:8080/user/{id} # 匹配后路由地址
  #          predicates: # 断言, 路径相匹配的进行路由
  #            - Path=/user/{id}
  main:
    allow-bean-definition-overriding: true  # 因为将来会引入很多依赖, 难免有重名的 bean

# 这个地方独立配置, 是网关的数据, 代码 GatewayConfig.java 配置类中读取被监听
nacos:
  gateway:
    route:
      config:
        data-id: e-commerce-gateway-router
        group: e-commerce

# 暴露端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

3、创建配置类, 读取 Nacos 相关的配置项, 用于配置监听器

@Configuration
public class GatewayConfig {

    /** 读取配置的超时时间 */
    public static final long DEFAULT_TIMEOUT = 30000;

    /** Nacos 服务器地址 */
    public static String NACOS_SERVER_ADDR;

    /** 命名空间 */
    public static String NACOS_NAMESPACE;

    /** data-id */
    public static String NACOS_ROUTE_DATA_ID;

    /** 分组 id */
    public static String NACOS_ROUTE_GROUP;

    @Value("${spring.cloud.nacos.discovery.server-addr}")
    public void setNacosServerAddr(String nacosServerAddr) {
        NACOS_SERVER_ADDR = nacosServerAddr;
    }

    @Value("${spring.cloud.nacos.discovery.namespace}")
    public void setNacosNamespace(String nacosNamespace) {
        NACOS_NAMESPACE = nacosNamespace;
    }

    @Value("${nacos.gateway.route.config.data-id}")
    public void setNacosRouteDataId(String nacosRouteDataId) {
        NACOS_ROUTE_DATA_ID = nacosRouteDataId;
    }

    @Value("${nacos.gateway.route.config.group}")
    public void setNacosRouteGroup(String nacosRouteGroup) {
        NACOS_ROUTE_GROUP = nacosRouteGroup;
    }
}

4、注册网关事件监听器(事件推送 Aware: 动态更新路由网关 Service)

@Slf4j
@Service
@SuppressWarnings("all")
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {

    /** 写路由定义 */
    private final RouteDefinitionWriter routeDefinitionWriter;
    /** 获取路由定义 */
    private final RouteDefinitionLocator routeDefinitionLocator;

    /** 事件发布 */
    private ApplicationEventPublisher publisher;

    public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter,
                                   RouteDefinitionLocator routeDefinitionLocator) {
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.routeDefinitionLocator = routeDefinitionLocator;
    }

    @Override
    public void setApplicationEventPublisher(
            ApplicationEventPublisher applicationEventPublisher) {
        // 完成事件推送句柄的初始化
        this.publisher = applicationEventPublisher;
    }

    /**
     * <h2>增加路由定义</h2>
     * */
    public String addRouteDefinition(RouteDefinition definition) {

        log.info("gateway add route: [{}]", definition);

        // 保存路由配置并发布
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        // 发布事件通知给 Gateway, 同步新增的路由定义
        this.publisher.publishEvent(new RefreshRoutesEvent(this));

        return "success";
    }

    /**
     * <h2>更新路由</h2>
     * */
    public String updateList(List<RouteDefinition> definitions) {

        log.info("gateway update route: [{}]", definitions);

        // 先拿到当前 Gateway 中存储的路由定义
        List<RouteDefinition> routeDefinitionsExits =
                routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
        if (!CollectionUtils.isEmpty(routeDefinitionsExits)) {
            // 清除掉之前所有的 "旧的" 路由定义
            routeDefinitionsExits.forEach(rd -> {
                log.info("delete route definition: [{}]", rd);
                deleteById(rd.getId());
            });
        }

        // 把更新的路由定义同步到 gateway 中
        definitions.forEach(definition -> updateByRouteDefinition(definition));
        return "success";
    }

    /**
     * <h2>根据路由 id 删除路由配置</h2>
     * */
    private String deleteById(String id) {

        try {
            log.info("gateway delete route id: [{}]", id);
            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
            // 发布事件通知给 gateway 更新路由定义
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "delete success";
        } catch (Exception ex) {
            log.error("gateway delete route fail: [{}]", ex.getMessage(), ex);
            return "delete fail";
        }
    }

    /**
     * <h2>更新路由</h2>
     * 更新的实现策略比较简单: 删除 + 新增 = 更新
     * */
    private String updateByRouteDefinition(RouteDefinition definition) {

        try {
            log.info("gateway update route: [{}]", definition);
            this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
        } catch (Exception ex) {
            return "update fail, not find route routeId: " + definition.getId();
        }

        try {
            this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        } catch (Exception ex) {
            return "update route fail";
        }
    }
}

RouteDefinition.java源码对应的是配置文件中的配置项
在这里插入图片描述

5、注册网关事件监听器(通过 nacos 下发动态路由配置, 监听 Nacos 中路由配置变更)

@Slf4j
@Component
@DependsOn({"gatewayConfig"}) //配置项先注入
public class DynamicRouteServiceImplByNacos {

    /** Nacos 配置服务 */
    private ConfigService configService;
    private final DynamicRouteServiceImpl dynamicRouteService;

    public DynamicRouteServiceImplByNacos(DynamicRouteServiceImpl dynamicRouteService) {
        this.dynamicRouteService = dynamicRouteService;
    }

    /**
     * <h2>Bean 在容器中构造完成之后会执行 init 方法</h2>
     * */
    @PostConstruct
    public void init() {

        log.info("gateway route init....");

        try {
            // 初始化 Nacos 配置客户端
            configService = initConfigService();
            if (null == configService) {
                log.error("init config service fail");
                return;
            }

            // 通过 Nacos Config 并指定路由配置路径去获取路由配置
            String configInfo = configService.getConfig(
                    GatewayConfig.NACOS_ROUTE_DATA_ID,
                    GatewayConfig.NACOS_ROUTE_GROUP,
                    GatewayConfig.DEFAULT_TIMEOUT
            );

            log.info("get current gateway config: [{}]", configInfo);
            List<RouteDefinition> definitionList =
                    JSON.parseArray(configInfo, RouteDefinition.class);

            if (CollectionUtils.isNotEmpty(definitionList)) {
                for (RouteDefinition definition : definitionList) {
                    log.info("init gateway config: [{}]", definition.toString());
                    dynamicRouteService.addRouteDefinition(definition);
                }
            }

        } catch (Exception ex) {
            log.error("gateway route init has some error: [{}]", ex.getMessage(), ex);
        }

        // 设置监听器
        dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID,
                GatewayConfig.NACOS_ROUTE_GROUP);
    }

    /**
     * <h2>初始化 Nacos Config</h2>
     * */
    private ConfigService initConfigService() {

        try {
            Properties properties = new Properties();
            properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);
            properties.setProperty("namespace", GatewayConfig.NACOS_NAMESPACE);
            return configService = NacosFactory.createConfigService(properties);
        } catch (Exception ex) {
            log.error("init gateway nacos config error: [{}]", ex.getMessage(), ex);
            return null;
        }
    }

    /**
     * <h2>监听 Nacos 下发的动态路由配置</h2>
     * */
    private void dynamicRouteByNacosListener(String dataId, String group) {

        try {
            // 给 Nacos Config 客户端增加一个监听器
            configService.addListener(dataId, group, new Listener() {

                /**
                 * <h2>自己提供线程池执行操作</h2>
                 * */
                @Override
                public Executor getExecutor() {
                    return null;
                }

                /**
                 * <h2>监听器收到配置更新</h2>
                 * @param configInfo Nacos 中最新的配置定义
                 * */
                @Override
                public void receiveConfigInfo(String configInfo) {

                    log.info("start to update config: [{}]", configInfo);
                    List<RouteDefinition> definitionList =
                            JSON.parseArray(configInfo, RouteDefinition.class);
                    log.info("update route: [{}]", definitionList.toString());
                    dynamicRouteService.updateList(definitionList);
                }
            });
        } catch (NacosException ex) {
            log.error("dynamic update gateway config error: [{}]", ex.getMessage(), ex);
        }
    }
}

4、过滤器SpringCloud Gateway Filter

一、相关概念

1、SpringCloud Gateway 基于过滤器实现,同 zuul 类似,有 pre 和 post两种方式的filter,分别处理前置逻辑和后置逻辑
2、客户端的请求先经过 pre 类型的 fiter,然后将请求转发到具体的业务服务,收到业务服务的响应之后,再经过 post 类型的 filter 处理,最后返回响应到客户端
3、Filter 一共有两大类:全局过滤器和局部过滤器

二、执行流程

1、过滤器有优先级之分,Order越大,优先级越低,越晚被执行
2、全局过滤器所有的请求都会执行
3、局部过滤器只有配置的请求才会执行
在这里插入图片描述


三、内置过滤器解读

(1)、全局过滤器RouteToRequestUrlFilter.java

作用示例:

假设你有如下的路由配置

spring:
  cloud:
    gateway:
      routes:
        - id: service1_route
          uri: lb://service-1
          predicates:
            - Path=/api/v1/**

当请求到达 http://localhost:8080/api/v1/hello 时:

路由匹配:请求路径 /api/v1/hello 会匹配到 service1_route 路由。
路由到目标服务:RouteToRequestUrlFilter 会使用服务发现机制(如 Nacos 或 Eureka),从中查找服务 ID 为 service-1 的实例。
生成目标 URL:假设 service-1 实际运行在 http://localhost:8081,RouteToRequestUrlFilter 会将原始请求的 URL http://localhost:8080/api/v1/hello 替换为 http://localhost:8081/api/v1/hello
转发请求:请求最终会被转发到 http://localhost:8081/api/v1/hello ,即目标微服务。


源码解读(看注释):

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 从exchange中取路由Route对象
    Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
    if (route == null) {
        return chain.filter(exchange);
    }
    log.trace("RouteToRequestUrlFilter start");
    // 取当前请求uri : http://localhost:8080/api/v1/hello
    URI uri = exchange.getRequest().getURI();
    boolean encoded = containsEncodedParts(uri);
    // 路由对象中保存的uri,也就是我们在yml文件中配置的值:   lb://service-1
    URI routeUri = route.getUri();

    if (hasAnotherScheme(routeUri)) {
        exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR, routeUri.getScheme());
        routeUri = URI.create(routeUri.getSchemeSpecificPart());
    }

    // 如果我们在yml文件中配置的uri,即不是lb开头并且host还为null,那么就抛异常
    if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
        throw new IllegalStateException("Invalid host: " + routeUri.toString());
    }

    // 转换结果为:  lb://service-1/api/v1/hello 
    //ReactiveLoadBalancerClientFilter转化为http://localhost:8081/api/v1/hello
    URI mergedUrl = UriComponentsBuilder.fromUri(uri)
        .scheme(routeUri.getScheme()).host(routeUri.getHost()).
        port(routeUri.getPort()).build(encoded).toUri();
    // 存入exchange中,之后的LoadBalancer全局Filter中会用到
    exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl);
    return chain.filter(exchange);
}


(2)、局部过滤器PrefixPathGatewayFilterFactory.java

作用示例:

spring;
	cloud:
		routes:
			-id: whqn
			uri: http://example/org
			filters:
				- PrefixPath=/mypath

当请求到达:http://gatewayhost:8080/hello
生成目标 URL:http://example/org/mypath/hello


源码解读(看注释):

 	@Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {

            boolean alreadyPrefixed = exchange.getAttributeOrDefault(GATEWAY_ALREADY_PREFIXED_ATTR, false);
            if (alreadyPrefixed) {
                return chain.filter(exchange);
            }
            exchange.getAttributes().put(GATEWAY_ALREADY_PREFIXED_ATTR, true);

            ServerHttpRequest req = exchange.getRequest();
            addOriginalRequestUrl(exchange, req.getURI());
            //config.prefix是需要加入的前缀
            String newPath = config.prefix + req.getURI().getRawPath();

            ServerHttpRequest request = req.mutate()
                    .path(newPath)
                    .build();

            exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI());

            if (log.isTraceEnabled()) {
                log.trace("Prefixed URI with: "+config.prefix+" -> "+request.getURI());
            }

            return chain.filter(exchange.mutate().request(request).build());
        };
    }
(3)、局部过滤器StripPrefixGatewayFilterFactory.java

作用示例:

spring;
	cloud:
		routes:
			-id: whqn
			uri: http://example/org
			predicates:
				- Path=/name/**
			filters:
				- StripPrefix=2

当请求到达:http://gatewayhost:8080/name/test/hello
生成目标 URL:http://example/org/hello


源码解读(看注释):

	@Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) ->  {
            ServerHttpRequest request = exchange.getRequest();
            addOriginalRequestUrl(exchange, request.getURI());
            String path = request.getURI().getRawPath();
            //config.parts是int类型,表示跳过几个部分
            String newPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(path, "/"))
                    .skip(config.parts).collect(Collectors.joining("/"));
            ServerHttpRequest newRequest = request.mutate()
                    .path(newPath)
                    .build();

            exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());

            return chain.filter(exchange.mutate().request(newRequest).build());
        };
    }

三、自定义过滤器


(1)、【局部过滤器】- 校验 Header 中的 Token

1、需要实现 GatewayFilter, Ordered,实现相关的方法
2、加入到过滤器工厂,并且将工厂注册到 Spring 容器中
3、在配置文件中进行配置,如果不配置则不启用此过滤器规则(路由规则)

作用示例:

"filters": [
      {
        "name": "HeaderToken" #配置之后才会生效
      },
    ]

请求头中需要存在从 HTTP Header 中寻找 key 为 token, value 为 whqn 的键值对


步骤(看注释):

1、HTTP 请求头部携带 Token 验证过滤器

public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        // 从 HTTP Header 中寻找 key 为 token, value 为 whqn 的键值对
        String name = exchange.getRequest().getHeaders().getFirst("token");
        if ("whqn".equals(name)) {
            return chain.filter(exchange);
        }

        // 标记此次请求没有权限, 并结束这次请求
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }

    @Override
    //值越大,越迟执行
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 2;
    }
}

2、新建过滤器工程,不创建无法生效

@Component
//取名称中的HeaderToke作为配置项
public class HeaderTokenGatewayFilterFactory
        extends AbstractGatewayFilterFactory<Object> {

    @Override
    public GatewayFilter apply(Object config) {
        return new HeaderTokenGatewayFilter();
    }
}


(2)、【全局过滤器】- 耗时日志
@Slf4j
@Component
public class GlobalElapsedLogFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        // 前置逻辑
        StopWatch sw = StopWatch.createStarted();
        String uri = exchange.getRequest().getURI().getPath();

        return chain.filter(exchange).then(
                // 后置逻辑
                Mono.fromRunnable(() ->
                        log.info("[{}] elapsed: [{}ms]",
                                uri, sw.getTime(TimeUnit.MILLISECONDS)))
        );
    }

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

(2)、【全局过滤器】- 缓存HTTP请求Body

作用:将请求头的body内容全部缓存起来

定义常量:

public class GatewayConstant {

    /** 登录 uri */
    public static final String LOGIN_URI = "/e-commerce/login";

    /** 注册 uri */
    public static final String REGISTER_URI = "/e-commerce/register";
    
}

编写过滤器,从登录或注册请求中缓存请求中的body

@Slf4j
@Component
@SuppressWarnings("all")
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        boolean isloginOrRegister =
                exchange.getRequest().getURI().getPath().contains(GatewayConstant.LOGIN_URI)
                || exchange.getRequest().getURI().getPath().contains(GatewayConstant.REGISTER_URI);

        if (null == exchange.getRequest().getHeaders().getContentType()
                || !isloginOrRegister) {
            return chain.filter(exchange);
        }

        // DataBufferUtils.join 拿到请求中的数据 --> DataBuffer
        return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {

            // 确保数据缓冲区不被释放, 必须要 DataBufferUtils.retain
            DataBufferUtils.retain(dataBuffer);
            // defer、just 都是去创建数据源, 得到当前数据的副本
            Flux<DataBuffer> cachedFlux = Flux.defer(() ->
                    Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
            // 重新包装 ServerHttpRequest, 重写 getBody 方法, 能够返回请求数据
            ServerHttpRequest mutatedRequest =
                    new ServerHttpRequestDecorator(exchange.getRequest()) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return cachedFlux;
                        }
                    };
            // 将包装之后的 ServerHttpRequest 向下继续传递
            return chain.filter(exchange.mutate().request(mutatedRequest).build());
        });
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 1;
    }
}

(3)、【全局过滤器】- 登录、注册、鉴权

1、定义常量:
鉴权微服务:context-path: /ecommerce-authority-center
id:e-commerce-authority-center
找到Ip和端口,最后进行拼接

public class GatewayConstant {

    /** 登录 uri */
    public static final String LOGIN_URI = "/e-commerce/login";

    /** 注册 uri */
    public static final String REGISTER_URI = "/e-commerce/register";

    /** 去授权中心拿到登录 token 的 uri 格式化接口 */
    public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT =
            "http://%s:%s/ecommerce-authority-center/authority/token";

    /** 去授权中心注册并拿到 token 的 uri 格式化接口 */
    public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT =
            "http://%s:%s/ecommerce-authority-center/authority/register";
}

2、将RestTemplate提前注入到网关中,用于远程调用微服务

@Configuration
public class GatewayBeanConf {

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

3、创建全局登录鉴权过滤器

@Slf4j
@Component
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {

    /** 注册中心客户端, 可以从注册中心中获取服务实例信息 */
    private final LoadBalancerClient loadBalancerClient;
    private final RestTemplate restTemplate;

    public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient,
                                       RestTemplate restTemplate) {
        this.loadBalancerClient = loadBalancerClient;
        this.restTemplate = restTemplate;
    }

    /**
     * 登录、注册、鉴权
     * 1. 如果是登录或注册, 则去授权中心拿到 Token 并返回给客户端
     * 2. 如果是访问其他的服务, 则鉴权, 没有权限返回 401
     * */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        // 1. 如果是登录
        if (request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)) {
            // 去授权中心拿 token
            String token = getTokenFromAuthorityCenter(
                    request, GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT
            );
            // header 中不能设置 null
            response.getHeaders().add(
                    CommonConstant.JWT_USER_INFO_KEY,
                    null == token ? "null" : token
            );
            response.setStatusCode(HttpStatus.OK);
            return response.setComplete();
        }

        // 2. 如果是注册
        if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)) {
            // 去授权中心拿 token: 先创建用户, 再返回 Token
            String token = getTokenFromAuthorityCenter(
                    request,
                    GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT
            );
            response.getHeaders().add(
                    CommonConstant.JWT_USER_INFO_KEY,
                    null == token ? "null" : token
            );
            response.setStatusCode(HttpStatus.OK);
            return response.setComplete();
        }

        // 3. 访问其他的服务, 则鉴权, 校验是否能够从 Token 中解析出用户信息
        HttpHeaders headers = request.getHeaders();
        String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
        LoginUserInfo loginUserInfo = null;

        try {
        	//TokenParseUtil是上一章学习记录编写的token解析工具类
            loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
        } catch (Exception ex) {
            log.error("parse user info from token error: [{}]", ex.getMessage(), ex);
        }

        // 获取不到登录用户信息, 返回 401
        if (null == loginUserInfo) {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        // 解析通过, 则放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 2;
    }

    /**
     * 从授权中心获取 Token
     * */
    private String getTokenFromAuthorityCenter(ServerHttpRequest request, String uriFormat) {

        // service id 就是服务名字, 负载均衡
        ServiceInstance serviceInstance = loadBalancerClient.choose(
                CommonConstant.AUTHORITY_CENTER_SERVICE_ID
        );
        log.info("Nacos Client Info: [{}], [{}], [{}]",
                serviceInstance.getServiceId(), serviceInstance.getInstanceId(),
                JSON.toJSONString(serviceInstance.getMetadata()));

        String requestUrl = String.format(
                uriFormat, serviceInstance.getHost(), serviceInstance.getPort()
        );
        UsernameAndPassword requestBody = JSON.parseObject(
                parseBodyFromRequest(request), UsernameAndPassword.class
        );
        log.info("login request url and body: [{}], [{}]", requestUrl,
                JSON.toJSONString(requestBody));

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        JwtToken token = restTemplate.postForObject(
                requestUrl,
                new HttpEntity<>(JSON.toJSONString(requestBody), headers),
                JwtToken.class
        );

        if (null != token) {
            return token.getToken();
        }

        return null;
    }

    /**
     * 从 Post 请求中获取到请求数据
     * */
    private String parseBodyFromRequest(ServerHttpRequest request) {

        // 获取请求体
        Flux<DataBuffer> body = request.getBody();
        AtomicReference<String> bodyRef = new AtomicReference<>();

        // 订阅缓冲区去消费请求体中的数据
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            // 一定要使用 DataBufferUtils.release 释放掉, 否则, 会出现内存泄露
            DataBufferUtils.release(buffer);
            bodyRef.set(charBuffer.toString());
        });

        // 获取 request body
        return bodyRef.get();
    }
}


四、验证过滤器


(1)、验证登录注册接口
### 登录
POST 127.0.0.1:9001/whqn/e-commerce/login
Content-Type: application/json

{
  "username": "WHQN@imooc.com",
  "password": "asdfdfds283aa400af464c76d713c07ad"
}

###
POST 127.0.0.1:9001/whqn/e-commerce/register
Content-Type: application/json

{
  "username": "WHQN97@imooc.com",
  "password": "asdfdfds283aa400af464c76d713c07ad"
}

使用了鉴权微服务并返回jwt

(2)、调用普通查询服务
### 查询nacos注册中心中网关服务的信息
GET http://127.0.0.1:9001/whqn/ecommerce-nacos-client/nacos-client/service-instance?serviceId=e-commerce-gateway
Accept: application/json
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjE3LFwidXNlcm5hbWVcIjpcIkltb29jUWlueWlAaW1vb2MuY29tXCJ9IiwianRpIjoiMGIxNzQyYWItMWU3OC00OTZjLWIyNTAtMjNkZGQ1ZGEyZTU1IiwiZXhwIjoxNjI0MjA0ODAwfQ.QKGHzohSHdYDHzUVHpe9gNPUgzfkPwrSbB-WiMWYjLlt2tr9BufzZM8bSt-whb_bd0hKoC6rkYYO0WUVR67uSML-2yaTL1xMIn8GH9Flyig3rpO4vefL3Hp2TXIpwHHa7WlKsLzcUpNk9lxWs2B5k0ICdYCH_jD5Tx6N7CzfSUG9u4fOnVeM9UFE2nX_DURupUh_DKCc2oOoMeyCSR7Ma8-Ab4WQU3r-U0YivR8G1A0kmKOIoTeRhM3LcPuxUPh3rCbrjzMg--fexRGw0O38Qsby6pz-ku2IlTyFXY6_jNOG1BZR34-jBOnaIciP1TExw9bFumeuC2GcowTHJVH1Nw
token: whqn #对应以上所述的HeaderTokenGatewayFilter过滤器


二、分布式日志追踪


1、SpringCloud日志追踪概述

一、组件

(1)、Sleuth

概要:它会自动为当前应用构建起各通信通道的跟踪机制(采用了 Brave 作为 tracer库
1、通过诸如 RabbitMQKafka(或者其他任何 SpringCloud Stream 绑定器实现的消息中间件)传递的请求
2、通过 ZuulGateway 代理传递的请求
3、通过 RestTemplate 发起的请求

跟踪原理
为了实现请求跟踪: 当请求发送到分布式系统的入口端点时,为该请求创建一个唯一的跟踪标识,Trace ID
为了统计各处理单元的时间延迟,请求到达各个服务组件时,通过唯一标识来标记它的开始、具体过程以及结束,Span ID
在这里插入图片描述

(2)、Zipkin

(1)、概要(Sleuth可以不使用 Zipkin)
解决微服务架构中的延迟问题, 包括数据的收集、存储、查找和展现

(2)、四大核心组件构成(如下面逻辑架构图所示)

  • Collector:收集器组件
  • Storage:存储组件
  • API:RESTFUIAPI,提供外部访问接口
  • UI:Web U,提供可视化查询页面
(3)、Brave

核心的概念 (是一个 tracer 库,提供的是 tracer接口)
1、trace: 以看作是一个逻辑执行过程中的整个链条
2、span:是 trace 跟踪的基本单位
在这里插入图片描述

二、结构

(1)、数据结构

Tracing:工具类,用于生成 Tracer 类实例
Tracer :也是工具类,用于生成 Span
Span:实际记录每个功能块执行信息的类
TraceContext:记录 trace 的执行过程中的元数据信息类
Propagation:用于在分布式环境或者跨进程条件下的trace 跟踪时实现 TraceContext 传递的工具类
在这里插入图片描述

(2)、逻辑架构

在这里插入图片描述

三、跨服务Trace追踪原理

概述:SpringCloud Sleuth 和 Brave 提供了很多不同的分布式框架的支持,例如 gRPC、Kafka、HTTP 等
通过 TraceContext 中核心信息的传递来实现的

在这里插入图片描述


2、集成 SpringCloud Sleuth

一、代码集成

原则:保证微服务存在跨进程通信,否则意义不大

pom文件加入依赖

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

二、日志信息

(1)、控制台变化

在这里插入图片描述

(2)、自定义输出

使用brave进行代码输出

@Slf4j
@Service
public class SleuthTraceInfoService {

    /** brave.Tracer 跟踪对象 */
    private final Tracer tracer;

    public SleuthTraceInfoService(Tracer tracer) {
        this.tracer = tracer;
    }

    /**
     * <h2>打印当前的跟踪信息到日志中</h2>
     * */
    public void logCurrentTraceInfo() {

        log.info("Sleuth trace id: [{}]", tracer.currentSpan().context().traceId());
        log.info("Sleuth span id: [{}]", tracer.currentSpan().context().spanId());
    }
}

三、设置采样率、抽样收集策略

(1)、概要

需要做好权衡
1.收集的跟踪信息越多,越能反映出系统的实际运行情况
2.高并发场景下,大量的请求调用会产生海量的跟踪日志信息,性能开销太大
控制台变化中,有个布尔类型的值,就是是否收集到zipkin的标识
在这里插入图片描述

(2)、抽样策略

(1)、ProbabilityBasedSampler 采样率策路
1.默认使用的策略, 以请求百分比的方式配置和收集跟踪信息: 它的默认值为 0.1,代表收集 10% 的请求跟踪信息
2.spring.sleuth.sampler.probability=0.5
(2)、RateLimitingSampler 抽样策略
1.限速采集,也就是说它可以用来限制每秒追踪请求的最大数量,优先级更高
2.spring.sleuth.sampler.rate=10

(3)、yml文件配置
spring:
	sleuth:
	    sampler:
	      # ProbabilityBasedSampler 抽样策略
	      probability: 1.0  # 采样比例, 1.0 表示 100%, 默认是 0.1
	      # RateLimitingSampler 抽样策略, 设置了限速采集, spring.sleuth.sampler.probability 属性值无效
	      rate: 100  # 每秒间隔接受的 trace 量
(4)、代码配置
@Configuration
public class SamplerConfig {

    /**
     * <h2>限速采集</h2>
     * */
    @Bean
    public Sampler sampler() {
        return RateLimitingSampler.create(100);
    }

    /**
     * <h2>概率采集, 默认的采样策略, 默认值是 0.1</h2>
     * */
    @Bean
    public Sampler defaultSampler() {
        return ProbabilityBasedSampler.create(0.5f);
    }
}

3、集成 SpringCloud Zipkin

一、搭建Zipkin Server

(1)、步骤

SpringCloud Finchley 版本(包含)之后,官官方不建议自己搭建 Zipkin-Server提供了已经打包好的jar
文件(SpringBoot工程),直接下载启动即可

下载地址:curl -sSL https://zipkin.io/quickstart.sh | bash -s
1.选择自己需要的版本,我的是 zipkin-server-2.21.7-exec.jar
2.选择 exec.jar 结尾的 jar

(2)、运行
(一)、简单执行
java -jar zipkin-server-2.21.7-exec.jar
(二)、使用mysql存储执行

MySQL 中添加数据表:
https://github.com/openzipkin/zipkin/blob/master/zipkin-storage/mysql-v1/src/main/resources/mysql.sql

java -jar zipkin-server-2.21.7-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 
--MYSQL_TCP_PORT=3306 --MYSQL_USER=root --MYSQL_PASS=root --MYSQL_DB=whqn_zipkin
(三)、使用kafka上报跟踪数据执行

步骤一、安装并运行kafka(不详细叙述)

下载 Kafka :https://kafka.apache.org/quickstart
解压、启动 ZK 和 Kafka Server 即可(使用默认配置 )

java -DKAFKA_BOOTSTRAP_SERVERS=127.0.0.1:9092 -jar zipkin-server-2.21.7-exec.jar 
--STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 
--MYSQL_TCP_PORT=3306 --MYSQL_USER=root --MYSQL_PASS=root --MYSQL_DB=whqn_zipkin

二、SpringCloud Sleuth 整合 Zipkin

(1)、pom文件加入依赖
<!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
(2)、yml文件加入配置
spring:
  kafka:
    bootstrap-servers: 127.0.0.1:9092
    producer:
      retries: 3
    consumer:
      auto-offset-reset: latest
  zipkin:
    sender:
      type: kafka # 默认是 web
    base-url: http://localhost:9411/
(3)、运行调用普通查询服务后的结果

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


三、微服务通信


1、通信方案

(1)、RPC

核心思想
全局注册表:将 RPC 支持的所有方法都注册进去
通过将 Java 对象进行编码(IDL json,xml 等等)+方法名传递( TCP/IP 协议)到目标服务器实现微服务通信
地址表:Server 的地址一般存储于 Zookeeper上
在这里插入图片描述

优点:

  • 目前市面上最流行的 RPC框架有:gRPC、Thrift、Dubbo,有较多的选择性。
  • 速度快、并发性能高 基于TCP进行通信

缺点:

  • 实现复杂(相对 Rest而言),需要做的工作与维护上更多(需要引入和维护 ZK)

(2)、 HTTP

核心思想
标准化的 HTTP 协议(GET、POST、PUT、DELETE 等),目前主流的微服务框架通信实现都是 HTTP
简单、标准,需要做的工作和维护工作少;几乎不需要做额外的工作即可与其他的微服务集成

(3)、 Message

核心思想
通过 Kafka、RocketMO等消息队列实现消息的发布与订阅(消费)
可以实现“削峰填谷”,缓冲机制实现数据、任务处理
缺点:只能够做到最终一致性,而不能做到实时一致性;当然,这也是看业务需求

(4)、 如何选择

  1. SpringCloud 建议的通信方案是 OpenFeign(Rest )
  2. 需要最终一致性且不要求快速响应的业务场景可以选择使用Message
  3. SpringCloud 要有足够强的理由说明你为什么要使用 RPC

2、RestTemplate通信

(1)、 两种实现方式

(一)、在代码(或配置文件中)写死IP 和 端口号
public JwtToken getTokenFromAuthorityService(UsernameAndPassword usernameAndPassword) 
{
        String requestUrl = "http://127.0.0.1:7000/ecommerce-authority-center" +
                "/authority/token";
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return new RestTemplate().postForObject(
                requestUrl,
                new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
                JwtToken.class //反序列化
        );
    }
(二)、通过注册中心获取服务地址

可以实现负载均衡的效果

在这里插入图片描述

//注入LoadBalancerClient实现负载均衡选择服务实例
private final LoadBalancerClient loadBalancerClient;

public UseRestTemplateService(LoadBalancerClient loadBalancerClient) {
	this.loadBalancerClient = loadBalancerClient;
}

public JwtToken getTokenFromAuthorityServiceWithLoadBalancer(
            UsernameAndPassword usernameAndPassword
    ) {

        //通过注册中心拿到服务的信息(是所有的实例), 再去发起调用
        ServiceInstance serviceInstance = loadBalancerClient.choose(
                CommonConstant.AUTHORITY_CENTER_SERVICE_ID //服务名称
        );

        String requestUrl = String.format(
                "http://%s:%s/ecommerce-authority-center/authority/token",
                serviceInstance.getHost(),
                serviceInstance.getPort()
        );

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return new RestTemplate().postForObject(
                requestUrl,
                new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
                JwtToken.class
        );
    }

3、SpringCloud Netflix Ribbon 通信

提供基于 RestTemplate 的 HTTP 客户端并且支持服务负载均衡功能

(1)、 原理解读

在这里插入图片描述

(2)、 手动实现

public JwtToken thinkingInRibbon(UsernameAndPassword usernameAndPassword) {

        String urlFormat = "http://%s/ecommerce-authority-center/authority/token";

        // 1. 找到服务提供方的地址和端口号
        List<ServiceInstance> targetInstances = discoveryClient.getInstances(
                CommonConstant.AUTHORITY_CENTER_SERVICE_ID
        );

        // 构造 Ribbon 服务列表
        List<Server> servers = new ArrayList<>(targetInstances.size());
        targetInstances.forEach(i -> {
            servers.add(new Server(i.getHost(), i.getPort()));
            log.info("found target instance: [{}] -> [{}]", i.getHost(), i.getPort());
        });

        // 2. 使用负载均衡策略实现远端服务调用
        // 构建 Ribbon 负载实例
        BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder()
                .buildFixedServerListLoadBalancer(servers);
        // 设置负载均衡策略
        loadBalancer.setRule(new RetryRule(new RandomRule(), 300));

        //命令模式使用负载均衡器构造请求,提交之后,阻塞第一个请求,直到返回结果
        String result = LoadBalancerCommand.builder().withLoadBalancer(loadBalancer)
                .build().submit(server -> {
					//将服务名称替换为IP和端口
                    String targetUrl = String.format(
                            urlFormat,
                            String.format("%s:%s", server.getHost(), server.getPort())
                    );
                    log.info("target request url: [{}]", targetUrl);

                    HttpHeaders headers = new HttpHeaders();
                    headers.setContentType(MediaType.APPLICATION_JSON);

                    String tokenStr = new RestTemplate().postForObject(
                            targetUrl,
                            new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
                            String.class
                    );

                    return Observable.just(tokenStr);//异步发射字符串,不阻塞会导致无法返回值

                }).toBlocking().first().toString();//阻塞请求,直到完成响应

        return JSON.parseObject(result, JwtToken.class);
    }

(3)、 实战应用

  1. pom 文件中引入 Ribbon 依赖
<!-- Ribbon -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
  1. 增强 RestTemplate,添加 @LoadBalanced 注解,使之具备负载均衡的能力
@Component
public class RibbonConfig {
    /**
     * <h2>注入 RestTemplate</h2>
     * */
    @Bean
    @LoadBalanced //启动时给ribbon扫描到,才能增强,不能New出来
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
  1. 编写代码
public JwtToken getTokenFromAuthorityServiceByRibbon(
            UsernameAndPassword usernameAndPassword) {

        // 注意到 url 中的 ip 和端口换成了服务名称
        String requestUrl = String.format(
                "http://%s/ecommerce-authority-center/authority/token",
                CommonConstant.AUTHORITY_CENTER_SERVICE_ID //服务名称
        );
        log.info("login request url and body: [{}], [{}]", requestUrl,
                JSON.toJSONString(usernameAndPassword));

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // 这里一定要使用自己注入的 RestTemplate
        return restTemplate.postForObject(
                requestUrl,
                new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
                JwtToken.class
        );
    }

4、SpringCloud OpenFeign 通信

只需要使用注解和接口的配置即可完成对服务提供方的接口绑定

(1)、 实战应用

  1. pom 文件中引入 Ribbon 依赖
<!-- open feign -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 添加 @EnableFeignClients 注解,启用 open-feign
/**
 * <h1>与 Authority 服务通信的 Feign Client 接口定义</h1>
 * */
@FeignClient(
        contextId = "AuthorityFeignClient", value = "e-commerce-authority-center"
)
public interface AuthorityFeignClient {

    @RequestMapping(value = "/ecommerce-authority-center/authority/token",
            method = RequestMethod.POST,
            consumes = "application/json", produces = "application/json")
    JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword);
    
}

(2)、 配置应用

(一)、开启 gzip 压缩,可以压缩请求体以及响应体
feign:
  # feign 开启 gzip 压缩
  compression:
    request:
      enabled: true
      mime-types: text/xml,application/xml,application/json
      min-request-size: 1024
    response:
      enabled: true

(二)、日志、重试、请求连接和响应时间限制
/**
 * <h1>OpenFeign 配置类</h1>
 * */
@Configuration
public class FeignConfig {

    /**
     * <h2>开启 OpenFeign 日志</h2>
     * */
    @Bean
    public Logger.Level feignLogger() {
        return Logger.Level.FULL;   //  需要注意, 日志级别需要修改成 debug
    }

    /**
     * <h2>OpenFeign 开启重试</h2>
     * period = 100 发起当前请求的时间间隔,请求失败后多少秒进行重试, 单位是 ms
     * maxPeriod = 1000 发起当前请求的最大时间间隔,由于http请求存在延迟,所以应该是个区间, 单位是 ms
     * maxAttempts = 5 最多请求次数
     * */
    @Bean
    public Retryer feignRetryer() {
        return new Retryer.Default(
                100,
                SECONDS.toMillis(1),
                5
        );
    }

    public static final int CONNECT_TIMEOUT_MILLS = 5000;
    public static final int READ_TIMEOUT_MILLS = 5000;

    /**
     * <h2>对请求的连接和响应时间进行限制</h2>
     * */
    @Bean
    public Request.Options options() {

        return new Request.Options(
                CONNECT_TIMEOUT_MILLS, TimeUnit.MICROSECONDS,//毫秒
                READ_TIMEOUT_MILLS, TimeUnit.MILLISECONDS,
                true
        );
    }
}
(三)、 使用 okhttp 替换 httpclient

将默认的urlconnetion的httpclient替换为okhttp

  1. pom配置okhttp依赖
<!-- feign 替换 JDK 默认的 URLConnection 为 okhttp -->
<dependency>
	<groupId>io.github.openfeign</groupId>
	<artifactId>feign-okhttp</artifactId>
</dependency>
  1. yml配置
feign:
  # feign 开启 gzip 压缩
  compression:
    request:
      enabled: true
      mime-types: text/xml,application/xml,application/json
      min-request-size: 1024
    response:
      enabled: true
  # 禁用默认的 http, 启用 okhttp
  httpclient:
    enabled: false
  okhttp:
    enabled: true

  1. 自定义配置
/**
 * <h1>OpenFeign 使用 OkHttp 配置类</h1>
 * */
@Configuration
@ConditionalOnClass(Feign.class) //有Feign才去配置
@AutoConfigureBefore(FeignAutoConfiguration.class) //需要在自动配置之前完成配置,提前初始化
public class FeignOkHttpConfig {

    /**
     * <h2>注入 OkHttp, 并自定义配置</h2>
     * */
    @Bean
    public okhttp3.OkHttpClient okHttpClient() {

        return new OkHttpClient.Builder()
                .connectTimeout(5, TimeUnit.SECONDS)    // 设置连接超时
                .readTimeout(5, TimeUnit.SECONDS)   // 设置读超时
                .writeTimeout(5, TimeUnit.SECONDS)  // 设置写超时
                .retryOnConnectionFailure(true)     // 是否自动重连
                // 配置连接池中的最大空闲线程个数为 10, 并保持 5 分钟
                // 让效率升高
                .connectionPool(new ConnectionPool(
                        10, 5L, TimeUnit.MINUTES))
                .build();
    }
}

(3)、 实现原理

1、将接口通过动态代理生成实现类
2、使用Contract将注解解析成feign的规则,原生api不完全兼容springcloud的请求注解
3、在Feign代理对象的方法被调用时,Feign的InvocationHandler会拦截所有方法调用。它会提取方法上的注解(如路径、请求方法、请求参数等),并将这些信息用于构造HTTP请求。
4、将对象包装成消息体
5、拦截器装饰:例如 添加的gzip配置,超时配置等
6、日志记录
7、使用http框架发送请求上面可替换为http,Feign会利用预先配置的HTTP客户端发送请求,返回响应并使用解码器将响应转换为Java对象。

在这里插入图片描述

(4)、 手动实现

使用 Feign 的原生 Api, 而不是 OpenFeign ( Feign + Ribbon)

private final DiscoveryClient discoveryClient;

public UseFeignApi(DiscoveryClient discoveryClient) {
	this.discoveryClient = discoveryClient;
}

public JwtToken thinkingInFeign(UsernameAndPassword usernameAndPassword) {

        // 通过反射去拿 serviceId
        String serviceId = null;
        Annotation[] annotations = AuthorityFeignClient.class.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType().equals(FeignClient.class)) {
                serviceId = ((FeignClient) annotation).value();
                break;
            }
        }

        // 如果服务 id 不存在, 直接抛异常
        if (null == serviceId) {
            throw new RuntimeException("can not get serviceId");
        }

        // 通过 serviceId 去拿可用服务实例
        List<ServiceInstance> targetInstances = discoveryClient.getInstances(serviceId);
        if (CollectionUtils.isEmpty(targetInstances)) {
            throw new RuntimeException("can not get target instance from serviceId: " +
                    serviceId);
        }

        // 随机选择一个服务实例: 负载均衡
        ServiceInstance randomInstance = targetInstances.get(
                new Random().nextInt(targetInstances.size())
        );
        log.info("choose service instance: [{}], [{}], [{}]", serviceId,
                randomInstance.getHost(), randomInstance.getPort());

        // Feign 客户端初始化, 必须要配置 encoder、decoder、contract
        // 由于原生的encoder和decodr不支持对象
        //Feign 的路径拼接工作确实是由 Contract 和 InvocationHandler 共同完成的
        //Contract 解析
        //InvocationHandler 拼接目标路径
        AuthorityFeignClient feignClient = Feign.builder()  // 1. Feign 默认配置初始化
                .encoder(new GsonEncoder())                 // 2.1 设置自定义配置
                .decoder(new GsonDecoder())                 // 2.2 设置自定义配置
                .logLevel(Logger.Level.FULL)                // 2.3 设置自定义配置
                .contract(new SpringMvcContract())			// 配置contract对注解进行解析
                .target(                                    // 3 生成代理对象
                        AuthorityFeignClient.class,
                        String.format("http://%s:%s",
                                randomInstance.getHost(), randomInstance.getPort())
                );
		//代理对象调用时会被InvocationHandler拦截,并发起请求
        return feignClient.getTokenByFeign(usernameAndPassword);
    }

5、SpringCloud Stream 消息通信

(1)、 概要

如果没有SpringCloud Stream,当我们需要切换消息中间件时,难以变更

在这里插入图片描述

核心概念:

  • Binder:负责与中间件交互的抽象绑定器,与外部消息系统通信,屏蔽了底层中间件的使用细节
  • Input:发送消息的应用通信信道,通过发送信道向Channel发送消息
  • Output:接收消息的应用通信信道,通过接收信道向Channel接受消息
  • Spring Messaging:统一消息的编程模型,Channel存放消息
  • 消息分类映射为通信信道,可以为不同类的消息自定义通信信道
    在这里插入图片描述

发布订阅模型:与kafka类似

  • Topic 可以认为就是 Kafka 中的 Topic 概念
  • Producer 通过 Input 信道发布消息到 Topic 上
  • Consumer 通过 Output 信道消费 Topic 上的消息

在这里插入图片描述


(2)、 实战应用

pom文件添加依赖,用不同的消息中间件需要不同的绑定器进行绑定

		<!-- SpringCloud Stream-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream</artifactId>
        </dependency>
        <!-- SpringCloud Stream + Kafka -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-kafka</artifactId>
        </dependency>
        <!-- SpringCloud Stream + RocketMQ -->
        <!--        <dependency>-->
        <!--            <groupId>com.alibaba.cloud</groupId>-->
        <!--            <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>-->
        <!--        </dependency>-->

配置文件进行配置

spring:
    # 消息驱动的配置
    stream:
      # SpringCloud Stream + Kafka
      kafka:
        binder:
          brokers: 127.0.0.1:9092
          auto-create-topics: true  # 如果设置为false, 就不会自动创建Topic, 你在使用之前需要手动创建好
      # SpringCloud Stream + RocketMQ
      #      rocketmq:
      #        binder:
      #          name-server: 127.0.0.1:9876
    
      bindings:
        # 默认发送方
        output:      # 这里用 Stream 给我们提供的默认 output 信道
          destination: ecommerce-stream-client-default    # 消息发往的目的地, Kafka 中就是 Topic
          content-type: text/plain    # 消息发送的格式, 接收端不用指定格式, 但是发送端要

        # 默认接收方
        input:      # 这里用 Stream 给我们提供的默认 input 信道
          destination: ecommerce-stream-client-default

创建自定义消息对象

@Data
@NoArgsConstructor
@AllArgsConstructor
public class WhqnMessage {

    private Integer id;
    private String projectName;
    private String org;
    private String author;

    /**
     * <h2>返回一个默认的消息, 方便使用</h2>
     * */
    public static WhqnMessage defaultMessage() {

        return new WhqnMessage(
                1,
                "e-commerce-stream-client",
                "imooc.com",
                "Whqn"
        );
    }
}

使用默认的通信信道实现消息的发送
绑定默认的通信信道:Source 进行通信

@Slf4j
@EnableBinding(Source.class)
public class DefaultSendService {

    private final Source source;

    public DefaultSendService(Source source) {
        this.source = source;
    }

    /**
     * <h2>使用默认的输出信道发送消息</h2>
     * */
    public void sendMessage(WhqnMessage message) {

        String _message = JSON.toJSONString(message);
        log.info("in DefaultSendService send message: [{}]", _message);

        // Spring Messaging, 统一消息的编程模型, 是 Stream 组件的重要组成部分之一
        source.output().send(MessageBuilder.withPayload(_message).build());
    }
}

使用默认的信道实现消息的接收

@Slf4j
@EnableBinding(Sink.class)
public class DefaultReceiveService {

    /**
     * <h2>使用默认的输入信道接收消息</h2>
     * */
    @StreamListener(Sink.INPUT)
    public void receiveMessage(Object payload) {

        log.info("in DefaultReceiveService consume message start");
        WhqnMessage whqnMessage = JSON.parseObject(
                payload.toString(), WhqnMessage.class
        );
        // 消费消息
        log.info("in DefaultReceiveService consume message success: [{}]",
                JSON.toJSONString(whqnMessage));
    }
}

(3)、 自定义通信信道

自定义输出信道名称,需要再yml中进行声明

public interface WhqnSource {

    String OUTPUT = "whqnOutput";

    /** 输出信道的名称是 whqnOutput, 需要使用 Stream 绑定器在 yml 文件中声明 */
    @Output(WhqnSource.OUTPUT)
    MessageChannel whqnOutput(); //统一消息的编程模型通道进行输出,向MessageChannel发送消息
}

构建消息的发送服务,与默认的通信信道一样

@Slf4j
@EnableBinding(WhqnSource.class)
public class WhqnSendService {

    private final WhqnSource whqnSource;

    public WhqnSendService(WhqnSource whqnSource) {
        this.whqnSource = whqnSource;
    }

    /**
     * <h2>使用自定义的输出信道发送消息</h2>
     * */
    public void sendMessage(WhqnMessage message) {

        String _message = JSON.toJSONString(message);
        log.info("in WhqnSendService send message: [{}]", _message);
        whqnSource.whqnOutput().send(
                MessageBuilder.withPayload(_message).build()
        );
    }
}

自定义输入信道名称,订阅输出信道,获取channel传来到输出信道的消息

public interface WhqnSink {

    String INPUT = "whqnInput";

    /** 输入信道的名称是 whqnInput, 需要使用 Stream 绑定器在 yml 文件中配置*/
    @Input(WhqnSink.INPUT)
    SubscribableChannel whqnInput();
}

使用自定义的输入信道实现消息的接收

@Slf4j
@EnableBinding(WhqnSink.class)
public class WhqnReceiveService {

    /** 使用自定义的输入信道接收消息 */
    @StreamListener(WhqnSink.INPUT) //spring框架自动扫描注解
    public void receiveMessage(@Payload Object payload) {

        log.info("in WhqnReceiveService consume message start");
        WhqnMessage whqnMessage = JSON.parseObject(payload.toString(), WhqnMessage.class);
        log.info("in WhqnReceiveService consume message success: [{}]",
                JSON.toJSONString(whqnMessage));
    }
}

配置文件进行绑定,上面的通信信道在配置文件中进行绑定,使用上述代码中的接口进行发布与订阅

spring:
    # 消息驱动的配置
    stream:
      bindings:
        # 可以像不同类型的topic发
        # Whqn 发送方
        whqnOutput:
          destination: ecommerce-stream-client-whqn
          content-type: text/plain
        # Whqn 接收方
        whqnInput:
          destination: ecommerce-stream-client-whqn

第一步:先在接口中定义通信信道名称
第二步:再配置文件中绑定通信信道
第三步:使用通信信道进行发送或接收消息


(4)、 消费分组

在这里插入图片描述

1、应用的不同实例放在一个消费者组中,每一条消息只会被一个实例消费
2、消费者组的思想是通过多实例扩展服务吞吐量,且不会造成消息的重复消费
3、不分配则每个消费者各自一个匿名消费组,会重复处理

直接在配置文件中定义消费组

spring:
    # 消息驱动的配置
    stream:
      bindings:
        input:
          group: e-commerce-whqn-default #不分配则每个消费者各自一个匿名消费组,会重复处理

(5)、 消费分区

为了确保具有共同特征标识的数据由同一个消费者实例进行处理

解决问题:

引入配置

spring:
    # 消息驱动的配置
    stream:
      instanceCount: 1  # 消费者的总数
      instanceIndex: 0  # 当前消费者的索引,从第一个索引开始分配
      bindings:
        # 默认发送方
        output:
          # 消息分区
          producer:
            partitionCount: 1   # 分区大小
  • 第一步:配置消费者实例个数与分区个数,形成一对一的对应关系

例如:
假设你有 3 个分区和 3 个消费者实例,instanceIndex 将决定每个消费者实例被分配到哪个分区
instanceIndex = 0 会被分配到分区 0,instanceIndex = 1 会被分配到分区 1,instanceIndex = 2 会被分配到分区 2。

  • 第二步:接收方配置分区支持
spring:
    # 消息驱动的配置
    stream:
      bindings:
        # 默认接收方
        input:      # 这里用 Stream 给我们提供的默认 input 信道
          # 消费者开启分区支持
          consumer:
            partitioned: true
  • 第三步:配置分区关键字(默认配置)
spring:
    # 消息驱动的配置
    stream:
      bindings:
        # 默认发送方
        output:
          # 消息分区
          producer:
            # 分区关键字, payload 指的是发送的对象, author 是对象中的属性
            partitionKeyExpression: payload.author  
  • 第四步:配置分区关键字(自定义配置)
spring:
    # 消息驱动的配置
    stream:
      bindings:
        # 默认发送方
        output:
          # 消息分区
          producer:
            # 使用自定义的分区策略, 注释掉 partitionKeyExpression,需用注解配置
            partitionKeyExtractorName: whqnPartitionKeyExtractorStrategy
            partitionSelectorName: whqnPartitionSelectorStrategy

自定义从 Message 中提取 partition key 的策略

@Slf4j
@Component
public class WhqnPartitionKeyExtractorStrategy implements PartitionKeyExtractorStrategy {

    @Override //接收message的消息,以此进行发送过滤分区
    public Object extractKey(Message<?> message) {

        WhqnMessage whqnMessage = JSON.parseObject(
                message.getPayload().toString(), WhqnMessage.class
        );
        // 自定义提取 key
        String key = whqnMessage.getProjectName();
        log.info("SpringCloud Stream Whqn Partition Key: [{}]", key);
        return key;
    }
}

决定 message 发送到哪个分区的策略

@Slf4j
@Component
public class WhqnPartitionSelectorStrategy implements PartitionSelectorStrategy {

    /**
     * <h2>选择分区的策略</h2>
     * */
    @Override
    public int selectPartition(Object key, int partitionCount) {
		//自定义配置策略
        int partition = key.toString().hashCode() % partitionCount;
        log.info("SpringCloud Stream Whqn Selector info: [{}], [{}], [{}]",
                key.toString(), partitionCount, partition);

        return partition;
    }
}

四、Hystrix


1、概览

概要与设计目标

Hystrix 是一个库,通过添加延迟容忍容错逻辑,帮助你控制这些分布式服务之间的交互
Hystrix 通过隔离服务之间的访问点、停止级联失败和提供回退选项来实现服务之间的容错

  • 1.对客户端访问的延迟和故障进行保护和控制

  • 2.隔离在复杂的分布式系统中阻止级联故障

  • 3.快速失败,快速恢复

  • 4.兜底回退,尽可能优雅的降级

解决的问题
服务之间存在许多依赖项,依赖项可能会存在故障,如果不做故障隔离整个服务可能被拖垮

在这里插入图片描述

如何解决

  • 对依赖项(服务)进行包装代理,不直接与依赖项交互
  • 调用超时时间允许自行设定,超时之后立刻熔断报错
  • 每一个依赖项都在自己的空间内(线程池或信号量隔离),依赖项之间不存在干扰
  • 请求依赖项失败后,可以选择出错或者是兜底回退

工作流程

  1. 构造一个 HystrixCommand 或者 HystrixObservableCommand 对象

  2. 执行命令获取响应
    execute():阻塞,直到收到响应或者抛出异常
    queue():返回一个 Future
    observe():订阅代表响应的 Observable
    toObservable():返回一个 Observable,当你订阅它以后,将会执行Hystrix 命令并且推送它的响应

  3. 响应是否已经被缓存,已缓存则立即返回

  4. 断路器是否打开,如果打开,则执行回退逻辑

  5. 线程池、信号量是否已经使用率 100%,是的话直接执行回退逻辑

  6. HystrixObservableCommand.construct() or HystrixCommand.run()

  7. 计算电路健康,统计数据决定是否跳闸

  8. 回退或返回成功响应


2、三种模式

(1)、断路器模式

设置超时或者失败等熔断策略
在这里插入图片描述

(2)、后备策略模式

断路器模式触发之后,如果存在后备策略,则执行后备
在这里插入图片描述

(3)、船壁模式

通过线程池管理调用外部资源的,默认情况下所有服务调用都公用一个线程池
从公用一个线程池,变为每个远程服务使用自己的私有线程池

在这里插入图片描述


3、实现容错降级


(1)、注解形式

pom文件

<!-- 集成 hystrix -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

启动类加入注解

@EnableCircuitBreaker   // 启动 Hystrix

使用命令模式包装容错降级方法

@Slf4j
@Service
public class UseHystrixCommandAnnotation {

    private final NacosClientService nacosClientService;

    public UseHystrixCommandAnnotation(NacosClientService nacosClientService) {
        this.nacosClientService = nacosClientService;
    }

    @HystrixCommand(
            // 用于对 Hystrix 命令进行分组, 分组之后便于统计展示于仪表盘、上传报告和预警等等
            // 内部进行度量统计时候的分组标识, 数据上报和统计的最小维度就是 groupKey
            groupKey = "NacosClientService",
            // HystrixCommand 的名字, 默认是当前类的名字, 主要方便 Hystrix 进行监控、报警等
            commandKey = "NacosClientService",
            // 舱壁模式
            threadPoolKey = "NacosClientService",
            // 后备模式
            fallbackMethod = "getNacosClientInfoFallback",
            // 断路器模式
            commandProperties = {
                    // 超时时间, 单位毫秒, 超时进 fallback
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500"),
                    // 判断熔断的最少请求数, 默认是10; 只有在一定时间内请求数量达到该值, 才会进行成功率的计算
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
                    // 熔断的阈值默认值 50, 表示在一定时间内有50%的请求处理失败, 会触发熔断
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10"),
            },
            // 舱壁模式
            threadPoolProperties = {
                    @HystrixProperty(name = "coreSize", value = "30"),
                    @HystrixProperty(name = "maxQueueSize", value = "101"),
                    @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
                    // 在时间窗口中, 收集统计信息的次数; 在 1440ms 的窗口中一共统计 12 次
                    @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
                    // 时间窗口, 从监听到第一次失败开始计时
                    @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
            }
    )
    public List<ServiceInstance> getNacosClientInfo(String serviceId) {

        log.info("use hystrix command annotation to get nacos client info: [{}], [{}]",
                serviceId, Thread.currentThread().getName());
        return nacosClientService.getNacosClientInfo(serviceId);
    }

    /**
     * <h2>getNacosClientInfo 的兜底策略 - Hystrix 后备模式</h2>
     * */
    public List<ServiceInstance> getNacosClientInfoFallback(String serviceId) {

        log.warn("can not get nacos client, trigger hystrix fallback: [{}], [{}]",
                serviceId, Thread.currentThread().getName());
        return Collections.emptyList();
    }
}

被包装的方法

public List<ServiceInstance> getNacosClientInfo(String serviceId) {

        // 测试 UseHystrixCommandAnnotation 的超时
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ex) {
            //
        }

        log.info("request nacos client to get service instance info: [{}]", serviceId);
        return discoveryClient.getInstances(serviceId);
    }

调用方式

    @GetMapping("/hystrix-command-annotation")
    public List<ServiceInstance> getNacosClientInfoUseAnnotation(
            @RequestParam String serviceId) {
        log.info("request nacos client info use annotation: [{}], [{}]",
                serviceId, Thread.currentThread().getName());
        return hystrixCommandAnnotation.getNacosClientInfo(serviceId);
    }

运行结果

在这里插入图片描述

controller中的方法、hystrix注释的方法、后备模式的方法使用的不同的线程池


(2)、编码形式(默认使用线程池)

创建继承HystrixCommand的类 给 NacosClientService 实现包装
线程隔离:Hystrix 可以将 run() 方法的执行放在一个单独的线程中,避免耗时操作阻塞主线程。每个命令的执行会在独立的线程池中执行,最大程度避免高并发情况下的阻塞和影响。
信号量隔离:如果使用信号量隔离模式,run() 方法会被执行在调用线程中,通过信号量控制并发请求的数量,避免系统过载。

//泛型指定为被包装的服务的返回值类型
public class NacosClientHystrixCommand extends HystrixCommand<List<ServiceInstance>> {

    /** 需要保护的服务 */
    private final NacosClientService nacosClientService;

    /** 方法需要传递的参数 */
    private final String serviceId;

    public NacosClientHystrixCommand(NacosClientService nacosClientService, String serviceId) {

        super(
                Setter.withGroupKey(
                        HystrixCommandGroupKey.Factory.asKey("NacosClientService"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("NacosClientHystrixCommand"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("NacosClientPool"))
                // 线程池 key 配置
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                        .withExecutionIsolationStrategy(THREAD) // 线程池隔离策略
                        .withFallbackEnabled(true)  // 开启降级
                        .withCircuitBreakerEnabled(true)    // 开启熔断器
                )
        );

        // 可以配置信号量隔离策略
//        Setter semaphore =
//                Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("NacosClientService"))
//                .andCommandKey(HystrixCommandKey.Factory.asKey("NacosClientHystrixCommand"))
//                .andCommandPropertiesDefaults(
//                        HystrixCommandProperties.Setter()
//                        .withCircuitBreakerRequestVolumeThreshold(10) //设置当断路器触发时,至少需要的请求数量(请求量阈值)
//                        .withCircuitBreakerSleepWindowInMilliseconds(5000) //断路器进入休眠状态后的时间
//                        .withCircuitBreakerErrorThresholdPercentage(50) //如果 10 个请求中有 5 个失败,断路器就会打开。
//                        .withExecutionIsolationStrategy(SEMAPHORE)  // 指定使用信号量隔离
//                        //.....
//                );

        this.nacosClientService = nacosClientService;
        this.serviceId = serviceId;
    }

    /**
     * <h2>要保护的方法调用写在 run 方法中</h2>
     * */
    @Override
    protected List<ServiceInstance> run() throws Exception {

        log.info("NacosClientService In Hystrix Command to Get Service Instance: [{}], [{}]",
                this.serviceId, Thread.currentThread().getName());
        return this.nacosClientService.getNacosClientInfo(this.serviceId);
    }

    /**
     * <h2>降级处理策略</h2>
     * */
    @Override
    protected List<ServiceInstance> getFallback() {

        log.warn("NacosClientService run error: [{}], [{}]",
                this.serviceId, Thread.currentThread().getName());
        return Collections.emptyList();
    }
}

首先注入Service


    private final NacosClientService nacosClientService;


    public HystrixController(NacosClientService nacosClientServicecacheHystrixCommandAnnotation) {
        this.nacosClientService = nacosClientService;
    }

调用方式一

List<ServiceInstance> serviceInstances01 = new NacosClientHystrixCommand(
                nacosClientService, serviceId
        ).execute();    // 同步阻塞 相当于方式二的queue+future

调用方式二

List<ServiceInstance> serviceInstances02;
        Future<List<ServiceInstance>> future = new NacosClientHystrixCommand(
                nacosClientService, serviceId
        ).queue();      // 异步非阻塞,获取将来执行的结果,这里发起调用
        // 这里可以做一些别的事, 需要的时候再去拿结果
serviceInstances02 = future.get();

调用方式三

Observable<List<ServiceInstance>> observable = new NacosClientHystrixCommand(
                nacosClientService, serviceId
        ).observe();        // 热响应调用,创建线程
//直到执行toBlocking()阻塞才发起调用
List<ServiceInstance> serviceInstances03 = observable.toBlocking().single();

调用方式四

Observable<List<ServiceInstance>> toObservable = new NacosClientHystrixCommand(
                nacosClientService, serviceId
        ).toObservable();        // 异步冷响应调用,不创建线程
//创建线程,直到执行toBlocking()阻塞才发起调用
List<ServiceInstance> serviceInstances04 = toObservable.toBlocking().single();

测试熔断的方法

    public List<ServiceInstance> getNacosClientInfo(String serviceId) {
        // 测试 NacosClientHystrixCommand 熔断
        throw new RuntimeException("has some error");
        log.info("request nacos client to get service instance info: [{}]", serviceId);
        return discoveryClient.getInstances(serviceId);
    }

运行结果,以下的日志结果,在上述代码中省略,需要自己编写

在这里插入图片描述


(3)、编码形式(默认使用信号量隔离)

创建继承HystrixObservableCommand的类 给 NacosClientService 实现包装

@Slf4j
public class NacosClientHystrixObservableCommand
        extends HystrixObservableCommand<List<ServiceInstance>> {

    /** 要保护的服务 */
    private final NacosClientService nacosClientService;

    /** 方法需要传递的参数,信号量隔离可以使用多个参数 */
    private final List<String> serviceIds;

	//类似线程池配置
    public NacosClientHystrixObservableCommand(NacosClientService nacosClientService,
                                               List<String> serviceIds) {
        super(
                HystrixObservableCommand.Setter
                        .withGroupKey(HystrixCommandGroupKey
                                .Factory.asKey("NacosClientService"))
                        .andCommandKey(HystrixCommandKey
                                .Factory.asKey("NacosClientHystrixObservableCommand"))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                        .withFallbackEnabled(true)          // 开启降级
                        .withCircuitBreakerEnabled(true)    // 开启熔断器
                )
        );

        this.nacosClientService = nacosClientService;
        this.serviceIds = serviceIds;
    }

    /**
     * <h2>要保护的方法调用写在这里,与线程池不同</h2>
     * */
    @Override
    protected Observable<List<ServiceInstance>> construct() {

        return Observable.create(new Observable.OnSubscribe<List<ServiceInstance>>() {
            // Observable 有三个关键的事件方法, 分别是 onNext、onCompleted、onError
            @Override
            public void call(Subscriber<? super List<ServiceInstance>> subscriber) {

                try {
                	//判断是否订阅,已订阅则可进行下一步发送
                    if (!subscriber.isUnsubscribed()) {
                        log.info("subscriber command task: [{}], [{}]",
                                JSON.toJSONString(serviceIds),
                                Thread.currentThread().getName());
                        serviceIds.forEach(
                        //onNext执行并将结果发送到订阅者
                                s -> subscriber
                                        .onNext(nacosClientService.getNacosClientInfo(s))
                        );
                        subscriber.onCompleted();
                        log.info("command task completed: [{}], [{}]",
                                JSON.toJSONString(serviceIds),
                                Thread.currentThread().getName());
                    }
                } catch (Exception ex) {
                    //若无后备策略,则通过onError返回一个异常信息
                    subscriber.onError(ex);
                }
            }
        });
    }

    /**
     * <h2>服务降级</h2>
     * */
    @Override
    protected Observable<List<ServiceInstance>> resumeWithFallback() {

        return Observable.create(new Observable.OnSubscribe<List<ServiceInstance>>() {
            @Override
            public void call(Subscriber<? super List<ServiceInstance>> subscriber) {

                try {
                    if (!subscriber.isUnsubscribed()) {
                        log.info("(fallback) subscriber command task: [{}], [{}]",
                                JSON.toJSONString(serviceIds),
                                Thread.currentThread().getName());
                        //后备策略是返回一个空数组
                        subscriber.onNext(Collections.emptyList());
                        subscriber.onCompleted();
                        log.info("(fallback) command task completed: [{}], [{}]",
                                JSON.toJSONString(serviceIds),
                                Thread.currentThread().getName());
                    }
                } catch (Exception ex) {
                    subscriber.onError(ex);
                }
            }
        });
    }
}

调用方式

List<String> serviceIds = Arrays.asList(serviceId, serviceId, serviceId);
List<List<ServiceInstance>> result = new ArrayList<>(serviceIds.size());

NacosClientHystrixObservableCommand observableCommand =
                new NacosClientHystrixObservableCommand(nacosClientService, serviceIds);

// 异步执行命令
//在发出一次,方法有判断是否订阅,所以不执行
Observable<List<ServiceInstance>> observe = observableCommand.observe(); 

// 从发送者获取结果
observe.subscribe(
	new Observer<List<ServiceInstance>>() {

		// 执行 onNext 之后再去执行 onCompleted
        @Override
        public void onCompleted() {
           log.info("all tasks is complete: [{}], [{}]",
           serviceId, Thread.currentThread().getName());
        }

		//处理异常情况
        @Override
        public void onError(Throwable e) {
           e.printStackTrace();
        }

		//每次收到的服务实例列表,上面每次执行onNext就能获取一次结果
        @Override
        public void onNext(List<ServiceInstance> instances) {
           result.add(instances);
        }
   }
);

log.info("observable command result is : [{}], [{}]",
JSON.toJSONString(result), Thread.currentThread().getName());

观察者订阅者编程方式实例

Observable<Integer> observable = Observable.create(emitter -> {
    emitter.onNext(1);  // 推送数据
    emitter.onNext(2);  // 推送数据
    emitter.onComplete();  // 发出完成事件
});

observable.subscribe(
    data -> System.out.println("接收到数据: " + data),  // 数据接收者(Subscriber)
    throwable -> System.out.println("发生错误: " + throwable),  // 错误处理
    () -> System.out.println("完成!")  // 完成处理
);
//一旦订阅,observable 会依次发送 1 和 2,并最终触发完成事件

运行结果:用的同一个线程池,效率较高

在这里插入图片描述


3、请求缓存


(1)、概要

Hystrix 的结果缓存指的是在一次 Hystrix 的请求上下文
请求的是同一个方法,使用同一个参数,则会缓存结果

在这里插入图片描述


(2)、初始化过滤器

过滤器,初始化 Hystrix 请求上下文环境,由于工程引入了Sleuth,这会导致缓存功能实现,需要重新对hystrix进行配置

@Slf4j
@Component
@WebFilter(
        filterName = "HystrixRequestContextServletFilter",
        urlPatterns = "/*",
        asyncSupported = true
)
public class HystrixRequestContextServletFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        // 初始化 Hystrix 请求上下文
        // 在不同的 context 中缓存是不共享的
        // 这个初始化是必须的
        HystrixRequestContext context = HystrixRequestContext.initializeContext();

        try {
            // 配置
            hystrixConcurrencyStrategyConfig();
            // 请求正常通过
            chain.doFilter(request, response);
        } finally {
            // 关闭 Hystrix 请求上下文
            context.shutdown();
        }
    }

    /**
     * <h2>配置 Hystrix 的并发策略</h2>
     * */
    public void hystrixConcurrencyStrategyConfig() {

        try {

            HystrixConcurrencyStrategy target =
                    HystrixConcurrencyStrategyDefault.getInstance();
            HystrixConcurrencyStrategy strategy =
                    HystrixPlugins.getInstance().getConcurrencyStrategy();
            //判断当前配置的类型是否是Sleuth的并发策略配置
            if (strategy instanceof HystrixConcurrencyStrategyDefault) {
                // 如果已经就是我们想要配置的
                return;
            }

            // 将原来其他的配置保存下来
            HystrixCommandExecutionHook commandExecutionHook =
                    HystrixPlugins.getInstance().getCommandExecutionHook();
            HystrixEventNotifier eventNotifier =
                    HystrixPlugins.getInstance().getEventNotifier();
            HystrixMetricsPublisher metricsPublisher =
                    HystrixPlugins.getInstance().getMetricsPublisher();
            HystrixPropertiesStrategy propertiesStrategy =
                    HystrixPlugins.getInstance().getPropertiesStrategy();

            // 先重置, 再把我们自定义的配置与原来的配置写回去
            HystrixPlugins.reset();
            //配置自定义的配置
            HystrixPlugins.getInstance().registerConcurrencyStrategy(target);
            //原来的配置写回去
            HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
            HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
            HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
            //重新注册
            HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);

            log.info("config hystrix concurrency strategy success");
        } catch (Exception ex) {
            log.error("Failed to register Hystrix Concurrency Strategy: [{}]",
                    ex.getMessage(), ex);
        }
    }
}

(3)、编码形式实现

创建继承HystrixCommand的类 给 NacosClientService 实现包装与上面不同的是需要继承响应的缓存方法
注:用编程方式,就不用注解方式,需要新建对象

@Slf4j
public class CacheHystrixCommand extends HystrixCommand<List<ServiceInstance>> {

    /** 需要保护的服务 */
    private final NacosClientService nacosClientService;

    /** 方法需要传递的参数 */
    private final String serviceId;

	//由于需要清除功能,所以需要在外部指定
    private static final HystrixCommandKey CACHED_KEY =
            HystrixCommandKey.Factory.asKey("CacheHystrixCommand");

    public CacheHystrixCommand(NacosClientService nacosClientService, String serviceId) {

        super(
                HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey
                        .Factory.asKey("CacheHystrixCommandGroup"))
                //注意:这是包装类的id,不是缓存的key
                .andCommandKey(CACHED_KEY)
        );

        this.nacosClientService = nacosClientService;
        this.serviceId = serviceId;
    }

    @Override
    protected List<ServiceInstance> run() throws Exception {

        log.info("CacheHystrixCommand In Hystrix Command to get service instance:" +
                " [{}], [{}]", this.serviceId, Thread.currentThread().getName());
        return this.nacosClientService.getNacosClientInfo(this.serviceId);
    }

	//继承这个方法即可实现缓存功能
    @Override
    protected String getCacheKey() {
        return serviceId;
    }

    @Override
    protected List<ServiceInstance> getFallback() {
        return Collections.emptyList();
    }

    /**
     * <h2>根据缓存 key 清理在一次 Hystrix 请求上下文中的缓存</h2>
     * */
    public static void flushRequestCache(String serviceId) {
		//赋值key和配置(使用的是过滤器的配置)
        HystrixRequestCache.getInstance(
                CACHED_KEY,
                HystrixConcurrencyStrategyDefault.getInstance()
        ).clear(serviceId);//调用clear清理缓存
        log.info("flush request cache in hystrix command: [{}], [{}]",
                serviceId, Thread.currentThread().getName());
    }
}

调用方式

		// 使用缓存 Command, 发起两次请求
        CacheHystrixCommand command1 = new CacheHystrixCommand(
                nacosClientService, serviceId
        );
        CacheHystrixCommand command2 = new CacheHystrixCommand(
                nacosClientService, serviceId
        );

        List<ServiceInstance> result01 = command1.execute();
        List<ServiceInstance> result02 = command2.execute();
        log.info("result01, result02: [{}], [{}]",
                JSON.toJSONString(result01), JSON.toJSONString(result02));

        // 清除缓存
        CacheHystrixCommand.flushRequestCache(serviceId);

        // 使用缓存 Command, 发起两次请求
        CacheHystrixCommand command3 = new CacheHystrixCommand(
                nacosClientService, serviceId
        );
        CacheHystrixCommand command4 = new CacheHystrixCommand(
                nacosClientService, serviceId
        );

        List<ServiceInstance> result03 = command3.execute();
        List<ServiceInstance> result04 = command4.execute();
        log.info("result03, result04: [{}], [{}]",
                JSON.toJSONString(result03), JSON.toJSONString(result04));

运行结果:
command1 command3 调用服务;
command2 command4 调用缓存;


(4)、注解形式实现

注解描述属性
@CacheResult该注解用来标记请求命令返回的结果应该被缓存,它必须与@HystrixCommand注解结合使用cacheKeyMethod
@CacheRemove该注解用来让请求命令的缓存失效,失效的缓存根据commandKey进行查找。commandKey,cacheKeyMethod
@CacheKey该注解用来在请求命令的参数上标记,使其作为cacheKey,如果没有使用此注解则会使用所有参数列表中的参数作为value

方式一

    @CacheResult(cacheKeyMethod = "getCacheKey")
    @HystrixCommand(commandKey = "CacheHystrixCommandAnnotation")
    public List<ServiceInstance> useCacheByAnnotation01(String serviceId) {
        log.info("use cache01 to get nacos client info: [{}]", serviceId);
        return nacosClientService.getNacosClientInfo(serviceId);
    }

    @CacheRemove(commandKey = "CacheHystrixCommandAnnotation",
            cacheKeyMethod = "getCacheKey")
    @HystrixCommand
    public void flushCacheByAnnotation01(String cacheId) {
        log.info("flush hystrix cache key: [{}]", cacheId);
    }

    public String getCacheKey(String cacheId) {
        return cacheId;
    }

方式二

@CacheResult
    @HystrixCommand(commandKey = "CacheHystrixCommandAnnotation")
    public List<ServiceInstance> useCacheByAnnotation02(@CacheKey String serviceId) {

        log.info("use cache02 to get nacos client info: [{}]", serviceId);
        return nacosClientService.getNacosClientInfo(serviceId);
    }

    @CacheRemove(commandKey = "CacheHystrixCommandAnnotation")
    @HystrixCommand
    public void flushCacheByAnnotation02(@CacheKey String cacheId) {
        log.info("flush hystrix cache key: [{}]", cacheId);
    }

方式三

	@CacheResult
    @HystrixCommand(commandKey = "CacheHystrixCommandAnnotation")
    public List<ServiceInstance> useCacheByAnnotation03(String serviceId) {

        log.info("use cache03 to get nacos client info: [{}]", serviceId);
        return nacosClientService.getNacosClientInfo(serviceId);
    }

    @CacheRemove(commandKey = "CacheHystrixCommandAnnotation")
    @HystrixCommand
    public void flushCacheByAnnotation03(String cacheId) {
        log.info("flush hystrix cache key: [{}]", cacheId);
    }

调用测试

List<ServiceInstance> result01 =
                cacheHystrixCommandAnnotation.useCacheByAnnotation01(serviceId);
List<ServiceInstance> result02 =
                cacheHystrixCommandAnnotation.useCacheByAnnotation01(serviceId);

// 清除掉缓存
cacheHystrixCommandAnnotation.flushCacheByAnnotation01(serviceId);

List<ServiceInstance> result03 =
                cacheHystrixCommandAnnotation.useCacheByAnnotation01(serviceId);
// 这里有第四次调用,重新调用服务
return cacheHystrixCommandAnnotation.useCacheByAnnotation01(serviceId);

4、请求合并


(1)、概要

默认情况下,每一个请求都会占用一个线程和一次网络请求,高并发场景下效率不高
在这里插入图片描述
使用 Hystrix 的请求合并,将多个请求 merge 为一个,提高服务的并发能力
在这里插入图片描述

(2)、编码形式实现

编写需要调用的合并方法

    public List<List<ServiceInstance>> getNacosClientInfos(List<String> serviceIds) {

        log.info("request nacos client to get service instance infos: [{}]",
                JSON.toJSONString(serviceIds));
        List<List<ServiceInstance>> result = new ArrayList<>(serviceIds.size());

        serviceIds.forEach(s -> result.add(discoveryClient.getInstances(s)));
        return result;
    }

创建继承HystrixCommand的类 给上述方法实现包装

@Slf4j
public class NacosClientBatchCommand extends HystrixCommand<List<List<ServiceInstance>>> {

    private final NacosClientService nacosClientService;
    private final List<String> serviceIds;

    protected NacosClientBatchCommand(
            NacosClientService nacosClientService, List<String> serviceIds
    ) {

        super(
                HystrixCommand.Setter.withGroupKey(
                        HystrixCommandGroupKey.Factory.asKey("NacosClientBatchCommand")
                )
        );

        this.nacosClientService = nacosClientService;
        this.serviceIds = serviceIds;
    }

    @Override
    protected List<List<ServiceInstance>> run() throws Exception {

        log.info("use nacos client batch command to get result: [{}]",
                JSON.toJSONString(serviceIds));
        return nacosClientService.getNacosClientInfos(serviceIds);
    }

    @Override
    protected List<List<ServiceInstance>> getFallback() {

        log.warn("nacos client batch command failure, use fallback");
        return Collections.emptyList();
    }
}

创建请求合并器

@Slf4j
//第一个参数是 批量批量方法的请求结果,第二个参数是单个请求的响应结果,第三个是参数
public class NacosClientCollapseCommand
        extends HystrixCollapser<List<List<ServiceInstance>>, List<ServiceInstance>, String> {

    private final NacosClientService nacosClientService;
    private final String serviceId;

    public NacosClientCollapseCommand(NacosClientService nacosClientService, String serviceId) {

        super(
                HystrixCollapser.Setter.withCollapserKey(
                        HystrixCollapserKey.Factory.asKey("NacosClientCollapseCommand")
                ).andCollapserPropertiesDefaults(
                        //300毫秒内的请求进行合并
                        HystrixCollapserProperties.Setter().withTimerDelayInMilliseconds(300)
                )
        );

        this.nacosClientService = nacosClientService;
        this.serviceId = serviceId;
    }

    /**
     * <h2>获取请求中的参数</h2>
     * */
    @Override
    public String getRequestArgument() {
        return this.serviceId;
    }

    /**
     * <h2>创建批量请求 Hystrix Command 也就是调用上述NacosClientBatchCommand的命令</h2>
     * */
    @Override
    protected HystrixCommand<List<List<ServiceInstance>>> createCommand(
            Collection<CollapsedRequest<List<ServiceInstance>, String>> collapsedRequests) {

        List<String> serviceIds = new ArrayList<>(collapsedRequests.size());
        serviceIds.addAll(
                collapsedRequests.stream()
                        .map(CollapsedRequest::getArgument)
                        .collect(Collectors.toList())
        );

        return new NacosClientBatchCommand(nacosClientService, serviceIds);
    }

    /**
     * <h2>响应分发给单独的请求</h2>
     * */
    @Override
    protected void mapResponseToRequests(List<List<ServiceInstance>> batchResponse,
                                         Collection<CollapsedRequest<List<ServiceInstance>,
                                                 String>> collapsedRequests) {

        int count = 0;

        for (CollapsedRequest<List<ServiceInstance>, String> collapsedRequest : collapsedRequests) {

            // 从批量响应集合中按顺序取出结果
            List<ServiceInstance> instances = batchResponse.get(count++);
            // 将结果返回原 Response 中,插入到单个响应结果中
            collapsedRequest.setResponse(instances);
        }
    }
}

调用方式

// 前三个请求会被合并
NacosClientCollapseCommand collapseCommand01 = new NacosClientCollapseCommand(
                nacosClientService, "e-commerce-nacos-client1");
NacosClientCollapseCommand collapseCommand02 = new NacosClientCollapseCommand(
                nacosClientService, "e-commerce-nacos-client2");
NacosClientCollapseCommand collapseCommand03 = new NacosClientCollapseCommand(
                nacosClientService, "e-commerce-nacos-client3");

Future<List<ServiceInstance>> future01 = collapseCommand01.queue();
Future<List<ServiceInstance>> future02 = collapseCommand02.queue();
Future<List<ServiceInstance>> future03 = collapseCommand03.queue();

future01.get();
future02.get();
future03.get();

Thread.sleep(2000);

// 过了合并的时间窗口, 第四个请求单独发起
NacosClientCollapseCommand collapseCommand04 = new NacosClientCollapseCommand(
                nacosClientService, "e-commerce-nacos-client4");
Future<List<ServiceInstance>> future04 = collapseCommand04.queue();
future04.get();

运行结果

在这里插入图片描述

(3)、注解形式实现

	// 使用注解实现 Hystrix 请求合并
    // 抛出异常说明有问题 例如找不到合并方法
    // Hystrix 会通过指定的 batchMethod(在这里是 findNacosClientInfos)来异步处理这些请求,返回Future异步处理
    @HystrixCollapser(
            batchMethod = "findNacosClientInfos",
            scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL, //对所有的请求拦截
            collapserProperties = {
                    @HystrixProperty(name = "timerDelayInMilliseconds", value = "300")
            }
    )
    public Future<List<ServiceInstance>> findNacosClientInfo(String serviceId) {

        // 系统运行正常, 不会走这个方法
        throw new RuntimeException("This method body should not be executed!");
    }

    @HystrixCommand
    public List<List<ServiceInstance>> findNacosClientInfos(List<String> serviceIds) {

        log.info("coming in find nacos client infos: [{}]", JSON.toJSONString(serviceIds));
        return getNacosClientInfos(serviceIds);
    }

调用方式

//直接返回future,上面需要使用queue方法返回future,证明queue方法封装在HystrixCollapser里面
Future<List<ServiceInstance>> future01 = nacosClientService.findNacosClientInfo(
                "e-commerce-nacos-client1"
        );
Future<List<ServiceInstance>> future02 = nacosClientService.findNacosClientInfo(
                "e-commerce-nacos-client2"
        );
Future<List<ServiceInstance>> future03 = nacosClientService.findNacosClientInfo(
                "e-commerce-nacos-client3"
        );

future01.get();
future02.get();
future03.get();

Thread.sleep(2000);

Future<List<ServiceInstance>> future04 = nacosClientService.findNacosClientInfo(
                "e-commerce-nacos-client4"
        );
future04.get();

运行效果与编码形式一致


5、openfeign集成hystrix


(1)、在配置文件中开启熔断功能

# Feign 的相关配置
feign:
  # OpenFeign 集成 Hystrix
  hystrix:
    enabled: true

(2)、使用FeignClient 注解的 fallback属性

创建实现类,相当于commad(上述编码形式、注释形式)中的默认实现

@Slf4j
@Component
public class AuthorityFeignClientFallback implements AuthorityFeignClient {

    @Override
    public JwtToken getTokenByFeign(UsernameAndPassword usernameAndPassword) {

        log.info("authority feign client get token by feign request error " +
                "(Hystrix Fallback): [{}]", JSON.toJSONString(usernameAndPassword));
        return new JwtToken("whqn");
    }
}

feign方法中备注

@FeignClient(
        contextId = "AuthorityFeignClient", value = "e-commerce-authority-center",
        fallback = AuthorityFeignClientFallback.class
)
public interface AuthorityFeignClient {

    @RequestMapping(value = "/ecommerce-authority-center/authority/token",
            method = RequestMethod.POST,
            consumes = "application/json", produces = "application/json")
    JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword);
}

运行结果与普通实现容错降级一致,但是无法报错异常错误,为此,需要使用fallbackFactory 属性


(3)、使用FeignClient 注解的 fallbackFactory属性

创建实现类,其中的泛型是需要包装的feign client

@Slf4j
@Component
public class AuthorityFeignClientFallbackFactory
        implements FallbackFactory<AuthorityFeignClient> {

    @Override
    public AuthorityFeignClient create(Throwable throwable) {

        log.warn("authority feign client get token by feign request error " +
                "(Hystrix FallbackFactory): [{}]", throwable.getMessage(), throwable);
		//需要返回一个类,但是feign是个接口,所以需要返回一个匿名内部类,实现默认方法
        return new AuthorityFeignClient() {

            @Override
            public JwtToken getTokenByFeign(UsernameAndPassword usernameAndPassword) {
                return new JwtToken("whqn-factory");
            }
        };
    }
}

feign方法中备注

@FeignClient(
        contextId = "AuthorityFeignClient", value = "e-commerce-authority-center",
        fallbackFactory = AuthorityFeignClientFallbackFactory.class
)
public interface AuthorityFeignClient {

    @RequestMapping(value = "/ecommerce-authority-center/authority/token",
            method = RequestMethod.POST,
            consumes = "application/json", produces = "application/json")
    JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword);
}

运行结果:有报错显示 如下

在这里插入图片描述


5、hystrix监控面板


(1)、搭建监控面板

为什么可以监控hystrix的信息
由于每个工程下都配有端点actuator,里面包含了hystrix的端点,实际上就是对这个端点的监控
是一个单独的应用,实时的对应用进行监测

创建工程,pom文件

server:
  port: 9999
  servlet:
    context-path: /ecommerce-hystrix-dashboard

spring:
  application:
    name: e-commerce-hystrix-dashboard
  cloud:
    nacos:
      # 服务注册发现
      discovery:
        enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        server-addr: 127.0.0.1:8848
        #        server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址
        namespace: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator

hystrix:
  dashboard:
    proxy-stream-allow-list: "127.0.0.1" #对这个地址进行监控

# 暴露端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

创建启动类

/**
 * <h1>hystrix dashboard 入口</h1>
 * 127.0.0.1:9999/ecommerce-hystrix-dashboard/hystrix/
 * */
@EnableDiscoveryClient
@SpringBootApplication
@EnableHystrixDashboard     // 开启 Hystrix Dashboard
public class HystrixDashboardApplication {

    public static void main(String[] args) {

        SpringApplication.run(HystrixDashboardApplication.class, args);
    }
}

运行结果
在这里插入图片描述

(2)、使用监控面板

需要监控的地址这里为 *****/actuator/hystrix.stream
使用http://127.0.0.1:8000/ecommerce-nacos-client/actuator/hystrix.stream测试
short-circuited 熔断的数量

在这里插入图片描述

线程池的情况

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值