初步接触微服务学习存档

该内容原先应该发布于本年7月左右的,到了现在才发布主要还是还是想起来有博客,不如发到网上留档后面在来修改
以下内容为个人学习总结,留档使用,因为要配合项目食用所以光看肯定看不懂的啦(笑

微服务实例代码搭建

微服务的实现分为一个生产一个消费

具体的案例实现见mrme-springCloudNetflix

生产方:就是正常的crud,要编写controller,service,dao,差不多就是一般的后端

消费方:由于通常是远程调用,只需要编写controller即可,但是还是需要编写启动类和xml文件

image-20251028171910827

为了实现远程调用,这里使用了restTemplate组件,springboot中由于没有自动配置,所以需要配置。

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

优化

其实就是对相同代码的的处理,上述的pojo中都是使用的相同的对象,我们可以使用一个模块吧pojo进行单独加入,只需要module引入即可

其他问题如下(也就是后面要学的一些技术)

在服务消费方consumer中,我们把url地址硬编码到了controller控制器方法中,不方便后期的维护。
在服务消费方consumer中,需要记忆服务提供方provider的地址,如果后续出现变更,如果得不到通知,地址会失效。
consumer不清楚provider的状态,provider如果宕机,consumer会不知道。
不管是consumer还是provider,只有一台服务,不具备高可用性。
即使provider形成集群,consumer如何进行负载均衡的调用?

也就是

服务管理
如何自动注册和发现
如何实现状态监管
如何实现动态路由
服务如何实现负载均衡
服务如何解决容灾问题
服务如何实现统一配置

Eureka

服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。同时,服务提供方与Eureka之间通过“心跳”机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。这就实现了服务的自动注册、发现、状态监控

image-20251102091219668

所以就是一个调度中心(本身也是个微服务)

搭建Eureka(客户端部分)

<dependencies>
        <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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>
server:
  port: 7001
eureka:
  client:
    register-with-eureka: false #禁止自己注册自己
    fetch-registry: false #禁止抓取服务信息
    service-url:
      defaultZone: http://localhost:7001/eureka/

访问:localhost:7001即可

提供服务(服务端部分 )

引入依赖

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

配置文件

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka/

启动类,添加

@EnableDiscoveryClient //开启服务发现的功能

其实也可以使用@EnableEurekaClient,但是这种注册中心类型的服务有很多,例如nacos等等,@EnableDiscoveryClient直接包括了全部(更好用)

结果:

image-20251102101047592

搭建eureka集群(多个客户端)

就是多个Eureka互相注册对方的集群,这样就可以获取对方的数据从而保证备份的效果

实现:例如要架设7001和7002端口的Eureka,就是让7001的注册7002,反之同理

server:
  port: 7001
eureka:
  client:
    register-with-eureka: false #禁止自己注册自己
    fetch-registry: false #禁止抓取服务信息
    service-url:
      defaultZone: http://localhost:7002/eureka/ #注册的eureka的地址
      
server:
  port: 7002
eureka:
  client:
    register-with-eureka: false #禁止自己注册自己
    fetch-registry: false #禁止抓取服务信息
    service-url:
      defaultZone: http://localhost:7001/eureka/ #注册的eureka的地址

这个时候最好服务端也改改配置,由于我们假设两个客户端,我们就要都注册一遍

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka/ , http://localhost:7001/eureka/ #注册的eureka的地址

服务集群(多个服务端)

配置同上,需要改个端口用提供方来做例子8001和8002

server:
  port: 8002

当我们有了两服务提供方以后,需要改造服务消费端的服务调用方式。应该访问两台服务提供方的哪一台服务呢?以前在消费方指定访问的是8001端口的微服务。现在我们需要修改访问提供方的访问规则,规则就是通过服务提供方的名称进行访问,不再通过服务提供方的ip+端口进行访问。

String url = "http://localhost:8001/provider/findById?id="+id; //需要改成同时可以访问8001和8002	
@RequestMapping("findById/{id}")
public Result<Payment> findById(@PathVariable("id") Long id){
//通过服务实例名称进行访问
String url = "http://SERVICE-PROVIDER/provider/findById?id=" + id;
Result result = restTemplate.getForObject(url, Result.class);
return result;
}

使用名称访问数据是通过负载实现的,需要开启,否则会报错:因为根据服务名称不知道到底调用哪台服务提供方的机器

@Configuration
public class MyConfig {
@Bean
@LoadBalanced //开启负载均衡的访问
public RestTemplate restTemplate(){
return new RestTemplate();
}
}

之后就可以使用了


服务发现

获取这些详细服务的各种细节(比如IP地址 服务端口号等),需要使用一个新的注解来开启服务发现功能,这个注解就是@EnableDiscoveryClient。对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息。

在微服务的主启动类上添加@EnableDiscoveryClient注解就可以开启这个功能

    @Resource
    private DiscoveryClient discoveryClient;
    
    @GetMapping("/customer/discovery")
    public Object discovery(){
        List<String> services = discoveryClient.getServices();
        for(String service : services){
            log.info("service:"+service);
        }
        // 根据服务提供方的名称获取对应的服务实例信息
        List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
        for(ServiceInstance instance : instances){
            String host = instance.getHost();
            int port = instance.getPort();
            String serviceId = instance.getServiceId();
            log.info("host:" + host + "port:" + port + "serviceId:" + serviceId);
        }
        return this.discoveryClient;
    }

示例获取


Eureka的自我保护机制

多数是服务假死导致

image-20251102121001743

image-20251102121141031


其他注册中心

zookeeper

需要安装到linxu中使用,linux中的安装等等不进行解释,这里直接进行idea中的代码

配置

<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.apache.zookeeper</groupId>
                <artifactId>zookeeper</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.9</version>
        <exclusions>
            <exclusion>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
server:
	port: 8003
spring:
	application:
		name: service-provider
	datasource:
		driver-class-name: com.mysql.jdbc.Driver
		url: jdbc:mysql://192.168.10.137:3306/cloud2020
		username: root
		password: Admin123!
	cloud:
		zookeeper:
			connect-string: 192.168.10.138:2181 # 注册到zk的配置信息
mybatis:
	config-location: classpath:/mybatis/sqlMapConfig.xml
	mapper-locations: classpath:/mybatis/mapper/*.xml

注意这时候如果使用负载均衡需要将name中的‘name: service-provider‘大小写要保持一致


Consul

这个在Windows上部署的

在对应的包下有一个exe和zip文件,使用开发者启动

consul agent -dev

默认访问是8500端口

依赖

<!--springcloud consul server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

启动类同上

配置文件

spring:
	application:
		name: service-provider # 配置服务名称
	datasource:
		driver-class-name: com.mysql.jdbc.Driver
		url: jdbc:mysql://192.168.10.137:3306/cloud2020
		username: root
		password: Admin123!
	cloud:
		consul:
			host: localhost
			port: 8500
			discovery:
				service-name: ${spring.application.name}

主要在9~14

同样要注意负载均衡


CAP理论

这个是注册中心的设计理念

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

借用一下维基百科CAP理论一文中关于C、A、P三者的定义。

Consistency : Every read receives the most recent write or an error

Availability : Every request receives a (non-error) response – without the guarantee that it contains the most recent write

Partition tolerance : The system continues to operate despite an arbitrary number of messages being dropped (or delayed) by the network between nodes

翻译一下就是:

一致性:对于客户端的每次读操作,要么读到的是最新的数据,要么读取失败。换句话说,一致性是站在分布式系统的角度,对访问本系统的客户端的一种诺:要么我给您返回一个错误,要么我给你返回绝对一致的最新数据,不难看出,其强调的是数据正确。

可用性:任何客户端的请求都能得到响应数据,不会出现响应错误。换句话说,可用性是站在分布式系统的角度,对访问本系统的客户的另一种承诺:我一定会给您返回数据,不会给你返回错误,但不保证数据最新,强调的是不出错。

分区容忍性:由于分布式系统通过网络进行通信,网络是不可靠的。当任意数量的消息丢失或延迟到达时,系统仍会继续提供服务,不会挂掉。换句话说,分区容忍性是站在分布式系统的角度,对访问本系统的客户端的再一种承诺:我会一直运行,不管我的内部出现何种数据同步问题,强调的是不挂掉。

权衡C A

image-20251104095444308

image-20251104095438029


Ribbon组件

负载均衡组件,就是对服务集群进行性能的分配,具体略

使用

在使用Eureka的时候就已经使用到了,并且Eureka的包中就有对应的依赖

String url = "http://SERVICE-PROVIDER/provider/findById?id="+id;

这里使用SERVICE-PROVIDER就是负载均衡通过名称来获取

负载均衡策略与使用(八股)

7个,每个了解如何运作即可

(1) 轮询策略
轮询策略:RoundRobinRule,按照一定的顺序依次调用服务实例。比如一共有 3 个服务,第一次调用服务 1,第二次调用服务 2,第三次调用服务3,依次类推。此策略的配置设置如下:

service-provider: # 服务提供方的名称
	ribbon:
		NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #设置负载均衡

(2)权重策略
权重策略:WeightedResponseTimeRule,根据每个服务提供者的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性也就越低。它的实现原理是,刚开始使用轮询策略并开启一个计时器,每一段时间收集一次所有服务提供者的平均响应时间,然后再给每个服务提供者附上一个权重,权重越高被选中的概率也越大。此策略的配置设置如下:

service-provider: # 服务提供方的名称
	ribbon:
		NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

(3) 随机策略

随机策略:RandomRule,从服务提供者的列表中随机选择一个服务实例。此策略的配置设置如下:

service-provider: # 服务提供方的名称
	ribbon:
		NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #设置随机策略的负载均衡

(4) 最小连接数策略

最小连接数策略:BestAvailableRule,也叫最小并发数策略,它是遍历服务提供者列表,选取连接数最小的⼀个服务实例。如果有相同的最小连接数,那么会调用轮询策略进行选取。此策略的配置设置如下:

service-provider: # 服务提供方的名称
	ribbon:
		NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule #设置负载均衡

(5) 重试策略

重试策略:RetryRule,按照轮询策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回null。此策略的配置设置如下:

ribbon:
	ConnectTimeout: 2000 # 请求连接的超时时间
	ReadTimeout: 5000 # 请求处理的超时时间
service-provider: # 服务提供方的名称
	ribbon:
		NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule #设置负载均衡

(6) 可用敏感性策略

可用敏感性策略:AvailabilityFilteringRule,先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例。此策略的配置设置如下:

service-provider: # 服务提供方的名称
	ribbon:
		NFLoadBalancerRuleClassName: com.netflix.loadbalancer.AvailabilityFilteringRule

(7) 区域敏感策略

区域敏感策略:ZoneAvoidanceRule,根据服务所在区域(zone)的性能和服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似。此策略的配置设置如下:

service-provider: # 服务提供方的名称
	ribbon:
		NFLoadBalancerRuleClassName: com.netflix.loadbalancer.ZoneAvoidanceRule

除了使用配置文件的方法,还可以使用config中的bean注入的方式来加入(自定义类型)

/**
 * 注入RestTemplate组件到ioc容器
 */
@Configuration
public class RestConfig {
    @Bean
    @LoadBalanced //开启负载均衡
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
    
    @Bean
    public IRule iRule(){
        return new RoundRobinRule();
    }
}

其中的irule就是,使用的是轮询规则


轮询规则源码

具体就是高级的i++

    public RoundRobinRule() {
        this.nextServerCyclicCounter = new AtomicInteger(0);
    }
    public RoundRobinRule(ILoadBalancer lb) {
        this();
        this.setLoadBalancer(lb);
    }

无参构造保证原子性,有参构造多一个用来转服务器集群的对象

image-20251104164446818

    private int incrementAndGetModulo(int modulo) {
        int current;
        int next;
        do {
            current = this.nextServerCyclicCounter.get();
            next = (current + 1) % modulo;
        } while(!this.nextServerCyclicCounter.compareAndSet(current, next));//比较并且替换,自旋锁机制

        return next;
    }

这里最关键的方法就是就是获取当前调用的服务器%总调用的健康服务器,之后i++

如果重启服务器就会重新从1开始计数


手写简易的轮询机制

@Component
public class MyIR implements LoadBalance{

    private AtomicInteger atomicInteger = new AtomicInteger();//保证原子性的同时维护count的数

    public int getAndGetModulo(){
        int current;
        int next;
        do{
            current = this.atomicInteger.get();
            next = current + 1;
        }while(!this.atomicInteger.compareAndSet(current, next));
        return next;
    }

    @Override
    public ServiceInstance instances(List<ServiceInstance> instances) {
        int index = getAndGetModulo() % instances.size();
        return instances.get(index);
    }
}

openfeign

那么好的,简单介绍下吧

前面使用的是:Ribbon+RestTemplate,OpenFeign作用跟这个是差不多的,主要解决了Ribbon中会出现的复杂远程调用和重复代码(指的是每一个微服务代码都需要进行封装)

在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了是RestTemplate进行服务调用的方式。

前者

image-20251105153208222

后者

image-20251105153222991

OpenFeign是Feign的扩展版本,类似mybatis和mp


使用

依赖

    <dependencies>
        <!-- Open Feign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>springcloud-service-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

common就是用来存放实体类的模块

定义feign

/**
 * feign中定于接口:只需要将服务中提供方里面的控制器方法拷贝过来就行,带上springmvc的注解,并且去掉方法体即可
 */
@FeignClient(name = "service-provider")
public interface PaymentFeignClient {
    @RequestMapping("provider/findById")//这里注意下有没有分离
    public Result<Payment> findById(@RequestParam("id") Long id);
}

之后还需要在消费方中加入feign的子模块(毕竟你都要用)

        <!--feign-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>springCloud-service-openfeign</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

服务消费方改造

//    @Resource
//    RestConfig restConfig;

    @Resource
    PaymentFeignClient feignClient;

    /**
     * 1调用restTemplate进行远程访问,url为地址,Result为返回类型
     * 2调用feignClient进行远程访问,url为地址,Result为返回类型
     * @param id
     * @return
     */
    @RequestMapping("findById/{id}")
    public Result<Payment> findById(@PathVariable("id") Long id){
//        String url = "http://SERVICE-PROVIDER/provider/findById?id="+id;
//        return restConfig.restTemplate().getForObject(url,Result.class);
        Result<Payment> byId = feignClient.findById(id);
        return byId;
    }

启动类需要加入feign接口扫描

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients //扫描feign客户端,扫描规则是当前启动类所在的包以及子包
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class,args);
    }
}

接着启动服务就行


注意规范

超时控制

Feign中的默认超时时间是1秒,当然这个时间有点苛刻,比较容易就超了

application中直接改就行

ribbon: 
	ReadTimeout: 5000 # 指的是建立连接所需要的的时间
	ConnectTimeout: 5000 # 指的是建立连接之后,服务器读取资源需要的时间

接口定义规范

在feign客户端中,我们定义feign接口的包,必须是和消费方中的包名要统一

例如:com.mrme.controller.xxx-------com.mrme.feign.xxx

在消费方的启动类上面有一个扫描feign客户端的注解@EnableFeignclients注解,它的扫描规则是当前启动类所在的目录及其子目录,都可以扫描。扫描的是feign接口的代理对象,然后将这个代理对象注入到ioc容器里面

传参

如果有传参数使用到@requestParam,则feign客户端都和控制器中都需要加上@requestParam,如果传的是实体就都需要(同上)加@reqeustBody,否则会出现400错误


关于日志打印

需要那个模块加入这个功能就直接加一个配置类

@Configuration
public class FeignLever {
    @Bean
    Logger.Level feignLoggerLever(){
        return Logger.Level.FULL;//设置日志输出级别
    }
}

写法基本固定

之后需要再application中配置

logging: 
	lever: 
		com.mrme.client.PaymentClient: debug

hystrix

WAMI

豪猪,一个防御性组件

Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。

概念性名词

**(1)服务降级:**

服务器忙碌或者网络拥堵时,不让客户端等待并立刻返回一个友好提示,fallback(备选方案)。

触发服务降级的情况:程序运行异常,调用超时,服务熔断触发服务降级,线程池/信号量打满触发服务降级

**(2) 服务熔断:**

服务熔断是指调用方访问服务时通过断路器做代理进行访问,断路器会持续观察服务返回的成功、失败的状态,当失败超过设置的阈值时断路器打开,请求就不能真正地访问到服务了。然后调用服务降级的方法并给用户返回一个友好的提示信息。

**(3)服务限流:**

服务限流是指当系统资源不够,不足以应对大量请求,即系统资源与访问量出现矛盾的时候,我们为了保证有限的资源能够正常服务,因此对系统按照预设的规则进行流量限制或功能限制的一种方法。

简单案例

提供方

    @RequestMapping("hello")
    Result<Payment> hello(){
        return new Result(200,"hello",null);
    }

Feign

/**
 * feign中定于接口:只需要将服务中提供方里面的控制器方法拷贝过来就行,带上springmvc的注解,并且去掉方法体即可
 */
@FeignClient(name = "service-provider")
public interface PaymentFeignClient {
    @RequestMapping("provider/findById")//这里注意下有没有分离
    public Result<Payment> findById(@RequestParam("id") Long id);

    @RequestMapping("provider/hello")
    public Result<Payment> hello();
}

消费方

    @RequestMapping("hello")
    public Result<Payment> hello(){
        Result<Payment> result = feignClient.hello();
        return result;
    }

我们可以使用Jmeter压测工具进行高并发模拟,具体就省略了

结果就是服务必然被拖垮,这时候就需要服务降级登场了

服务降级

兜底使用,一般服务降级放在消费端,但是提供者端一样能使用,如果我们的服务不能正常被访问,就要让用户不要处于一直等待的状态,而是要执行对应的兜底方法,立马给用户返回一个友好的提示信息(一点不友好,滑稽)

兜底方法的原则:兜底方法的返回值和形参要更被兜底的方法一致

局部服务降级

写一段就懂了,消费端和提供方都是一样的

hystrix依赖

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

启动器加入组件支持

//@SpringBootApplication
//@EnableDiscoveryClient
//@EnableCircuitBreaker //开启使用Hystrix组件的功能
@SpringCloudApplication // @SpringBootApplication + @EnableDiscoveryClient + @EnableCircuitBreaker三合一注解
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class,args);
    }
}

对应方法书写

@HystrixCommand(fallbackMethod = "CallBackByhello",commandProperties = {//使用兜底方法,兜底反应配置
            //使用超时反馈,2.5秒后则返回
            @HystrixProperty(name ="execution.isolation.thread.timeoutInMilliseconds",value = "2500")
    })
    @RequestMapping("findById/{id}")
    public Result<Payment> findById(@PathVariable("id") Long id){
//        String url = "http://SERVICE-PROVIDER/provider/findById?id="+id;
//        return restConfig.restTemplate().getForObject(url,Result.class);
        Result<Payment> byId = feignClient.findById(id);
        return byId;
    }
    //备选方案
    public Result<Payment> CallBackByhello(Long id){
        return new Result(200,"findById方法触发了局部的降级策略",null);
    }

需要加入@HystrixCommand,配置对应的方法和规则,还是比较简单的嘛(?)

全局服务降级

更好用的,上面的降级策略,很明显造成了代码的杂乱,提升了耦合度,而且也很麻烦,这里只要做一个全局配置即可

@DefaultProperties(defaultFallback = "golbalCallBack")//全局降级配置
public class ConsumerController {

    @Resource
    PaymentFeignClient feignClient;

    @HystrixCommand(//只需要加入降级配置即可
            //使用超时反馈,2.5秒后则返回
            @HystrixProperty(name ="execution.isolation.thread.timeoutInMilliseconds",value = "2500")
    })
    @RequestMapping("findById/{id}")
    public Result<Payment> findById(@PathVariable("id") Long id){
//        String url = "http://SERVICE-PROVIDER/provider/findById?id="+id;
//        return restConfig.restTemplate().getForObject(url,Result.class);
        Result<Payment> byId = feignClient.findById(id);
        return byId;
    }

    //全局配置方法,只需要返回值一致即可
    public Result<Payment> golbalCallBack(Long id){
        return new Result(200,"findById方法触发了局部的降级策略",null);
    }

需要加入的:

@DefaultProperties(defaultFallback = "golbalCallBack")

@HystrixCommand(//只需要加入降级配置即可
//使用超时反馈,2.5秒后则返回
@HystrixProperty(name ="execution.isolation.thread.timeoutInMilliseconds",value = "2500")
})

还有个全局配置方法,只需要只需要返回值一致即可

Hystrix整合OpenFeign实现服务降级

更适合广大程序员宝宝体质的配置(笑),相比与上面实现了职责单一性原则

首先还是配置,支持feign

# 开启Feign对Hystrix的支持
feign:
	hystrix:
		enabled: true

对应降级策略

public class PaymentFeignClientCallBack implements PaymentFeignClient{
    @Override
    public Result<Payment> findById(Long id) {
        return new Result(200,"触发了局部的降级策略",null);
    }

    @Override
    public Result<Payment> hello() {
        return new Result(200,"触发了局部的降级策略",null);
    }
}

对应接口配置

@FeignClient(name = "service-provider",fallback = PaymentFeignClientCallBack.class)
public interface PaymentFeignClient {
    @RequestMapping("provider/findById")//这里注意下有没有分离
    public Result<Payment> findById(@RequestParam("id") Long id);

    @RequestMapping("provider/hello")
    public Result<Payment> hello();
}

控制器内只有对应的业务方法

    @RequestMapping("findById/{id}")
    public Result<Payment> findById(@PathVariable("id") Long id){
//        String url = "http://SERVICE-PROVIDER/provider/findById?id="+id;
//        return restConfig.restTemplate().getForObject(url,Result.class);
        Result<Payment> byId = feignClient.findById(id);
        return byId;
    }

服务熔断

熔断器,也叫断路器,其英文单词为:Circuit Breaker

就跟保险丝熔断差不多,一旦熔断就无法访问了(切断服务),不过还是有点不一样的

熔断状态机3个状态:

Closed:关闭状态,所有请求都正常访问。
Open:打开状态,所有请求都会被降级。Hystrix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。
Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路器,否则继续保持打开,再次进行休眠计时

三个专有名词参数

1. 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒钟。
2. 请求总数阈值:在快照时间窗内,必须满足请求总阈值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时,或其他原因失败,断路器都不会打开。
3. 错误百分比阈值:当请求总次数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时或者异常,也就是超过了50%的错误百分比,在默认设定50%阈值情况下,断路器就会打开。

给他整个活(示例)

在Hystrix中,开启熔断机制的注解是@HystrixCommand

消费方,配置业务

    @HystrixCommand(commandProperties = {
            //设置峰值,超过 3 秒,就会调用兜底方法,这个时间也可以由feign控制
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2500"),
            @HystrixProperty(name="circuitBreaker.enabled", value="true"), // 是否开启断路器
            @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),//请求次数
            @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="10000"), // 时间窗口期
            @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="60"), // 失败率达到多少后跳闸
    })
    @RequestMapping("testCircuitBreaker")
    public Result<Payment> testCircuitBreaker(Long id){
        Result<Payment> result = feignClient.testCircuitBreaker(id);
        return result;
    }

提供方实现,实际上和应该是在service中实现的,这里懒了(躺)

    @RequestMapping("testCircuitBreaker")
    public Result<Payment> testCircuitBreaker(@RequestParam Long id) {
        if(id < -1){
            throw new RuntimeException("id不能为负数");
        }else{
            return new Result(200,"testCircuitBreaker调用成功",null);
        }
    }

在Feign客户端中定义服务远程调用的接口

/**
 * feign中定于接口:只需要将服务中提供方里面的控制器方法拷贝过来就行,带上springmvc的注解,并且去掉方法体即可
 */
@FeignClient(name = "service-provider",fallback = PaymentFeignClientCallBack.class)
public interface PaymentFeignClient {
    @RequestMapping("provider/findById")//这里注意下有没有分离
    public Result<Payment> findById(@RequestParam("id") Long id);

    @RequestMapping("provider/hello")
    public Result<Payment> hello();

    @RequestMapping("provider/testCircuitBreaker")
    Result<Payment> testCircuitBreaker(@RequestParam("id") Long id);
}

Hystrix DashBoard

就是个图形化界面可以化

image-20251107111247552

首先写一个独立模板

依赖

   <dependencies>
        <!-- hystrix Dashboard-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

启动类中需要加入开启仪表盘的功能

@SpringBootApplication
@EnableHystrixDashboard //开启Hystrix仪表盘监控的功能
public class HystrixDashBoardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashBoardApplication.class,args);
}
}

配置文件

server:
	port: 9001
hystrix:
	dashboard:
		proxy-stream-allow-list: "*" #开启对微服务的所有微服进行监控

之后在需要监控的服务内加入配置类,官方提供

@Configuration
public class JavaConfig {
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new
                HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new
                ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/actuator/hystrix.stream");//访问路径
        registrationBean.setName("hystrix.stream");
        return registrationBean;
    }
}

http://localhost:9001/hystrix就行

如果要监控每个就输入对应的方法即可


GetaWay

概念

Route(路由)

路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。请求从外面过来,会先被网关处理,网关会决定该请求会调用哪个微服务,但是这个请求怎么调用呢?所以网关是不是必须要有一个路由器啊,它要可以进行请求的路由转发,具体决定该请求会调用哪个微服务。

Predicate(断言)

参考的是Java8的java.util.function.Predicate开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由转发。

Filter(过滤)

指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

核心工作流程

image-20251110102559650

释义:客户端将请求发送给SpringCloud GateWay,如果GateWay Handler Mapping确定这个请求和一个路由匹配,它将此请求发给GatwWay Web Handler进行处理。此时GatwWay Web Handler就会处理请求,通过Filter Chain 来进行处理,这个过滤器链会在请求路由前后都执行。简而言之就是通过一连串的Filter处理匹配到特定规则Predicates的请求。

流程图

image-20251110102740404

SpringCloud GateWay的执行流程大致分为如下:

  1. Gateway Client向Gateway Server发送请求

  2. 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文

  3. 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping

  4. RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用

  5. 如果过断言成功,由FilteringWebHandler创建过滤器链并调用。

  6. 请求会一次经过PreFilter--微服务--PostFilter的方法,最终返回响应。


使用

只需要配置文件即可

依赖

    <dependencies>
        <!--gateway网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--eureka-client gateWay作为网关,也要注册进服务中心-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- gateway和web不能同时存在,即web相关jar包不能导入 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>

gateway和web不能同时存在,即web相关jar包不能导入,因此这里就没有web类的依赖了

启动类,注意加支持注册中心即可

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

application.yml

server:
  port: 9527
spring:
  application:
    name: service-gateway
  cloud:
    gateway:
      routes:
        - id: consumer_route # 路由id 一般唯一即可。
          uri: http://localhost:80 # 目标url
          predicates:
          - Path=/api/consumer/** # 断言
          filters:
          - StripPrefix=1 # 过滤器,这个表示在访问前,去掉最外一层路径,通常用于表示使用到了路由
eureka:
  client:
    register-with-eureka: true
      fetch-registry: true
        service-url:
          defaultZone: http://localhost:7001/eureka/

启动后,访问http://localhost:9827/api/consumer/findById/1实际上再路由引导之后是:http://localhost:80/consumer/findById/1

启用负载均衡的写法

  cloud:
    gateway:
      routes:
        - id: provider_route # 路由id 一般唯一即可。
            uri: lb://service-provider # lb均衡的关键字 lb://微服务的名字
            predicates:
              - Path=/provider/** # 这里之后就不需要写过滤器了

断言大全(13种)

如果我们搜索RoutePredicateFactory

ctrl+H之后就可以查看对应的13种方法

image-20251113155812640

After

After 在指定时间之后,顾名思义就是只有在指定时间之后的请求才会生效,After后面跟的是一个时间

时间格式可以这样查看

public static void main(String[] args) {
	ZonedDateTime time = ZonedDateTime.now();
	System.out.println(time);
}

输出的就是

predicates:
  - Path=/api/consumer/**
  - After=2025-05-16T20:29:54.475+08:00[GMT+08:00]

Before

与after相反

Between

between 需要在设定的时间范围之内才能进行请求转发。

predicates:
  - Path=/api/consumer/**
  - Between=2025-05-16T20:19:54.475+08:00[GMT+08:00],2025-05-16T20:32:54.475+08:00[GMT+08:00]

Cookie

要求请求中包含指定Cookie名和满足特定正则要求的值,Cookie必须有两个值,第一个Cookie包含的参数名,第二个表示参数对应的值,可以是具体的值,也可以是正则表达式。

- Cookie=username,eric

Header

表示请求头中必须包含的内容。注意:参数名和参数值之间依然使用逗号,参数值可以写具体的值,也可以使用正则表达式。如果Header只有一个值表示请求头中必须包含的参数。如果有两个值,第一个表示请求头必须包含的参数名,第二个表示请求头参数对应值。

- Header=name,james

Host

匹配请求参数中Host参数的值,可以有多个,使用逗号隔开,**表示支持模糊匹配。

- Host=127.0.0.1:8080,**.test.com

Method

请求方式

- Method=GET # 只允许使用GET请求方式

Query

必须携带对应的参数。

- Query=age,18

RemoteAddr

允许访问的客户端地址,但是不能使用类似localhost这种。

- RemoteAddr=127.0.0.1

过滤器

全局

实现:实现GlobalFilter接口,默认需要完善filter和getOrder方法

需求:请求url必须携带username参数,否则不允许通过。

@Component
public class GateWayFilter implements GlobalFilter, Ordered {
    //执行过滤业务的方法
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
            chain) {
        String username =
                exchange.getRequest().getQueryParams().getFirst("username");
        if(username == null || username.equals("")){
//设置响应状态码
            exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
            return exchange.getResponse().setComplete(); //结束当前请求
        }
        return chain.filter(exchange);//过滤器放行
    }
    //定义过滤器优先级的方法
    @Override
    public int getOrder() {
        return 0;//描述过滤器执行的优先级 数字越小 优先执行的概率越高
    }
}

局部

实现:需要实现GatewayFilter接口即可

需求:定义一个局部过滤器,拦截指定的请求,并计算该请求执行的时长


@Component
public class MyFilter implements GatewayFilter, Ordered {
    //局部过滤器的执行逻辑
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
            chain) {
		//记录请求的开始时间,用Map的方式存储<start,具体毫秒时间>
        exchange.getAttributes().put("start",System.currentTimeMillis());
        return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
            @Override
            public void run() {
                long start = exchange.getAttribute("start");//从map中获取刚刚存储的时间
                System.out.println(exchange.getRequest().getURI() + "耗时:" + (System.currentTimeMillis() - start));
            }
        }));
    }
    //过滤器执行的优先级
    @Override
    public int getOrder() {
        return 0;
    }
}

注意注释


springCloud Config

配置中心,跟nacos作用是一样的

服务中心

 <dependencies>
        <!-- config Server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <!--eureka-client config Server也要注册进服务中心-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

启动类

@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer //开启服务配置中心
public class ConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigApplication.class,args);
    }
}

编写配置文件

server:
  port: 3344
spring:
  application:
    name: config-server
  cloud:
      config:
        server:
          git:
            uri: xxx # git的远程仓库地址
            search-paths:
            - springCloud-config # 远程仓库
          label: master # 分支
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka/

之后就可以先访问了

localhost:3344/master/文件.yml

客户端

    <dependencies>
        <!-- config Client 和 服务端的依赖不一样 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <!--eureka-client config Server也要注册进服务中心-->
        <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-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

启动类同上

server:
  port: 3355
spring:
  application:
    name: config-client
  cloud:
      config:
      	label: master # 分支名称
      	name: application # 配置文件名称,文件也可以是client-config-dev.yml这种格式的,这里就写 client-config
		profile: dev # 使用配置环境
		uri: http://localhost:3344 # config Server 地址
		# 综合上面四个 即读取配置文件地址为: http://locahost:3344/master/application-dev.yml
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka/

简单搞个controller

@RestController
@RequestMapping("client")
public class HelloController {
@Value("${user.username}")
String username;
@RequestMapping("hello")
public String hello(){
return username;
}
}

http://localhost:3355/client/hello访问

配置信息动态刷新

客户端获取git中的消息时候只有客户端启动的时候获取,其余时候都是先前的缓存,配置客户端是无法实时感知到发生更新的配置信息,so需要实现配置的动态刷新

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

客户端配置文件中加上:

# 暴露监控端点
management:
	endpoints:
		web:
			exposure:
				include: "*"

controller中加上注解

@RefreshScope //实时加载注解


BUS

当我们在更新gitee上面的配置以后,如果想要获取到最新的配置,需要手动刷新客户端,客户端越来越多的时候,需要每个客户端都执行一遍,这种方案就不太适合了。使用Spring Cloud Bus(国人很形象的翻译为消息总线)可以完美解决这一问题。

具体实现如下:

image-20251115112939586

利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置

这也是最合理主流的方式

代码实现

首先下载RabbitMQ,具体下载去其他地方查查吧,默认密码和账号都是guest

客户端(被提示的)

依赖

<!-- 添加rabbitMQ的消息总线支持包 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

配置文件加入类似如下,也就是加入mq配置

image-20251115113501319

添加即时刷新注解@RefreshScope注解,controller上加上@RefreshScope注解实现即时刷新

服务端(用于提示的)

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 添加rabbitMQ的消息总线支持包 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

配置文件

image-20251115113724392

需要多一个端点暴露,用于告诉其他客户端需要刷新的信号

上述配置都配置完毕后进行测试

1首先修改配置信息,模拟配置信息被修改

2使用Postman向配置服务中心发送配置动态刷新的请求:http://localhost:3344/actuator/bus-refresh(3344是服务端的端口)

之后客户端就可以收到来自仓库的配置了

定点刷新

上面的形式都是配置的全局刷新,如果我们现在不想所有的客户端都配置刷新,如下请求

需求:刷新配置,只想3355配置生效,3366不生效

http://localhost:3344/actuator/bus-refresh/config-client:3355

达成目标

以上还是比较简单的


结束,还是比较简单的,主要还是实践比较重要点,偏理论

posted @ 2025-12-19 17:01  MRME39  阅读(0)  评论(0)    收藏  举报