• Spring Cloud 核心组件——注册中心


    1. 什么是微服务的注册中心

    注册中心:服务管理,核心是有个服务注册表,心跳机制动态维护。

    为什么要用?

    微服务应用和机器越来越多,调用方需要知道接口的网络地址,如果靠配置文件的方式去控制网络地址,对于动态新增机器,维护带来很大问题。

    主流的注册中心:Zookeeper、Eureka、Consul、ETCD 等。

    服务提供者 Provider:启动的时候向注册中心上报自己的网络信息。

    服务消费者 Consumer:启动的时候向注册中心上报自己的网络信息,拉取 Provider 的相关网络信息。

    2. 分布式应用知识CAP理论知识

    CAP定理:指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition Tolerance(分区容错性),三者不可同时获得。

    一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(所有节点在同一时间的数据完全一致,越多节点,数据同步越耗时)

    可用性(A):负载过大后,集群整体是否还能响应客户端的读写请求。(服务一直可用,而且是正常响应时间)

    分区容错性(P):分区容忍性,就是高可用性,一个节点崩了,并不影响其它的节点。(100个节点,挂了几个,不影响服务,越多机器越好)

    CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡。

    原因:

    CA 满足的情况下,P 不能满足的原因:数据同步(C)需要时间,也要正常的时间内响应(A),那么机器数量就要少,所以P就不满足。

    CP 满足的情况下,A 不能满足的原因:数据同步(C)需要时间,,机器数量也多(P),但是同步数据需要时间,所以不能再正常时间内响应,所以A就不满足。

    AP 满足的情况下,C不能满足的原因:机器数量也多(P),正常的时间内响应(A),那么数据就不能及时同步到其他节点,所以C不满足。

    注册中心选择:

    Zookeeper:CP 设计,保证了一致性,集群搭建的时候,某个节点失效,则会进行选举行的 leader,或者半数以上节点不可用,则无法提供服务,因此可用性没法满足。

    Eureka:AP 原则,无主从节点,一个节点挂了,自动切换其他节点可以使用,去中心化。

    结论:

    分布式系统中P,肯定要满足,所以只能在 C 和 A 中二选一。没有最好的选择,最好的选择是根据业务场景来进行架构设计,如果要求一致性,则选择 Zookeeper,如金融行业;如果要去可用性,则 Eureka,如电商系统。

    Eureka 原理图:

    Spring Cloud 体系官方地址:http://projects.spring.io/spring-cloud/

    参考:

    https://www.jianshu.com/p/d32ae141f680
    https://blog.csdn.net/zjcjava/article/details/78608892

    3. 使用 IDEA 搭建 Eureka 服务中心 Server 端并启动

    官方文档:http://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#spring-cloud-eureka-server

    第一步:创建项目

    和创建普通的 Spring Boot 项目是一样的,只是需要选择下图所示依赖

    第二步:添加注解 @EnableEurekaServer

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

    第三步:增加配置 application.yml(其实可以使用 application.properties,但官网上使用 yml,这里我们照抄官网)

    server:
      port: 8761
    
    eureka:
      instance:
        hostname: localhost
      client:
        registerWithEureka: false
        fetchRegistry: false
        serviceUrl:
          defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

    第四步:访问注册中心页面

    使用 http://localhost:8761/ 访问注册中心页面,这个地址按照配置文件中的来,这里注意下,按照配置文件说的,访问地址为:http://localhost:8761/eureka/

    但不同版本,可能不一样,我所使用的版本是 Greenwich,它就不能添加 /eureka/ 否则会报 404 错误

     

    Eureka 管理后台出现一串红色字体:EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

    这个是警告,说明有服务上线率低。

    关闭检查方法:Eureka 服务端配置文件加入

    server:
      enable-self-preservation: false

    注意:自我保护模式禁止关闭,默认是开启状态 true

    4. 创建商品服务,并将服务注册到注册中心

    第一步:创建一个 Spring Boot 应用,增加服务注册和发现依赖


    第二步:模拟商品信息,存储在内存中

    第三步:开发商品列表接口,商品详情接口

    第四步:配置文件加入注册中心地址

    server:
      port: 8771
    
    
    #指定注册中心地址
    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/
    
    #服务的名称
    spring:
      application:
        name: product-service

    我们可以用当前项目启动多个实例,参考教程:https://blog.csdn.net/zhou520yue520/article/details/81167841

    启动后,我们可以登录下访问注册中心页面,看到如下效果:

    为什么只加一个注册中心地址,就可以注册?

    官网解释:By having spring-cloud-starter-netflix-eureka-client on the classpath, your application automatically registers with the Eureka Server.(也就是说,这样 Jar 包在类路径上,就可以自动识别)

    5.常用的服务间的调用方式

    RPC:远程过程调用,像调用本地服务(方法)一样调用服务器的服务。支持同步、异步调用。客户端和服务器之间建立 TCP 连接,可以一次建立一个,也可以多个调用复用一次链接。PRC 数据包小。

    Rest:Http 请求,支持多种协议和功能。开发方便成本低。Http 数据包大。类似 HttpClient,URLConnection。

    6.订单服务调用商品服务获取商品信息

    第一步:创建 order_service 项目

    注意:调用方需要引入 Ribbon 依赖

    第二步:使用 Ribbon(类似 HTTPClient,URLConnection)

    启动类增加注解

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

    第三步:开发伪下单接口

    第四步:根据名称进行调用商品,获取商品详情

    注意:红字标识的就是上面商品服务的 spring.application.name

    @Service
    public class ProductOrderServiceImpl implements ProductOrderService {
    
    
        @Autowired
        private RestTemplate restTemplate;
    
        @Override
        public ProductOrder save(int userId, int productId) {
    
            Object obj = restTemplate.getForObject("http://product-service/api/v1/product/find?id="+productId, Object.class);
    
            System.out.println(obj);
    
            ProductOrder productOrder = new ProductOrder();
            productOrder.setCreateTime(new Date());
            productOrder.setUserId(userId);
            productOrder.setTradeNo(UUID.randomUUID().toString());
    
            return productOrder;
        }
    }

    当我们用请求多次访问时,可以从控制台看到端口号的不同,说明这是从不同端口的应用返回的数据

    商品服务的 Controller 如下:

    @RestController
    @RequestMapping("/api/v1/product")
    public class ProductController {
    
        @Value("${server.port}")
        private String port;
    
        @Autowired
        private ProductService productService;
    
        /**
         * 获取所有商品列表
         * @return
         */
        @RequestMapping("list")
        public Object list(){
            return productService.listProduct();
        }
    
        /**
         * 根据id查找商品详情
         * @param id
         * @return
         */
        @RequestMapping("find")
        public Object findById(int id){
    
            Product product = productService.findById(id);
    
            Product result = new Product();
            BeanUtils.copyProperties(product,result);
            result.setName( result.getName() + " data from port="+port );
            return result;
        }
    }

    我们还可以通过另一种调用方式进行调用

    //调用方式二
    ServiceInstance instance = loadBalancer.choose("product-service");
    String url = String.format("http://%s:%s/api/v1/product/find?id="+productId, instance.getHost(),instance.getPort());
    RestTemplate restTemplate = new RestTemplate();
    Object obj = restTemplate.getForObject(url, Object.class);
    //Map<String,Object> productMap = restTemplate.getForObject(url, Map.class);

    调用原理:

    1)首先从注册中心获取 Provider 的列表

    2)通过一定的策略选择其中一个节点

    3)再返回给 restTemplate 调用

    我们也可以自定义负载均衡策略,官网说明:https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.2.0.M3/reference/html/#customizing-the-ribbon-client-by-setting-properties

    server:
      port: 8781
    
    
    #指定注册中心地址
    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/
    
    #服务的名称
    spring:
      application:
        name: order-service
    
    #自定义负载均衡策略
    product-service:
      ribbon:
        NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

    策略选择:

    1)如果每个机器配置一样,则建议不修改策略 (推荐)

    2)如果部分机器配置强,则可以改为 WeightedResponseTimeRule

    7.使用 Feign 改造订单服务 

    Feign: 伪 RPC 客户端(本质还是用http)

    官方文档: https://cloud.spring.io/spring-cloud-openfeign/

    第一步:加入依赖

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

    第二步:启动类增加 @EnableFeignClients

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

    第三步:增加一个接口并使用注解 @FeignClient(name="product-service")

    package com.jwen.order_service.service;
    
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    /**
     * 商品服务客户端
     */
    @FeignClient(name = "product-service")
    public interface ProductClient {
        @GetMapping("/api/v1/product/find")
        String findById(@RequestParam(value = "id") int id);
    }

    注意点:

    1)服务名和 Http 方法必须对应

    2)使用 RequestBody,应该使用 @PostMapping

    3)多个参数的时候,通过 @RequestParam(value = "id") int id 方式调用

    第四步:更改调用方式编码

    package com.jwen.order_service.service.impl;
    
    import com.fasterxml.jackson.databind.JsonNode;
    import com.jwen.order_service.domain.ProductOrder;
    import com.jwen.order_service.service.ProductClient;
    import com.jwen.order_service.service.ProductOrderService;
    import com.jwen.order_service.utils.JsonUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.Date;
    import java.util.UUID;
    
    @Service
    public class ProductOrderServiceImpl implements ProductOrderService {
        @Autowired
        private ProductClient productClient;
    
        @Override
        public ProductOrder save(int userId, int productId) {
            String response = productClient.findById(productId);
            JsonNode jsonNode = JsonUtils.str2JsonNode(response);
    
            ProductOrder productOrder = new ProductOrder();
            productOrder.setCreateTime(new Date());
            productOrder.setUserId(userId);
            productOrder.setTradeNo(UUID.randomUUID().toString());
            productOrder.setProductName(jsonNode.get("name").toString());
            productOrder.setPrice(Integer.parseInt(jsonNode.get("price").toString()));
            return productOrder;
        }
    }
    JsonUtils 工具类的代码
    package com.jwen.order_service.utils;
    
    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import java.io.IOException;
    
    /**
     * json工具类
     */
    public class JsonUtils {
        private static final ObjectMapper objectMappper = new ObjectMapper();
        /**
         * json字符串转JsonNode对象的方法
         */
        public static JsonNode str2JsonNode(String str){
            try {
                return  objectMappper.readTree(str);
            } catch (IOException e) {
                return null;
            }
        }
    }

    Ribbon 和 Feign 两个之间,应该选择 Feign。Feign 默认集成了 Ribbon。写起来更加思路清晰和方便。采用注解方式进行配置,配置熔断等方式方便

    超时配置

    #修改调用超时时间
    feign:
      client:
        config:
          default:
            connectTimeout: 5000
            readTimeout: 5000

    模拟接口响应慢,线程睡眠新的方式

    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
  • 相关阅读:
    express,中间件(body-parser),req.body获取不到参数(含postman发请求的方法)
    echarts+百度地图+vue 填坑记(一)(百度地图、鼠标移入移出标注,信息框会产生闪烁)
    echarts tooltip提示框 自定义小圆点(颜色、形状和大小等等)
    百度2019校招Web前端工程师笔试卷(9月14日)
    python基础学习
    使用javascript模拟常见数据结构(四)
    使用javascript模拟常见数据结构(三)
    使用javascript模拟常见数据结构(二)
    mongodb的安装与增删改查
    使用javascript模拟常见数据结构(一)
  • 原文地址:https://www.cnblogs.com/jwen1994/p/11408511.html
Copyright © 2020-2023  润新知