SpringCloud

SpringCloud学习

微服务架构的系统是个分布式系统,按业务领域划分,为独立的服务单元,有自动化运维、容错、快速演进的特点,它能够解决传统单体架构系统的痛点,同时也能满足越来越复杂的业务需求。

Eureka组件

spring cloud eurekazookeeper 的区别

问:为什么zookeeper 不适合做注册中心?

CAP 原则又称CAP 定理,指的是在一个分布式系统中,

  • 一致性(Consistency
  • 可用性(Availability
  • 分区容错性(Partition tolerance)(这个特性是不可避免的)

CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

分布式的特征

C : 数据的一致性(A,B,C 里面的数据是一致的)

  1. zookeeper 注重数据的一致性。
  2. Eureka 不是很注重数据的一致性!

A: 服务的可用性(若zookeeper 集群里面的master 挂了怎么办)Paxos(多数派)

  1. zookeeper 里面,若主机挂了,则zookeeper 集群整体不对外提供服务了,需要选一个新的出来(120s
    左右)才能继续对外提供服务!
  2. Eureka 注重服务的可用性,当Eureka 集群只有一台活着,它就能对外提供服务

P:分区的容错性(在集群里面的机器,因为网络原因,机房的原因,可能导致数据不会里面
同步),它在分布式必须需要实现的特性!

  1. Zookeeper 注重数据的一致性,CP zookeeper(注册中心,配置文件中心,协调中心)
  2. Eureka 注重服务的可用性AP eureka (注册中心)

搭建Eureka服务端

pom文件:这里只记录了重要的内容,其余内容省略了

 <!--本质还是springboot项目-->
<parent>
    <!-- 实质还是springboot 项目-->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
 <!--各个依赖的版本-->
<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
 <!---->
<dependencies>
     <!--rureka依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

启动类

@EnableEurekaServer // 开启eureka注册中心服务端
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

配置文件

server:
  port: 8761
spring:
  application:
    name: eureka-server

这里的端口号只能是8761,其他端口号会报错

原因:Eureka-Server 不仅提供让别人注册的功能,它也能注册到别人里面,自己注册自己所以,在启动项目时,默认会注册自己,我们也可以关掉这个功能。

具体原因可以查看一下源码,源码的位置为EurekaClientConfigBean,在下面的代码片段中,我们可以看到,serviceUrl默认的为本地的8761端口

public EurekaClientConfigBean() {
    this.serviceUrl.put("defaultZone", "http://localhost:8761/eureka/");
    this.gZipContent = true;
    this.useDnsForFetchingServiceUrls = false;
    this.registerWithEureka = true;
    this.preferSameZoneEureka = true;
    this.availabilityZones = new HashMap();
    this.filterOnlyUpInstances = true;
    this.fetchRegistry = true;
    this.dollarReplacement = "_-";
    this.escapeCharReplacement = "__";
    this.allowRedirects = false;
    this.onDemandUpdateStatusChange = true;
    this.clientDataAccept = EurekaAccept.full.name();
    this.shouldUnregisterOnShutdown = true;
    this.shouldEnforceRegistrationAtInit = false;
    this.order = 0;
}

访问地址:http://localhost:8761/

在这里插入图片描述

搭建Eureka客户端

pom文件:这里只记录了重要的内容,其余内容省略了

记录一个问题,如果你忘记加spring-boot-starter-web依赖,此时项目并不会报错,在启动完成之后,就退出了

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

启动类

@EnableEurekaClient // 开启eureka客户端
@SpringBootApplication
public class EurekaClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaClientApplication.class, args);
    }
}

配置文件

server:
  port: 8001
spring:
  application:
    name: eureka-client-a
eureka:
  client:
    service-url: 
      defaultZone: http://localhost:8761/eureka/ # eureka服务端和客户端的交互地址

客户端注册到服务端的效果

在这里插入图片描述

注册中心的状态

  1. UP: 服务是上线的,括号里面是具体服务实例的个数,提供服务的最小单元
  2. DOWN: 服务是下线的
  3. UN_KONW: 服务的状态未知

服务端常用的配置

server:
  port: 8761
spring:
  application:
    name: eureka-server
eureka:
  client:
    service-url: 
      defaultZone: http://localhost:8761/eureka # 注册中心地址,如果集群的话用,隔开
    fetch-registry: true # 是否拉取服务列表
    register-with-eureka: true # 是否注册自己(单机eureka 一般关闭注册自己,集群注意打开)
  server:
    eviction-interval-timer-in-ms: 30000 # 清除无效节点的频率(毫秒) 定期删除
    enable-self-preservation: true # server的自我保护机制,避免因为网络原因造成误删除,生产环境建议打开
    renewal-percent-threshold: 0.85 # 85% 如果在一个机房的client端,15分钟内有85%的client没有续约,那么则可能是网络原因,认为服务实例没有问题,不会提出他们,宁可放过一万,不可错杀一个,确保高可用
  instance:
    hostname: localhost # 服务主机名称
    instance-id: ${eureka.instance.hostname}.${eureka.application.name}.${server.port} # 实例的id
    prefer-ip-address: true # 服务列表以ip的形式展示
    lease-renewal-interval-in-seconds: 10 # 表示eureka client发送心跳给server的频率
    lease-expiration-duration-in-seconds: 20 # 表示eyreka server 至 上一次收到client的心跳之后,等待下一此心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该实例

客户端常用的配置

server:
  port: 8080
spring:
  application:
    name: eureka-client
eureka:
  client:
    service-url: #eureka 服务端和客户端的交互地址,集群用,隔开
      defaultZone: http://localhost:8761/eureka
    register-with-eureka: true #注册自己
    fetch-registry: true #拉取服务列表
    registry-fetch-interval-seconds: 5 # 表示eureka-client 间隔多久去拉取服务注册信息
  instance:
    hostname: localhost # 服务主机名称
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} # 实例id
    prefer-ip-address: true # 服务列表以ip 的形式展示
    lease-renewal-interval-in-seconds: 10 # 表示eureka client 发送心跳给server 端的频率
    lease-expiration-duration-in-seconds: 20 #表示eureka server 至上一次收到client 的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该实例

构建高可用的eureka集群

server-1

server:
  port: 8761 #为什么是8761,其他端口就报错
spring:
  application:
    name: eureka-server #服务名称
eureka:
  client:
    fetch-registry: true #是否拉取服务列表
    register-with-eureka: true #是否注册自己(集群需要注册自己和拉取服务)
    service-url:
	  defaultZone: http://localhost:8762/eureka/,http://localhost:8763/eureka/
  server:
    eviction-interval-timer-in-ms: 90000 #清除无效节点的评率(毫秒)
  instance:
    lease-expiration-duration-in-seconds: 90 #server 在等待下一个客户端发送的心跳时间,若在指定时间不能收到客户端心跳,则剔除此实例并且禁止流量

server-2

server:
  port: 8762
spring:
  application:
    name: eureka-server #服务名称
eureka:
  client:
    fetch-registry: true #是否拉取服务列表
    register-with-eureka: true #是否注册自己(集群需要注册自己和拉取服务)
    service-url:
	  defaultZone: http://localhost:8761/eureka/,http://localhost:8763/eureka/
  server:
    eviction-interval-timer-in-ms: 90000 #清除无效节点的评率(毫秒)
  instance:
    lease-expiration-duration-in-seconds: 90 #server 在等待下一个客户端发送的心跳时间,若在指定时间不能收到客户端心跳,则剔除此实例并且禁止流量

server-3

server:
  port: 8763
spring:
  application:
    name: eureka-server #服务名称
eureka:
  client:
    fetch-registry: true #是否拉取服务列表
    register-with-eureka: true #是否注册自己(集群需要注册自己和拉取服务)
    service-url:
	  defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
  server:
    eviction-interval-timer-in-ms: 90000 #清除无效节点的评率(毫秒)
  instance:
    lease-expiration-duration-in-seconds: 90 #server 在等待下一个客户端发送的心跳时间,若在指定时间不能收到客户端心跳,则剔除此实例并且禁止流量

Eureka相关概念

服务的注册

当项目启动时(eureka 的客户端),就会向eureka-server 发送自己的元数据(原始数据)(运行的ip,端口port,健康的状态监控等,因为使用的是http/ResuFul 请求风格),eureka-server 会在自己内部保留这些元数据(内存中)。(有一个服务列表)(restful 风格,以http 动词的请求方式,完成对url 资源的操作)

服务的续约

项目启动成功了,除了向eureka-server 注册自己成功,还会定时的向eureka-server 汇报自己,心跳,表示自己还活着。(修改一个时间)

服务下线(主动下线)

当项目关闭时,会给eureka-server 报告,说明自己要下机了。

服务的剔除(被动下线,主动剔除)

当项目超过了指定时间没有向eureka-server 汇报自己,那么eureka-server 就会认为此节点死掉了,会把它剔除掉,也不会放流量和请求到此节点了。

Ribbon组件

Spring Cloud Ribbon 是一个基于HTTPTCP 的客户端负载均衡工具,它基于NetflixRibbon 实现。通过Spring Cloud 的封装,可以让我们轻松地将面向服务的REST 模版请求自动转换成客户端负载均衡的服务调用。轮询hash 权重…简单的说Ribbon 就是netfix 公司的一个开源项目,主要功能是提供客户端负载均衡算法和服务调用。Ribbon 客户端组件提供了一套完善的配置项,比如连接超时,重试等。

Spring Cloud 构建的微服务系统中, Ribbon 作为服务消费者的负载均衡器,有两种使用方式,一种是和RestTemplate 相结合,另一种是和OpenFeign 相结合。OpenFeign 已经默认集成了Ribbon,关于OpenFeign 的内容将会在下一章进行详细讲解。Ribbon 有很多子模块,但很多模块没有用于生产环境!

入门案例

编写两个服务提供者

pom文件

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

测试接口A

@RestController
public class ProviderController {
    @GetMapping("hello")
    public String hello(){
        return "我是提供者aaaa的接口";
    }
}

测试接口B

@RestController
public class ProviderController {
    @GetMapping("hello")
    public String hello(){
        return "我是提供者bbbb的接口";
    }
}

配置文件A

server:
    port: 8080
spring:
    application:
        name: provider # 名字不用改
eureka:
    client:
        service-url:
            defaultZone: http://192.168.76.128:8761/eureka
    instance:
        hostname: localhost
        prefer-ip-address: true
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

配置文件B

server:
    port: 8081
spring:
    application:
        name: provider # 名字不用改
eureka:
    client:
        service-url:
            defaultZone: http://192.168.76.128:8761/eureka
    instance:
        hostname: localhost
        prefer-ip-address: true
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

启动类A

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

启动类B

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

消费者

使用RestTemplate进行Eureka Client(包括服务提供者以及服务消费者,在这里其实是服务消费者使用RestTemplate)之间的通信,为RestTemplate配置类添加@LoadBalanced注解即可,如下所示:

@Bean
@LoadBalanced // ribbon的负载均衡注解
public RestTemplate restTemplate() {
  return new RestTemplate();
}

启动类

@SpringBootApplication
@EnableEurekaClient
// 在启动该微服务的时候就能去加载我们的自定义Ribbon配置类,从而使配置生效
@RibbonClient(name="MICROSERVICECLOUD-DEPT")
public class DeptConsumer80_App{
	public static void main(String[] args){
		SpringApplication.run(DeptConsumer80_App.class, args);
	}
    @Bean
    @LoadBalanced // ribbon的负载均衡注解
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

消费者的方法

@GetMapping("/testRibbon")
public String testRibbon(String serviceName){
    // 正常来讲需要拿到ip和port 以及路径才可以用
    // http://provider/hello
    String result = restTemplate.getForObject("http://" + serviceName + "/hello", String.class);
    // 只要你给restTemplate 加了ribbon的注解 项目中这个对象发起的请求 都会走ribbon的代理
    // 如果你想使用原生的restTemplate 就需要重新创建一个对象
    // RestTemplate myRest = new RestTemplate();
    // String forObject = myRest.getForObject("http://localhost:8888/aaa", String.class);
    return result;
}

配置文件

server:
    port: 8082
spring:
    application:
        name: consumer
eureka:
    client:
        service-url:
            defaultZone: http://47.100.238.122:8761/eureka
    instance:
        hostname: localhost
        prefer-ip-address: true
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

测试

http://localhost:8082/testRibbon?serviceName=provider

Ribbon实现思路

  1. 拦截这个请求
  2. 截取主机名称
  3. 借助eureka来做服务发现 list<>
  4. 通过负载均衡算法 拿到一个服务ip port
  5. reConstructURL 重构url
  6. 发起请求

注意:启动的时候,先启动服务端,在启动提供者,最后启动消费者。

Ribbon 要做什么事情?

先通过"http://" + serviceId + "/info" 我们思考ribbon 在真正调用之前需要做什么?

restTemplate.getForObject(“http://provider/info”, String.class);

想要把上面这个请求执行成功,我们需要以下几步

  1. 拦截该请求;
  2. 获取该请求的URL 地址:http://provider/info
  3. 截取URL 地址中的provider
  4. 从服务列表中找到keyprovider 的服务实例的集合(服务发现)
  5. 根据负载均衡算法选出一个符合的实例
  6. 拿到该实例的hostport,重构原来URL 中的provider
  7. 真正的发送restTemplate.getForObject(“http://ip:port/info”,String.class)

轮询算法

取余运算,但设计到线程安全问题。

怎么能做一个线程安全的轮训算法 加锁 效率极低 CAS 自旋锁 没有线程的等待和唤醒的开销

CAS 优点 性能好 java层面无锁的状态 但是在jvm层面 有锁的cmpxchg

CAS 缺点 会导致短暂时间内 CPU 飙升 还有ABA 问题

源码分析

这里学习到了一个查看源码的小技巧,首先debug项目。【结合ribbon轮询算法进行解释】

ServiceInstance choose = loadBalancerClient.choose(serviceName);

进入choose方法,点击choose方法的实现,这里我们肯定看的是RibbonLoadBalancerClient,在方法return语句打上断点。这里为了方便观看,我把格式调整了一下

public ServiceInstance choose(String serviceId) {
    return this.choose(serviceId, (Object)null);
}
public ServiceInstance choose(String serviceId, Object hint) {
    Server server = this.getServer(this.getLoadBalancer(serviceId), hint);
    return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(
        serviceId, 
        server, 
        this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server)
    );
}

此时,debug运行项目,访问该请求,点击this.getServer方法

protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
    return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}

可以看到chooseServer方法,点进去之后,发现是一个接口,但是我们此时是不知道它的实现类是什么的。在上面的return语句打上断点,f9跳到这个断点,按住Alt点击chooseServer方法,会弹出一个框,显示是谁调用了它,显示的是ZoneAwareLoadBalancer

public Server chooseServer(Object key) {
    if (ENABLED.get() && this.getLoadBalancerStats().getAvailableZones().size() > 1) {
        Server server = null;

        try {
            LoadBalancerStats lbStats = this.getLoadBalancerStats();
            Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
            logger.debug("Zone snapshots: {}", zoneSnapshot);
            if (this.triggeringLoad == null) {
                this.triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2D);
            }

            if (this.triggeringBlackoutPercentage == null) {
                this.triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999D);
            }

            Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, this.triggeringLoad.get(), this.triggeringBlackoutPercentage.get());
            logger.debug("Available zones: {}", availableZones);
            if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                logger.debug("Zone chosen: {}", zone);
                if (zone != null) {
                    BaseLoadBalancer zoneLoadBalancer = this.getLoadBalancer(zone);
                    server = zoneLoadBalancer.chooseServer(key); // 看这里
                }
            }
        } catch (Exception var8) {
            logger.error("Error choosing server using zone aware logic for load balancer={}", this.name, var8);
        }

        if (server != null) {
            return server;
        } else {
            logger.debug("Zone avoidance logic is not invoked.");
            return super.chooseServer(key);
        }
    } else {
        logger.debug("Zone aware logic disabled or there is only one zone");
        return super.chooseServer(key);
    }
}

上面的代码片段中,我们可以发现最终是进入了 server = zoneLoadBalancer.chooseServer(key); 方法中,继续深入,

public Server chooseServer(Object key) {
    if (this.counter == null) {
        this.counter = this.createCounter();
    }

    this.counter.increment();
    if (this.rule == null) {
        return null;
    } else {
        try {
            return this.rule.choose(key);
        } catch (Exception var3) {
            logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", new Object[]{this.name, key, var3});
            return null;
        }
    }
}

在这个方法中继续打断点,调用this.rule.choose(key);PredicateBasedRule

public Server choose(Object key) {
    ILoadBalancer lb = this.getLoadBalancer();
    Optional<Server> server = this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
    return server.isPresent() ? (Server)server.get() : null;
}

可以看到chooseRoundRobinAfterFiltering这个方法,点击进去之后发现有一个方法incrementAndGetModulo从字面可以看出是有取余操作的,点进去看看

public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
    List<Server> eligible = this.getEligibleServers(servers, loadBalancerKey);
    return eligible.size() == 0 ? Optional.absent() : Optional.of(
        eligible.get(this.incrementAndGetModulo(eligible.size()))
    );
}

最终的实现

private int incrementAndGetModulo(int modulo) {
    int current;
    int next;
    do {
        current = this.nextIndex.get();
        next = (current + 1) % modulo;
    } while(!this.nextIndex.compareAndSet(current, next) || current >= modulo);

    return current;
}

ribbon中如何设置负载均衡算法规则

修改默认负载均衡算法,几种算法的全限定类名

NFLoadBalancerClassName: loadBalance 策略

NFLoadBalancerPingClassName: ping 机制策略

NIWSServerListClassName: 服务列表策略

NIWSServerListFilterClassName: 服务列表过滤策略

ZonePreferenceServerListFilter : 默认是优先过滤非一个区的服务列表

# 访问不用的服务可以使用不用的算法规则
provider:  # 先写服务提供者的应用名称
    ribbon:
        NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule    #几种算法的全限定类名
ribbon:
    eager-load:
        enabled: false # ribbon它只有自己的话 能不能做服务发现 借助eureka  # ribbon需要去eureka中获取服务列表 如果false就懒加载
    eureka:
        enabled: true
    http:  # 我们使用ribbon 用的restTemplate发请求 java.net.HttpUrlConnection 发的请求  很方便 但是它不支持连接池
        client:  # 发请求的工具有很多 httpClient  它支持连接池 效率更好  如果你想改请求的工具 记得加这个依赖即可
            enabled: false
    okhttp: # 这个也是请求工具 移动端用的比较多 轻量级的请求
        enabled: false

自定义算法

@Component
public class MyRule implements IRule {
    @Override
    public Server choose(Object key) {
        return null;
    }

    @Override
    public void setLoadBalancer(ILoadBalancer lb) {
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return null;
    }
}

启动类

@SpringBootApplication
@EnableEurekaClient
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
    /**
     * 往容器中放一个rule对象
     * 你访问任何一个提供者 都是这个算法
     * @return
     */
    @Bean
    public IRule myRule(){
        return new RandomRule();
    }
}

Ribbon默认配置DefaultClientConfigImpl,一下是部分内容,可以根据类名搜索进行查看。

public static final Boolean DEFAULT_PRIORITIZE_VIP_ADDRESS_BASED_SERVERS;
public static final String DEFAULT_NFLOADBALANCER_PING_CLASSNAME = "com.netflix.loadbalancer.DummyPing";
public static final String DEFAULT_NFLOADBALANCER_RULE_CLASSNAME = "com.netflix.loadbalancer.AvailabilityFilteringRule";
public static final String DEFAULT_NFLOADBALANCER_CLASSNAME = "com.netflix.loadbalancer.ZoneAwareLoadBalancer";
public static final boolean DEFAULT_USEIPADDRESS_FOR_SERVER;
public static final String DEFAULT_CLIENT_CLASSNAME = "com.netflix.niws.client.http.RestClient";
public static final String DEFAULT_VIPADDRESS_RESOLVER_CLASSNAME = "com.netflix.client.SimpleVipAddressResolver";
public static final String DEFAULT_PRIME_CONNECTIONS_URI = "/";
public static final int DEFAULT_MAX_TOTAL_TIME_TO_PRIME_CONNECTIONS = 30000;
public static final int DEFAULT_MAX_RETRIES_PER_SERVER_PRIME_CONNECTION = 9;
public static final Boolean DEFAULT_ENABLE_PRIME_CONNECTIONS;
public static final int DEFAULT_MAX_REQUESTS_ALLOWED_PER_WINDOW = 2147483647;
public static final int DEFAULT_REQUEST_THROTTLING_WINDOW_IN_MILLIS = 60000;
public static final Boolean DEFAULT_ENABLE_REQUEST_THROTTLING;
public static final Boolean DEFAULT_ENABLE_GZIP_CONTENT_ENCODING_FILTER;
public static final Boolean DEFAULT_CONNECTION_POOL_CLEANER_TASK_ENABLED;
public static final Boolean DEFAULT_FOLLOW_REDIRECTS;
public static final float DEFAULT_PERCENTAGE_NIWS_EVENT_LOGGED = 0.0F;
public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1;
public static final int DEFAULT_MAX_AUTO_RETRIES = 0;
public static final int DEFAULT_BACKOFF_INTERVAL = 0;
public static final int DEFAULT_READ_TIMEOUT = 5000;
public static final int DEFAULT_CONNECTION_MANAGER_TIMEOUT = 2000;
public static final int DEFAULT_CONNECT_TIMEOUT = 2000;
public static final Boolean DEFAULT_ENABLE_CONNECTION_POOL;

OpenFeign组件

Feign 是声明性(注解)Web 服务客户端。它使编写Web 服务客户端更加容易。要使用Feign,请创建一个接口并对其进行注解。它具有可插入注解支持,包括Feign 注解和JAX-RS 注解。Feign 还支持可插拔编码器和解码器。Spring Cloud 添加了对Spring MVC 注解的支持,并支持使用HttpMessageConvertersSpring Web 中默认使用的注解。Spring Cloud 集成了RibbonEureka 以及Spring Cloud LoadBalancer,以在使用Feign 时提供负载平衡的http 客户端。

Feign 是一个远程调用的组件(接口,注解) http 调用的

Feign 集成了ribbon ribbon 里面集成了eureka

入门案例

创建订单服务

pom文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

启动类

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

controller

@RestController
public class OrderController {
    @GetMapping("doOrder")
    public String doOrder(){
        //try {
        //    // 模拟操作数据库等 耗时2s
        //    TimeUnit.SECONDS.sleep(2);
        //} catch (InterruptedException e) {
        //    e.printStackTrace();
        //}
        return "油条豆浆-热干面";
    }
}

配置文件

server:
    port: 8080
spring:
    application:
        name: order-service
eureka:
    client:
        service-url:
            defaultZone: http://192.168.153.1:8761/eureka

创建用户服务,模拟下单的简单过程。

pom文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

启动类

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.powernode.feign"}) // 开启feign的客户端功能 才可以帮助我们发起调用
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

controller

@RestController
public class UserController {

    @Autowired
    public UserOrderFeign userOrderFeign;

    @GetMapping("userDoOrder")
    public String userDoOrder() {
        System.out.println("有用户进来了");
        // 这里需要发起远程调用
        String s = userOrderFeign.doOrder();
        return s;
    }

配置文件

server:
    port: 8081
spring:
    application:
        name: user-service
eureka:
    client:
        service-url:
            defaultZone: http://192.168.153.1:8761/eureka

创建feign文件夹,并创建想要远程调用的接口

@FeignClient(value = "order-service")
public interface UserOrderFeign {
    /**
     * 你需要调用哪个controller  就写它的方法签名
     * 方法签名(就是包含一个方法的所有的属性)
     * @return
     */
    @GetMapping("doOrder")
    String doOrder();
}

问题

user服务中调用order服务,而order服务由于很复杂,耗时很长,此时上面的代码会出现超时的问题

改造order服务中的controller,验证是否是超时的

@GetMapping("doOrder")
public String doOrder(){
    try {
        // 模拟操作数据库等 耗时5s
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "油条豆浆-热干面";
}

错误信息

java.net.SocketTimeoutException: Read timed out # 请求超时

解决办法:在user服务中的配置文件中添加如下配置,经过6秒的等待还是可以成功的。

同时还有服务器在连接的时候,也需要指定超时时间

# feign只是帮你封装了远程调用的功能  底层还是ribbon 所以我们需要去修改ribbon的时间
ribbon:
    ReadTimeout: 6000 # 给3s超时时间
    ConnectTimeout: 5000 # 连接服务的超时时间

feign的调用过程

  • 接口是不能做事情的
  • 如果想做事 必须要有对象
  • 那么这个接口肯定是被创建出代理对象的
  • 动态代理 jdk``(java interface 接口 $Proxy ) cglib(subClass 子类)
  • jdk动态代理 只要是代理对象调用的方法必须走 java.lang.reflect.InvocationHandler#invoke(java.lang.Object,java.lang.reflect.Method, java.lang.Object[])
@Autowired
private RestTemplate restTemplate;

/**
     * 手写feign的核心步骤
     */
@Test
void contextLoads() {
    UserOrderFeign o = (UserOrderFeign) Proxy.newProxyInstance(UserController.class.getClassLoader(), new Class[]{UserOrderFeign.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 能去拿到对方的ip和port 并且拿到这个方法上面的注解里面的值 那么就完事了
            GetMapping annotation = method.getAnnotation(GetMapping.class);
            String[] paths = annotation.value();
            String path = paths[0];
            Class<?> aClass = method.getDeclaringClass();
            FeignClient annotation1 = aClass.getAnnotation(FeignClient.class);
            String applicationName = annotation1.value();
            String url = "http://" + applicationName + "/" + path;
            String forObject = restTemplate.getForObject(url, String.class);
            return forObject;
        }
    });
    String s = o.doOrder();
    System.out.println(s);
}

OpenFeign参数

模拟集中不同的请求方式

编写请求接口

// RESTFul风格的,在路径中填写参数
@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age) {
    System.out.println(name + ":" + age);
    return "ok";
}
// 普通风格
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = false) String name) {
    System.out.println(name);
    return "ok";
}
// 普通风格多个参数
@GetMapping("twoParam")
public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age) {
    System.out.println(name);
    System.out.println(age);
    return "ok";
}
// post请求带请求体参数
@PostMapping("oneObj")
public String oneObj(@RequestBody Order order) {
    System.out.println(order);
    return "ok";
}
// 一个请求体,一个路径参数
@PostMapping("oneObjOneParam")
public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name) {
    System.out.println(name);
    System.out.println(order);
    return "ok";
}

OpenFeign中的写法,只要将你写的请求的请求签名等信息copy过来即可,没有方法体

@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age);
 
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = false) String name);

@GetMapping("twoParam")
public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age);

@PostMapping("oneObj")
public String oneObj(@RequestBody Order order);

@PostMapping("oneObjOneParam")
public String oneObjOneParam(@RequestBody Order order, @RequestParam("name") String name);

测试接口

@GetMapping("testParam")
public String testParam(){
    String cxs = userOrderFeign.testUrl("ljq", 18);
    System.out.println(cxs);

    String t = userOrderFeign.oneParam("111");
    System.out.println(t);

    String lg = userOrderFeign.twoParam("12", 31);
    System.out.println(lg);

    Order order = Order.builder()
        .name("可乐")
        .price(188D)
        .time(new Date())
        .id(1)
        .build();

    String s = userOrderFeign.oneObj(order);
    System.out.println(s);

    String param = userOrderFeign.oneObjOneParam(order, "ljq");
    System.out.println(param);
    return "ok";
}

单独传递Data问题

  • 不建议单独传递时间参数
  • 转成字符串 2022-03-20 10:25:55:213 因为字符串不会改变
  • jdk LocalDate 年月日 LocalDateTime 会丢失秒
  • feign的源码

建议,使用String类型进行传递

OpenFeifn的日志处理

在启动类中

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.powernode.feign"}) // 开启feign的客户端功能 才可以帮助我们发起调用
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    /**
     * 打印fein日志信息 级别
     * @return
     */
    @Bean
    public Logger.Level level(){
        return Logger.Level.FULL;
    }
}

在配置文件中添加

logging:
    level:
        com.powernode.feign.UserOrderFeign: debug  # 我需要打印这个接口下面的日志

Hystrix组件

熔断器,也叫断路器!(正常情况下断路器是关的只有出了问题才打开)用来保护微服务不雪崩的方法。

当有服务调用的时候,才会出现服务雪崩,所以Hystrix 常和OpenFeignRibbon 一起出现

入门案例

创建customer服务

pom文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

配置文件:这里需要注意下,如果没有写最后的feign.hystrix.enabled配置, hystrix默认是关闭状态的,我们的测试是不会有效果的

server:
    port: 8081
spring:
    application:
        name: customer-service
eureka:
    client:
        service-url:
            defaultZone: http://192.168.153.1:8761/eureka
    instance:
        hostname: localhost
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
feign:
    hystrix:
        enabled: true  # 在cloud的F版以前 是默认开启的 但是因为后来有了其他的熔断组件

controller代码

@RestController
public class CustomerController {
    @Autowired
    private CustomerRentFeign customerRentFeign;
    
    @GetMapping("customerRent")
    public String CustomerRent(){
        System.out.println("客户来租车了");
        // RPC
        String rent = customerRentFeign.rent();
        return rent;
    }
}

feign包下的远程调用

注意:这里在FeignClient注解中会有稍微的不同,因为我们这里需要定义在远程调用不生效的情况下, 进行熔断的场景,注解中的fallback = CustomerRentFeignHystrix.class就意思是在远程调用失败的情况下,程序会去调用哪个类下的熔断方法

/**
 * 这里需要指定熔断的类
 */
@FeignClient(value = "rent-car-service",fallback = CustomerRentFeignHystrix.class)
public interface CustomerRentFeign {
    @GetMapping("rent")
    public String rent();
}

hystrix包下的CustomerRentFeign接口的实现类

注意:这里的实现类需要实现feign的接口,并将类纳入spring进行管理

@Component
public class CustomerRentFeignHystrix implements CustomerRentFeign {
    /**
     * 这个方法就是备选方案
     * @return
     */
    @Override
    public String rent() {
        return "我是备胎";
    }
}

启动类

@SpringBootApplication
@EnableEurekaClient // 开启eureka客户端
@EnableFeignClients // 开启openfeign客户端功能
public class CustomerServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(CustomerServiceApplication.class, args);
    }
}

创建rent-car-service服务

pom文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

配置文件

server:
    port: 8080
spring:
    application:
        name: rent-car-service
eureka:
    client:
        service-url:
            defaultZone: http://192.168.153.1:8761/eureka
    instance:
        hostname: localhost
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

controller文件:简单模拟一下

@RestController
public class RentCarController {
    @GetMapping("rent")
    public String rent() {
        return "租车成功";
    }
}

启动类

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

然后就可以测试了。

常用配置文件

server:
  port:8081
spring:
  application:
    name:consumer-user-service
eureka:
    client:
        service-url:
            defaultZone: http://localhost:8761/eureka/
        fetch-registry: true
        register-with-eureka: true
    instance:
        instance-id: ${spring.application.name}:${server.port}
        prefer-ip-address: true
feign:
    hystrix:
        enabled: true
hystrix:    #hystrix 的全局控制
    command:
        default: #default 是全局控制,也可以换成单个方法控制,把 default 换成方法名即可
            circuitBreaker:
                enabled: true   #开启断路器
                requestVolumeThreshold: 3   #失败次数(阀值) 30次 30s
                sleepWindowInMilliseconds: 20000    #窗口时间
                errorThresholdPercentage: 60    #失败率
            fallback:
                isolation:
                    semaphore:
                        maxConcurrentRequests: 1000 #信号量隔离级别最大并发数
            execution:
                isolation:
                    Strategy: thread  #隔离方式  thread 线程隔离集合和 SEMAPHORE 信号量隔离
                        thread:
                            timeoutInMilliseconds: 3000 #调用超时时长
ribbon:
    ReadTimeout: 5000 #要结合feign 的底层ribbon 调用的时长
    ConnectTimeout: 5000
#隔离方式两种隔离方式 thread 线程池按照 group(10 个线程)划分服务提供者,用户请求的线程和做远程的线程不一样
# 好处当 B 服务调用失败了或者请求B 服务的量太大了不会对C 服务造成影响用户访问比较大的情况下使用比较好异步的方式
# 缺点线程间切换开销大,对机器性能影响
# 应用场景调用第三方服务并发量大的情况下
# SEMAPHORE 信号量隔离每次请进来有一个原子计数器做请求次数的++ 当请求完成以后--
# 好处对 cpu 开销小
# 缺点并发请求不易太多当请求过多就会拒绝请求做一个保护机制
# 场景使用内部调用,并发小的情况下
# 源码入门 HystrixCommand AbstractCommand HystrixThreadPool

熔断器模拟

新建请求

@RestController
public class FishController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("doRpc")
    @MyFish
    public String doRpc(){
        String result = restTemplate.getForObject("http://localhost:8989/abc", String.class);
        return result;
    }

}

自定义注解

/**
 * 熔断器切面注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyFish {
}

面向切面,使用AOP来模拟拦截器

@Component
@Aspect
public class FishAspect {

//    public static final String POINT_CUT = "execution (* com.powernode.controller.FishController.doRpc(..))";

    // 因为一个消费者可以去调用多个提供者  每个提供者都有自己的断路器
    // 在消费者里面去创建一个断路器的容器
    public static Map<String, Fish> fishMap = new HashMap<>();

    static {
        // 假设 是需要去调用order-service的服务
        fishMap.put("order-service", new Fish());
    }


    Random random = new Random();

    /**
     * 这个就类比拦截器
     * 就是要判断 当前断路器的状态 从而决定是否发起调用(执行目标方法)
     *
     * @param joinPoint
     * @return
     */
    @Around(value = "@annotation(com.powernode.anno.MyFish)")
    public Object fishAround(ProceedingJoinPoint joinPoint) {
        Object result = null;
        // 获取到当前提供者的断路器
        Fish fish = fishMap.get("order-service");
        FishStatus status = fish.getStatus();
        switch (status) {
            case CLOSE:
                // 正常  去调用 执行目标方法
                try {
                    result = joinPoint.proceed();
                    return result;
                } catch (Throwable throwable) {
                    // 说明调用失败  记录次数
                    fish.addFailCount();
                    return "我是备胎";
                }
            case OPEN:
                // 不能调用
                return "我是备胎";
            case HALF_OPEN:
                // 可以用少许流量去调用
                int i = random.nextInt(5);
                System.out.println(i);
                if (i == 1) {
                    // 去调用
                    try {
                        result = joinPoint.proceed();
                        // 说明成功了 断路器关闭
                        fish.setStatus(FishStatus.CLOSE);
                        synchronized (fish.getLock()) {
                            fish.getLock().notifyAll();
                        }
                        return result;
                    } catch (Throwable throwable) {
                        return "我是备胎";
                    }
                }
            default:
                return "我是备胎";
        }
    }
}

熔断器模型类

/**
 * 这个是断路器的模型
 */
@Data
public class Fish {

    /**
     * 窗口时间
     */
    public static final Integer WINDOW_TIME = 20;

    /**
     * 最大失败次数
     */
    public static final Integer MAX_FAIL_COUNT = 3;


    /**
     * 断路器中有它自己的状态
     */
    private FishStatus status = FishStatus.CLOSE;

    /**
     * 当前这个断路器失败了几次
     * i++
     * AtomicInteger 可以保证线程安全
     */
    private AtomicInteger currentFailCount = new AtomicInteger(0);

    /**
     * 线程池
     */
    private ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            4,
            8,
            30,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(2000),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy()
    );


    private Object lock = new Object();

    {
        poolExecutor.execute(() -> {
            // 定期删除
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(WINDOW_TIME);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 如果断路器是开的 那么 不会去调用  就不会有失败  就不会记录次数 没有必要清零  这个线程可以不执行
                if (this.status.equals(FishStatus.CLOSE)) {
                    // 清零
                    this.currentFailCount.set(0);
                } else {
                    // 半开或者开 不需要去记录次数 这个线程可以不工作
                    // 学过生产者 消费者模型  wait notifyAll  condition singleAll await   它们只能随机唤醒某一个线程
                    // lock锁 源码  CLH 队列 放线程 A B C D E  park unpark  可以 唤醒指定的某一个线程
//                    LockSupport.park();
//                    LockSupport.unpark();
                    synchronized (lock) {
                        try {
                            lock.wait();
                            System.out.println("我被唤醒了,开始工作");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
    }
    /**
     * 记录失败次数
     */
    public void addFailCount() {
        int i = currentFailCount.incrementAndGet();  // ++i
        if (i >= MAX_FAIL_COUNT) {
            // 说失败次数已经到了阈值了
            // 修改当前状态为 open
            this.setStatus(FishStatus.OPEN);
            // 当断路器打开以后  就不能去访问了  需要将他变成半开
            // 等待一个时间窗口  让断路器变成半开
            poolExecutor.execute(() -> {
                try {
                    TimeUnit.SECONDS.sleep(WINDOW_TIME);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.setStatus(FishStatus.HALF_OPEN);
                // 重置失败次数  不然下次进来直接就会打开断路器
                this.currentFailCount.set(0);
            });
        }
    }
}

熔断器状态枚举类

public enum FishStatus {
    CLOSE,
    OPEN,
    HALF_OPEN
}

springcloud工程简介

这章讲的是常见的工程结构,可以为以后自己写项目做参考

注意,所有工程的包路径需要统一

在这里插入图片描述

这里进行说明:

  • 我们把所有的实体类信息存放在project-domain
  • order-center依赖于project-domaincommon-api模拟实现订单服务的代码
  • user-center依赖于project-domaincommon-api模拟实现用户服务的代码
  • common-api用来实现feign的远程调用功能,所有需要暴露出来的接口都可以在里面创建,在order-centeruser-center分别有feign的实现类,在common-api同时还包含了关于熔断的一些操作

首先创建maven工程:主要做的是依赖管理,版本控制等工作

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">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>project-domain</module>
        <module>common-api</module>
        <module>user-center</module>
        <module>order-center</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.powernode</groupId>
    <artifactId>04-feign-project</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!--全局版本号控制的地方-->
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>

    <!-- 这里的依赖所以的子模块都会有-->
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <!-- 加在这里的依赖不会被这的引入进项目 只是做了一个版本控制-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 打包 仓库 等配置 -->
<!--    <build>-->
<!--    </build>-->
</project>
创建common-api模块

common-api模块主要存放一些通过openfeign开放出来的api,这样所有的项目只要依赖于这个项目就可以实现远程连接了

方法是:定义openfeign相关接口

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>04-feign-project</artifactId>
        <groupId>com.powernode</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>common-api</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.powernode</groupId>
            <artifactId>project-domain</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

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

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

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

</project>

定义feign文件下的远程调用接口

@FeignClient(value = "order-service", fallback = UserOrderFeignHystrix.class)
public interface UserOrderFeign {
    // 查询订单
    @GetMapping("/order/getOrderByUserId")
    Order getOrderByUserId(@RequestParam Integer userId);
}

hystrix文件夹下的熔断实现类

@Component
public class UserOrderFeignHystrix implements UserOrderFeign {
    /**
     * 一般远程调用的熔断可以直接返回null
     * @param userId
     * @return
     */
    @Override
    public Order getOrderByUserId(Integer userId) {
        return null;
    }
}
创建order-center模块

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>04-feign-project</artifactId>
        <groupId>com.powernode</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>order-center</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.powernode</groupId>
            <artifactId>common-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>


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

</project>

启动类

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

controller

@RestController
public class OrderController implements UserOrderFeign {
    @Override
    public Order getOrderByUserId(Integer userId) {
        System.out.println(userId);
        Order order = Order.builder()
                .name("青椒肉丝盖饭")
                .price(15D)
                .orderId(1)
                .build();
        return order;
    }
}

配置文件

server:
    port: 8080
spring:
    application:
        name: order-service
    zipkin:
        base-url: http://localhost:9411
    sleuth:
        sampler:
            probability: 1  #配置采样率  默认的采样比例为: 0.1,即 10%,所设置的值介于 0 到 1 之间,1 则表示全部采集
            rate: 10   #为了使用速率限制采样器,选择每秒间隔接受的trace量,最小数字为0,最大值为2,147,483,647(最大int) 默认为10。
eureka:
    client:
        service-url:
            defaultZone: http://192.168.153.1:8761/eureka
    instance:
        hostname: localhost
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
创建project-domain模块

该模块用来存放左右的实体类

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>04-feign-project</artifactId>
        <groupId>com.powernode</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>project-domain</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

实体类信息

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Order {

    private Integer orderId;
    private String name;
    private Double price;

}
创建user-center模块

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>04-feign-project</artifactId>
        <groupId>com.powernode</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-center</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.powernode</groupId>
            <artifactId>common-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!--  暴露自身检查端点 endPoints 一个依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

    </dependencies>

</project>

controller

@RestController
public class UserController {
    @Resource
    public UserOrderFeign userOrderFeign;

    @GetMapping("find")
    public Order findOrder() {
        return userOrderFeign.getOrderByUserId(1);
    }
}

启动类

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class UserServiceApp {

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

配置文件

server:
    port: 8081
spring:
    application:
        name: user-service
    zipkin:
        base-url: http://localhost:9411
    sleuth:
        sampler:
            probability: 1  #配置采样率  默认的采样比例为: 0.1,即 10%,所设置的值介于 0 到 1 之间,1 则表示全部采集
            rate: 10   #为了使用速率限制采样器,选择每秒间隔接受的trace量,最小数字为0,最大值为2,147,483,647(最大int) 默认为10。
eureka:
    client:
        service-url:
            defaultZone: http://192.168.153.1:8761/eureka
    instance:
        hostname: localhost
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
feign:
    hystrix:
        enabled: true # 开启熔断
management:
    endpoints:
        web:
            exposure:
                include: '*'
Sleuth链路追踪组件

远程调用最好不要超过三层

pom文件中引入依赖

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

配置文件中添加配置

spring:
    application:
        name: user-service
    zipkin:
        base-url: http://localhost:9411
    sleuth:
        sampler:
            probability: 1  #配置采样率  默认的采样比例为: 0.1,即 10%,所设置的值介于 0 到 1 之间,1 则表示全部采集
            rate: 10   #为了使用速率限制采样器,选择每秒间隔接受的trace量,最小数字为0,最大值为2,147,483,647(最大int)

启动服务,然后访问http://loaclhost:9411端口

admin监控

用于监控模块的所有信息,可以在idea中的Endpoints中查看

admin模块可以直接监控eureka注册中心中的所有服务信息,前提是,服务必须有admin-client依赖,并且暴露出所有的信息

实质就是心跳检测,如果被检测的模块不暴露出信息是无法监测的

暴露自身信息

management:
    endpoints:
        web:
            exposure:
                include: '*'

创建admin-server模块

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.powernode</groupId>
    <artifactId>admin-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>05-admin-server</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-boot-admin.version>2.3.0</spring-boot-admin.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>de.codecentric</groupId>
                <artifactId>spring-boot-admin-dependencies</artifactId>
                <version>${spring-boot-admin.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

</project>

启动类

@SpringBootApplication
@EnableEurekaClient
@EnableAdminServer // 开启admin服务注解
public class AdminServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AdminServerApplication.class, args);
    }
}

配置文件

server:
    port: 10086   # 端口号范围   0-65535
spring:
    application:
        name: admin-server
eureka:
    client:
        service-url:
            defaultZone: http://192.168.153.1:8761/eureka
    instance:
        hostname: localhost
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
management:
    endpoints:
        web:
            exposure:
                include: '*'  # 暴露所有的监控端点 # 如果一个服务需要被监控 那么就要讲自身的一些情况(一些信息接口)暴露出去

访问配置的端口信息 :http://localhost:10086

Gateway组件

网关是微服务最边缘的服务,直接暴露给用户,用来做用户和微服务的桥梁

  1. 没有网关:客户端直接访问我们的微服务,会需要在客户端配置很多的ip:port,如果user-service 并发比较大,则无法完成负载均衡
  2. 有网关:客户端访问网关,网关来访问微服务,(网关可以和注册中心整合,通过服务名称找到目标的ip:prot)这样只需要使用服务名称即可访问微服务,可以实现负载均衡,可以实现token 拦截,权限验证,限流等操作

Gateway三大核心概念

Route(路由)(重点和eureka 结合做动态路由)

路由信息的组成:由一个ID、一个目的URL、一组断言工厂、一组Filter 组成。如果路由断言为真,说明请求URL 和配置路由匹配。

Predicate(断言)(就是一个返回bool 的表达式)

Java 8 中的断言函数。lambda 四大接口供给形,消费性,函数型,断言型Spring Cloud Gateway 中的断言函数输入类型是Spring 5.0 框架中的ServerWebExchangeSpring Cloud Gateway 的断言函数允许开发者去定义匹配来自于Http Request 中的任何信息比如请求头和参数。

Filter(过滤) (重点)

一个标准的Spring WebFilterWeb 三大组件(servlet listener filter) mvcinterceptor

Spring Cloud Gateway 中的Filter 分为两种类型的Filter,分别是Gateway FilterGlobal Filter。过滤器Filter 将会对请求和响应进行修改处理。

一个是针对某一个路由(路径)的filter 对某一个接口做限流

一个是针对全局的filter token ip 黑名单

入门案例

创建login-service模块

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.powernode</groupId>
    <artifactId>login-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>02-login-service</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

</project>

controller方法

@RestController
@CrossOrigin // 加上这个注解之后 这个controller里面的方法就可以直接被访问了
public class LoginController {


    @Autowired
    public StringRedisTemplate redisTemplate;

    @GetMapping("doLogin")
    @CrossOrigin
    public String doLogin(String name, String pwd) {
        System.out.println(name);
        System.out.println(pwd);
        // 这里假设去做了登录
        User user = new User(1, name, pwd, 18);
        // token
        String token = UUID.randomUUID().toString();

        return token;
    }
}

配置文件

server:
    port: 8081
spring:
    application:
        name: login-service

创建gateway模块

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.powernode</groupId>
    <artifactId>gateway-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>01-gateway-server</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

</project>

配置文件

server:
    port: 80 # 网关一般是80
spring:
    application:
        name: gateway-server
    cloud:
        gateway:
            enabled: true # =只要加了依赖 默认开启
            routes: # 如果一个服务里面有100个路径  如果我想做负载均衡 ??   动态路由
                -   id: login-service-route  # 这个是路由的id 保持唯一即可
                    #                    uri: http://localhost:8081   # uri统一资源定位符   url 统一资源标识符
                    uri: lb://login-service   # uri统一资源定位符   url 统一资源标识符
                    predicates: # 断言是给某一个路由来设定的一种匹配规则 默认不能作用在动态路由上
                        - Path=/doLogin  # 匹配规则  只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
                        - After=2022-03-22T08:42:59.521+08:00[Asia/Shanghai]
                        - Method=GET,POST

路由方式

代码方式路由

@Configuration
public class RouteConfig {

    /**
     * 代码的路由  和yml不冲突  都可以用
     * 如果你的uri后面给了一个访问地址 和匹配地址相同 那么就不会再凭借
     * @param builder
     * @return
     */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return  builder.routes()
            .route("guochuang-id",r->r.path("/guochuang").uri("https://www.bilibili.com/guochuang"))
            .route("dance-id",r->r.path("/v/dance").uri("https://www.bilibili.com"))
            .route("kichiku-id",r->r.path("/v/kichiku").uri("https://www.bilibili.com"))
            .build();
    }
}

动态路由,需要将gateway注册到eureka

访问的时候需要加上注册中心的实例名称 http://localshot/login-service/doLogin

server:
    port: 80 # 网关一般是80
spring:
    application:
        name: gateway-server
    cloud:
        gateway:
            enabled: true # =只要加了依赖 默认开启
            discovery:
                locator:
                    enabled: true  # 开启动态路由  开启通用应用名称 找到服务的功能
                    lower-case-service-id: true  # 开启服务名称小写
eureka:
    client:
        service-url:
            defaultZone: http://192.168.153.1:8761/eureka
        registry-fetch-interval-seconds: 3 # 网关拉去服务列表的时间缩短
    instance:
        hostname: localhost
        instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

断言工厂

文档地址 https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gateway-request-predicates-factories

server:
    port: 80 # 网关一般是80
spring:
    application:
        name: gateway-server
    cloud:
        gateway:
            enabled: true # =只要加了依赖 默认开启
            routes: # 如果一个服务里面有100个路径  如果我想做负载均衡 ??   动态路由
                -   id: login-service-route  # 这个是路由的id 保持唯一即可
                    #                    uri: http://localhost:8081   # uri统一资源定位符   url 统一资源标识符
                    uri: lb://login-service   # uri统一资源定位符   url 统一资源标识符
                    predicates: # 断言是给某一个路由来设定的一种匹配规则 默认不能作用在动态路由上
                        - Path=/doLogin  # 匹配规则  只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
                        - After=2022-03-22T08:42:59.521+08:00[Asia/Shanghai]
                        - Method=GET,POST
                    #                        - Query=name,admin.   #正则表达式的值

Filter过滤器工厂

gateway 里面的过滤器和Servlet 里面的过滤器,功能差不多,路由过滤器可以用于修改进入Http 请求和返回Http 响应

自定义过滤器

@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {

    /**
     * 这个就是过滤的方法
     * 过滤器链模式
     * 责任链模式
     * 网关里面有使用  mybatis的 二级缓存有变种责任链模式
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 针对请求的过滤  拿到请求  header  url 参数 ....
        ServerHttpRequest request = exchange.getRequest();
        // HttpServletRequest  这个是web里面的
        // ServerHttpRequest  webFlux里面 响应式里面的
        String path = request.getURI().getPath();
        System.out.println(path);
        HttpHeaders headers = request.getHeaders();
        System.out.println(headers);
        String methodName = request.getMethod().name();
        System.out.println(methodName);
        String ip = request.getHeaders().getHost().getHostString();
        System.out.println(ip);
        // 响应相关的数据
        ServerHttpResponse response = exchange.getResponse();
        // .....逻辑处理
        // 放行 到下一个过滤器了
        return chain.filter(exchange);
    }

    /**
     * 指定顺序的方法
     * 越小越先执行
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

模拟IP拦截

@Component
public class IPCheckFilter implements GlobalFilter, Ordered {

    /**
     * 网关的并发比较高 不要再网关里面直接操作mysql
     * 后台系统可以查询数据库 用户量 并发量不大
     * 如果并发量大 可以查redis 或者 在内存中写好
     */
    public static final List<String> BLACK_LIST = Arrays.asList("127.0.0.1", "144.128.232.147");

    /**
     * 1.拿到ip
     * 2.校验ip是否符合规范
     * 3.放行/拦截
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String ip = request.getHeaders().getHost().getHostString();
        // 查询数据库 看这个ip是否存在黑名单里面   mysql数据库的并发
        // 只要是能存储数据地方都叫数据库 redis  mysql
        if (!BLACK_LIST.contains(ip)) {
            return chain.filter(exchange);
        }
        // 拦截
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().set("content-type","application/json;charset=utf-8");
        HashMap<String, Object> map = new HashMap<>(4);
        map.put("code", 438);
        map.put("msg","你是黑名单");
        ObjectMapper objectMapper = new ObjectMapper();
        byte[] bytes = new byte[0];
        try {
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }

    @Override
    public int getOrder() {
        return -5;
    }
}

Springcloud alibaba Nacos注册中心

  • Nacos 致力于发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
  • Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构(例如微服务范式、云原生范式) 的服务基础设施

概念

服务(Service)

服务是指一个或一组软件功能(例如特定信息的检索或一组操作的执行),其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。Nacos 支持主流的服务生态,如Kubernetes ServicegRPC|Dubbo RPC Service 或者Spring Cloud RESTful Service.

服务注册中心(Service Registry)

服务注册中心,它是服务实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查API 来验证它是否能够处理请求。

服务元数据(Service Metadata)

服务元数据是指包括服务端点(endpoints)、服务标签、服务版本号、服务实例权重、路由规则、安全策略等描述服务的数据

服务提供方(Service Provider)

是指提供可复用和可调用服务的应用方

服务消费方(Service Consumer)

是指会发起对某个服务调用的应用方

配置(Configuration) 配置文件中心

在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如WARJAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。

配置管理(Configuration Management)

在数据中心中,系统中所有配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动统称为配置管理。

名字服务(Naming Service)

提供分布式系统中所有对象(Object)、实体(Entity)的“名字”到关联的元数据之间的映射管理服务,例如ServiceName -> Endpoints Info, Distributed Lock Name -> LockOwner/Status InfoDNS Domain Name -> IP List, 服务发现和DNS 就是名字服务的2 大场景。

配置服务(Configuration Service)

在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。

目录介绍

  • bin:可执行文件夹目录,包含:启动、停止命令等等
  • conf:配置文件目录
  • target:存放naocs-server.jar
  • LICENSE:授权信息,Nacos 使用Apache License Version 2.0 授权
  • NOTICE:公告信息

修改配置文件

1、application.properties

Nacos 默认使用嵌入式数据库实现数据的存储,并不方便观察数据存储的基本情况,这里面我们修改为使用Mysql 数据库做数据的存储,方便我们观察数据的结构。

spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456

nacos自带的sql文件在conf目录下nacos-mysql.sql

在数据库工具中直接导入即可

启动nacos

startup.cmd -m standalone # 单机版启动

也可以修改nacos的配置文件,默认是集群的方式。目录是bin下面的startup.cmd,以下是简略信息。

@echo off
rem Copyright 1999-2018 Alibaba Group Holding Ltd.
rem Licensed under the Apache License, Version 2.0 (the "License");
rem you may not use this file except in compliance with the License.
rem You may obtain a copy of the License at
rem
rem      http://www.apache.org/licenses/LICENSE-2.0
rem
rem Unless required by applicable law or agreed to in writing, software
rem distributed under the License is distributed on an "AS IS" BASIS,
rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
rem See the License for the specific language governing permissions and
rem limitations under the License.
if not exist "%JAVA_HOME%\bin\java.exe" echo Please set the JAVA_HOME variable in your environment, We need java(x64)! jdk8 or later is better! & EXIT /B 1
set "JAVA=%JAVA_HOME%\bin\java.exe"

setlocal enabledelayedexpansion

set BASE_DIR=%~dp0
rem added double quotation marks to avoid the issue caused by the folder names containing spaces.
rem removed the last 5 chars(which means \bin\) to get the base DIR.
set BASE_DIR="%BASE_DIR:~0,-5%"

set CUSTOM_SEARCH_LOCATIONS=file:%BASE_DIR%/conf/

set MODE="standalone"
set FUNCTION_MODE="all"
set SERVER=nacos-server
set MODE_INDEX=-1
set FUNCTION_MODE_INDEX=-1
set SERVER_INDEX=-1
set EMBEDDED_STORAGE_INDEX=-1
set EMBEDDED_STORAGE=""

入门案例

创建client-a,因为两个服务的配置文件都是相同的,所以client-b就不再重复pom文件内容了

pom文件

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    <spring-cloud-alibaba.version>2.2.7.RELEASE</spring-cloud-alibaba.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

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

配置文件

server:
    port: 8080
spring:
    application:
        name: nacos-client-a
    cloud:
        nacos: # 如果不指定命名空间会默认注册到public里面去 如果没有指定分组 会注册到DEFAULT_GROUP
            server-addr: localhost:8848 # 往这个地址去注册自己
            username: nacos
            password: nacos
            discovery:
                namespace: f5e847a0-119d-4baa-9dff-4bf15e418a42 # 对应命名空间的id
                group: A_GROUP

client-b只有配置文件不同

server:
    port: 8081
spring:
    application:
        name: nacos-client-b
    cloud:
        nacos:
            server-addr: localhost:8848
            username: nacos
            password: nacos
            discovery:  # 这里是和注册相关的配置
                namespace: f5e847a0-119d-4baa-9dff-4bf15e418a42
                group: A_GROUP # 往哪个组注册
                service: user-service  # 这个才是注册列表的名字 如果不写 默认是取${spring.application.name}

启动类:a服务与b服务的启动类上的注解是一致的。

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

整合openFeign

上面的pom文件中已经引入了openfegin的组件

client-b中定义测试接口

@GetMapping("info")
public String info() {
    return "测试B";
}

client-a项目中添加openfeign相关代码

@FeignClient(value = "user-service") // 注意这里,这里填写的是b服务的注册名称,别写错了
public interface TestFeign {
    @GetMapping("info")
    public String info();
}

client-a中的接口

@Autowired
public TestFeign testFeign;

@GetMapping("test")
public String test() {
    return testFeign.info();
}

client-a中的启动类

@SpringBootApplication
@EnableDiscoveryClient  // 开启服务发现客户端
@EnableFeignClients // 开启openfeign的注解
public class NacosClientAApplication {
    public static void main(String[] args) {
        SpringApplication.run(NacosClientAApplication.class, args);
    }
}

nacos配置文件详解

配置项Key默认值说明
服务端地址spring.cloud.nacos.discovery.server-addrNacos Server 启动监听的ip 地址和端口
服务名spring.cloud.nacos.discovery.service${spring.application.name}给当前的服务命名
服务分组spring.cloud.nacos.discovery.groupDEFAULT_GROUP设置服务所处的分组
权重spring.cloud.nacos.discovery.weight1取值范围1 到100,数值越大,权重越大
网卡名spring.cloud.nacos.discovery.network-interfaceIP 未配置时,注册的IP 为此网卡所对应的IP 地址,如果此项也未配置,则默认取第一块网卡的地址
注册的ip地址spring.cloud.nacos.discovery.ip优先级最高
注册的端口spring.cloud.nacos.discovery.port-1默认情况下不用配置,会自动探测
命名空间spring.cloud.nacos.discovery.namespace常用场景之一是不同环境的注册的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。
AccessKeyspring.cloud.nacos.discovery.access-key当要上阿里云时,阿里云上面的一个云账号名
SecretKeyspring.cloud.nacos.discovery.secret-key当要上阿里云时,阿里云上面的一个云账号密码
Metadataspring.cloud.nacos.discovery.metadata使用Map 格式配置,用户可以根据自己的需要自定义一些和服务相关的元数据信息
日志文件名spring.cloud.nacos.discovery.log-name
集群spring.cloud.nacos.discovery.cluster-nameDEFAULT配置成Nacos 集群名称
接入点spring.cloud.nacos.discovery.enpointUTF-8地域的某个服务的入口域名,通过此域名可以动态地拿到服务端地址
是否继承ribbonribbon.nacos.enabledtrue
是否开启Nacos Watchspring.cloud.nacos.discovery.watch.enabledtrue可以设置成false 来关闭watch

Springcloud alibaba Nacos配置中心

使用Spring Cloud Alibaba Nacos Config,可基于Spring Cloud 的编程模型快速接入Nacos 配置管理功能

入门案例

pom文件

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
    <spring-cloud-alibaba.version>2.2.7.RELEASE</spring-cloud-alibaba.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

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

在项目中创建bootstrap.yml文件

server:
    port: 8081
spring:
    application:
        name: nacos-config-a
# 项目在启动的时候去哪里找它对应的配置文件呢??
    cloud:
        nacos:
            config:
                server-addr: localhost:8848
                username: nacos # nacos的登录名
                password: nacos # nacos的登录密码
                prefix: nacos-config  # 读哪个配置文件 默认用的是应用名称  是可以修改的 默认是spring.application.name
                file-extension: yml  # 文件类型
    profiles:
        active: dev

编写配置类,这里建议创建一个类来管理读取的内容

@RefreshScope 注解:动态刷新配置文件中的内容

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@RefreshScope  // 给这个类上 添加一个刷新的作用域
public class Hero {
    @Value("${hero.name}")
    private String name;

    @Value("${hero.age}")
    private Integer age;

    @Value("${hero.address}")
    private String address;
}

测试controller

@RestController
public class TestController {

    @Autowired
    public Hero hero;

    @GetMapping("info")
    public String heroInfo() {
        return hero.getName() + ":" + hero.getAge() + ":" + hero.getAge();
    }
}

读取配置文件的方式

nacos 配置中心通过namespacedataIdgroup 来唯一确定一条配置。

  • Namespace:即命名空间。默认的命名空间为public,我们可以在Nacos 控制台中新建命名空间;
  • dataId:即配置文件名称
  • Group : 即配置分组, 默认为DEFAULT_GROUP , 可以通过spring.cloud.nacos.config.group 配置。

其中:dataId是最关键的配置字段,格式如下:

${prefix} - ${spring.profiles.active} . ${file-extension}
  • prefix 默认为spring.application.name 的值, 也可以通过配置项spring.cloud.nacos.config.prefix 来配置;
  • spring.profiles.active 即为当前环境对应的profile 。注意, 当spring.profiles.active 为空时,对应的连接符-也将不存在,dataId 的拼接格式变成${prefix}.${file-extension}
  • file-extension 为配置内容的数据格式, 可以通过配置项spring.cloud.nacos.config.file-extension 来配置。

注意: 在写dataId 的时候一定要添加文件类型后缀

如: nacos-config-dev.yml

Nacos 配置中心的namespacedataIdgroup 可以方便灵活地划分配置。比如,我们现在有一个项目需要开发,项目名称为test,项目开发人员分为两个组:GROUP_AGROUP_B,项目分为三个环境:开发环境dev、测试环境test 和生产环境prod

如:

server:
    port: 8081
spring:
    application:
        name: nacos-config-a
    cloud:
        nacos:
            config:
                server-addr: localhost:8848
                username: nacos
                password: nacos
                prefix: nacos-config  # 读哪个配置文件 默认用的是应用名称  是可以修改的
                file-extension: yml  # 文件类型
    profiles:
        active: dev

获取多配置文件

同一命名空间下,不同的组

server:
    port: 8082
spring:
    application:
        name: nacos-config-test
    cloud:
        nacos:
            config:
                server-addr: localhost:8848
                username: nacos
                password: nacos
                namespace: c52e715f-3f00-4ad8-803b-9c8dba19a221 # 目前读取多配置文件的方式只支持在同一个命名空间下
                file-extension: yml
                extension-configs: # 可以读多个配置文件 需要在同一个命名空间下面 可以是不同的组
                    -   dataId: user-center-dev.yml
                        group: A_GROUP
                        refresh: true
                    -   dataId: member-center-dev.yml
                        group: B_GROUP
                        refresh: false  # 不去动态刷新配置文件
    profiles:
        active: dev

常用配置

配置项key默认值说明
服务端地址spring.cloud.nacos.config.server-addr
DataId 前缀spring.cloud.nacos.config.prefixspring.application.name
Groupspring.cloud.nacos.config.groupDEFAULT_GROUP
dataID 后缀及内容文件格式spring.cloud.nacos.config.file-extensionpropertiesdataId 的后缀,同时也是配置内容的文件格式
配置内容的编码方式spring.cloud.nacos.config.encodeUTF-8配置的编码
获取配置的超时时间spring.cloud.nacos.config.timeout3000单位为ms
配置的命名空间spring.cloud.nacos.config.namespace常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源隔离等。
AccessKeyspring.cloud.nacos.config.access-key
SecretKeyspring.cloud.nacos.config.secret-key
相对路径spring.cloud.nacos.config.context-path服务端API 的相对路径
接入点spring.cloud.nacos.config.endpoint地域的某个服务的入口域名,通过此域名可以动态地拿到服务端地址
是否开启监听和自动刷新spring.cloud.nacos.config.refresh.enabledtrue

总结

bootstrap.yml写什么内容?

  • 应用名称 spring.application.name
  • nacos的注册和拉取的配置

nacos中的配置文件写什么内容?

  • 远端放 端口 数据源 redis mq 能放远端的全放 因为方便管理和修改 包括自定义配置
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值