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();
}

 

posted @ 2019-09-15 23:42  认真对待世界的小白  阅读(2918)  评论(0编辑  收藏  举报