SpringCloud

SpringCloud解决的四大难题

  • 模块拆分后服务变得很多,客户端怎么访问?

  • 服务这么多,服务之间怎么通信?

  • 这么多服务,怎么治理?

  • 服务挂了怎么办?

    image-20230707163256405

组件(解决方案)

Eureka(注册与发现中心)

作用

  • 概念

    • 服务端:注册中心,专门用于存储客户端服务列表和管理客户端的一个服务器,一般称为服务端
    • 客户端:专门服务于用户的服务器,里面存储的是各个请求接口用于返回数据。这个名字容易被混淆,被误认为用户端
  • 存放和调度服务,实现服务和注册中心,服务和服务之间的相互通信

手写Eureka思路

image-20230630120200554
  1. 在注册中心中需要一个服务列表(容器)保存应用(各个服务)信息
  2. 应用下线了或者挂了服务列表需要整理
    • 主动下线:服务自己关掉链接
    • 被动下线:由于服务长时间没有与注册中心交互(心跳机制:每次心跳就是一次请求),被注册中心剔除了
  3. 服务与服务之间的通信模式
    • 当服务A需要访问服务B时,正常流程时服务A先到注册中心拿到服务B的相关信息,再访问服务B。
    • 为了解决这种模式,让服务每隔一段时间从注册中心拉取服务列表并缓存到本次,从而实现快速查到其它服务的IP、端口。但容易出现脏读
  4. 怎么解决(缓解)脏读问题?
    • 缓解脏读问题,拉取时间间隔越少脏读越少,随之性能消耗就更大

Eureka集群

集群作用

  • 当其中一台注册中心服务器挂掉之后,不影响使用

集群的实现

  • 就是开多台机子(设置相同的应用名称:${spring.application.name}),使用不同的端口

集群的实现方式

  • ​ 主从模式
    • 从多台服务器中选一台主服务器实现读写分离和数据同步
  • 哨兵模式
    • 主从模式的升级版,当主服务器挂了(没有向从服务器发出PING命令),则所有从服务器开启投票,即所有服务器进行初始化,先初始化的从机则先开启投票,也就成为了新的主机
  • 去中心化模式

Eureka源码分析

  • 服务注册

    • 当客户端向服务端发送注册请求时,将客户端的配置文件数据封装到instanceInfo中,并封装请求发送到服务端(根据服务端serviceUrl找到)

      boolean register() throws Throwable {
              logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
              EurekaHttpResponse<Void> httpResponse;
              try {
                  httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
              } catch (Exception e) {
         
                  
      public EurekaHttpResponse<Void> register(InstanceInfo info) {
          String urlPath = "apps/" + info.getAppName();
          ClientResponse response = null;
          try {
              Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
              addExtraHeaders(resourceBuilder);
              response = resourceBuilder
                      .header("Accept-Encoding", "gzip")
                      .type(MediaType.APPLICATION_JSON_TYPE)
                      .accept(MediaType.APPLICATION_JSON)
                      .post(ClientResponse.class, info);
              return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
          } finally {
      
    • 将客户端配置文件数据封装到map

      public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
          this.read.lock();
      
          try {
              Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());
              EurekaMonitors.REGISTER.increment(isReplication);
              if (gMap == null) {
                  ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap();
                  gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap);
                  if (gMap == null) {
                      gMap = gNewMap;
                  }
              }
      
      
  • 服务续约

  • 服务下线

    • 主动下线
    • 被动下线

服务发现

  • 解释:各个客户端如何实现通信

  • 实现:通过服务的应用名称找到服务的具体实例

        @Autowired
        DiscoveryClient discoveryClient;
    
        @GetMapping("/test")
        public String doDiscovery(String serviceName){
    //        服务发现:通过服务的具体名称找到服务的具体信息
            List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
            instances.forEach(System.out::println);
            return instances.get(0).toString();
    

RestTemplate

  • 作用:实现使用Java代码发送请求或请求一个页面

        void contextLoads() {
    //        在java中发送一个请求 请求一个页面
            RestTemplate restTemplate = new RestTemplate();
            String url="https://www.baidu.com";
            String forObject = restTemplate.getForObject(url, String.class);
            System.out.println(forObject);
    

Ribbon(负载均衡)

  • 简介:ribbon只是用于负载均衡,但不能直接链接到各个客户端,因此需要集成一个发送请求的工具RestTemplate用于找到对应的客户端从而进行负载均衡

Feign(远程调用)

  • 简介:该组件可以对客户端进行远程调用,也可以说代替了RestTemplate。同时feign集成了ribbon,所以feign主要用于远程调用客户端并对客户端进行负载均衡

  • 调用

    • 本地调用:用一些操作系统内核函数时,虽然不是同一个进程中,但也属于同一个机器上
    • 远程调用:调用双方并不在同一个机器上
  • 注意点

    • ribbon默认的最大调用等待时间时1秒,如果需要修改,需要通过修改ribbon配置文件
  • 手写feign

    • 思路:使用JDK动态代理实现,增强嘛,在方法之前找到对应的客户端进行增强

      • 调用条件:获取到客户端的IP,接口路径,端口
      • ribbon:组成"http://"+注册中心的应用名称+ 接口路径即可。ribbo通过应用名称找到该客户点的配置文件信息并封装到路径中
    • 通过@FeignClient("")注解找到对应客户端主机名

    • 通过@GetMapping("")注解找到对应路径

    • 封装url并发送请求到指定客户端并返回

    • //    手写feign  :jdk动态代理
          @Autowired
          RestTemplate restTemplate;
      
          @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、端口  组成"http://"+注册中心的应用名称+ 接口路径,String.class
      //                注册中心的应用名称:@FeignClient("order-service")
      //                接口路径:@GetMapping("/order")
      
      //              拿名称
                      Class<?> aClass = method.getDeclaringClass();
                      FeignClient feignClient = aClass.getAnnotation(FeignClient.class);
                      String appName = feignClient.value();
      //              拿路径
                      GetMapping annotation = method.getAnnotation(GetMapping.class);
                      String[] paths = annotation.value();
                      String path = paths[0];
      //              组路径
                      String url="http://" + appName + "/" + path;
      //              发送
                      String forObject = restTemplate.getForObject(url, String.class);
                      return forObject;
                  }
              });
      
              System.out.println(o.order());
          }
      
  • feign的参数处理

    • 构建者模式:在实体类中加@Builder注解,可以不用再使用set方法值给属性赋值

    • @Data
      @AllArgsConstructor
      @NoArgsConstructor
      @Builder
      public class DoMin {
          private int id;
          private String name;
          private int age;
      }
      
      class test{
          public static void main(String[] args) {
      
              DoMin liny = DoMin.builder()
                      .id(1)
                      .name("liny")
                      .age(19)
                      .build();
              
              System.out.println(liny);
          }
      }
      
    • 注意点

      1. 每个参数都需要加上对应注解,如果不加会报错。

      2. 不要直接传时间参数,会有误差,一般转为字符串传参

        String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        
  • feign源码分析

  • feign的日志处理

    1. feign自带日志,需要手动开启

      1. ​ 选择对应的打印级别

        public enum Level {
          /**
           * No logging.
           */
          NONE,
          /**
           * Log only the request method and URL and the response status code and execution time.
           */
          BASIC,
          /**
           * Log the basic information along with request and response headers.
           */
          HEADERS,
          /**
           * Log the headers, body, and metadata for both requests and responses.
           */
          FULL
        }
        
      2. 放入IOC容器中

        @Bean
        public Logger.Level level(){
            return  Logger.Level.FULL;
        }
        
      3. 配置yml文件设置日志级别

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

Hystrix(熔断器)

  • 服务雪崩
    • 服务调用之间是单线程吗?
    • 当服务A调用服务B,服务B会分配一个线程支持服务A的调用,服务B发现需要完成服务A的操作需要去调用服务C,但是服务C宕机了,服务B不知道,服务B就只能等待超时后再返回数据。
    • 引起服务雪崩:服务B进行等待超时这段时间服务A和服务B的线程都被占用,如果现在有很多请求需要调用A或B,就会开始排队,当排队请求过量后,服务就会出现故障。
  • 简介
    • 当上一级服务发现下一级服务宕机后进行熔断操作
    • 熔断操作
      • 发现被调用服务宕机后不进行等待超时之间返回
      • 还可以设置备用方案
  • 手写熔断器思路

Feign的工程化案例‘

  • 实际公司项目结构

链路追踪

admin监控

  • 监控的本质:心跳检测、打乒乓球
  • admin分为服务端与客户端。只要服务端可以获取所有客户端的ip和端口号,就可以向客户端发送检测信号。所以之间把admin服务端往注册中心注册,就可以拉取所有客户端的ip和端口号
posted @ 2023-08-21 11:22  Liny5469  阅读(18)  评论(0)    收藏  举报