• SPRING CLOUD微服务DEMO-上篇


    1. 微服务架构

    系统架构的演变从单应用,到根据每个模块垂直拆分,到分布式服务,SOA,到目前进化成了微服务形态。

    微服务的特点

    • 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
    • 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
    • 面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
    • 自治:自治是说服务间互相独立,互不干扰
      • 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
      • 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
      • 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口
      • 数据库分离:每个服务都使用自己的数据源
      • 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护

    2. 远程调用方式

    2.1 RPC/RMI

    RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型

    2.2 Http

    Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议。也可以用来进行远程服务调用。缺点是消息封装臃肿。

    2.3 如何选择

    既然两种方式都可以实现远程调用,我们该如何选择呢?

    • 速度来看,RPC要比http更快,虽然底层都是TCP,但是http协议的信息往往比较臃肿,不过可以采用gzip压缩。
    • 难度来看,RPC实现较为复杂,http相对比较简单
    • 灵活性来看,http更胜一筹,因为它不关心实现细节,跨平台、跨语言。RPC方式需要在API层面进行封装,限制了开发的语言环境。

    因此,两者都有不同的使用场景:

    • 如果对效率要求更高,并且开发过程使用统一的技术栈,那么用RPC还是不错的。
    • 如果需要更加灵活,跨语言、跨平台,显然http更合适

    微服务,更加强调的是独立、自治、灵活。而RPC方式的限制较多,因此微服务框架中,一般都会采用基于Http的Rest风格服务。

    3. Http客户端工具

    可以使用一些流行的开源Http客户端工具请求Rest接口

    • HttpClient
    • OKHttp
    • URLConnection

    3.1 RestTemplate

    使用Http客户端工具请求Rest接口,得到数据后反序列化成对象。这样做比较麻烦,Spring提供了一个RestTemplate模版工具类,对Http客户端进行了封装(默认使用URLConnection),实现了对象和Json的序列化和反序列化,方便得很。下面搭建一个Spring Boot工程,同时演示RestTemplate如何使用。

    4. Spring Boot 搭建项目

    微服务的一个重要特点是每个服务都是一个可以独立运行的项目,要是按照以前的SSM的搭建方式,无法做到服务的快速搭建和部署。于是Spring Boot应运而生,它的理念就是约定大于配置,你选好模块,自动帮你搭建好环境,非常便捷。

    接下来用Spring Boot搭建两个简单的用户微服务:user-serviceuser-consume,实现的功能是:user-consume使用RestTemplate调用user-service服务

    具体参考这篇笔记(子文章)SPRING BOOT搭建两个微服务模块

    5. Spring Cloud简介

    Spring Cloud是实现微服务架构的技术。它集成了Netflix公司的一些微服务组件。(如果你不知道这家公司,那你肯定不爱看美剧。)

    Netflix微服务架构图

    • Eureka:注册中心
    • Zuul:服务网关
    • Ribbon:负载均衡
    • Feign:服务调用
    • Hystix:熔断器

    6. 微服务场景模拟

    在第四节我们已经搭建好了user-serviceuser-consume两个微服务。

    consume使用RestTemplate调用service提供的rest接口,获得json数据,反序列化成User对象返回到前台。其中存在着一些问题:

    • consume中调用的rest接口的地址是硬编码的,不方便维护。
    • 如果service的rest接口变更或关闭了,consume并不知情,最终什么都得不到。
    • service只有1台,一旦宕机,整个服务就不可用了。如果扩展多台,那consume又要自己考虑负载均衡。

    接下来介绍的几个组件就是为解决这些问题而生的。

    7. Eureka注册中心

    7.1 简介

    Eureka负责微服务的管理。

    如果一个项目有数十个微服务,调用者想要自己找到一个适合的,可用的微服务是很麻烦的一件事。

    就比如你想要坐车出门,自己上街拦出租车就很麻烦,要么司机拒载,要么车里已经有人了,要么干脆就没有车来...后来就出现了滴滴,做为一个网约车的“注册中心”,可以为你分配离你最近的空闲出租车。

    Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。

    同时,服务提供方与Eureka之间通过“心跳”机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。

    这就实现了服务的自动注册、发现、状态监控。

    7.2 原理图

    • EurekaServer:注册中心,可以是多个Eureka的集群,对外暴露自己的地址。
    • 服务提供者:启动后在注册中心中注册,成功后定期使用http方法向注册中心发送心跳包,表明自己还活着。
    • 客户端消费者:向注册中心订阅服务。注册中心会向消费者发送合适的服务提供者列表,并且定期更新。需要使用某个服务时,可以从列表中找到并调用。

    7.3 搭建注册中心

    搭建过程和第4节说的一样,注意选择Eureka模块即可。

    7.3.1 代码和配置文件

    • 启动类

    @EnableEurekaServer注解表示这是一个注册中心

    @EnableEurekaServer
    @SpringBootApplication
    public class EurekaApplication {
        public static void main(String[] args) {
            SpringApplication.run(EurekaApplication.class, args);
        }
    }
    
    • 配置文件

    现在我们只启动一个eureka作为注册中心。

    这是Spring官方文档中的Standalone Eureka Server的建议配置。

    注意eureka下的client配置,这个配置意思是eureka应用作为一个客户端,对注册中心的动作。

    其中service-url的配置必须要填写,内容是注册中心的地址,如果有多个,逗号隔开。

    defaultZone路径后面必须加上/eureka后缀,别问我为啥。

    server:
      port: 10086 # 端口
    spring:
      application:
        name: eureka-server # 应用名称,会在Eureka中显示
    eureka:
      client:
        register-with-eureka: false # 是否注册自己的信息到EurekaServer,默认是true
        fetch-registry: false # 是否拉取服务列表,默认是true
        service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
          defaultZone: http://127.0.0.1:${server.port}/eureka
    

    访问http://localhost:10086即可看到注册中心的内容,eureka只有一个,服务列表为空:

    7.4 将user-service注册到eureka

    7.4.1 添加依赖

    eureka客户端依赖

    <!-- Eureka客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    

    Spring Cloud的依赖,注意我这里的版本是Greenwich.SR1

        <!-- SpringCloud的依赖 -->
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>Greenwich.SR1</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
        <!-- Spring的仓库地址 -->
        <repositories>
            <repository>
                <id>spring-milestones</id>
                <name>Spring Milestones</name>
                <url>https://repo.spring.io/milestone</url>
                <snapshots>
                    <enabled>false</enabled>
                </snapshots>
            </repository>
        </repositories>
    

    7.4.2 开启EurekaClient功能

    添加@EnableDiscoveryClient注解

    @SpringBootApplication
    @EnableDiscoveryClient // 开启EurekaClient功能
    public class UserServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(UserServiceApplication.class, args);
        }
    }
    

    7.4.3 配置eureka客户端属性

    server:
      port: 8081
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/XXXXX?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
        username: XXXXX
        password: XXXXX
        hikari:
          maximum-pool-size: 20
          minimum-idle: 10
      application:
        name: user-service # 应用名称
    mybatis:
      type-aliases-package: com.vplus.demo.userservice.pojo
    eureka:
      client:
        service-url: # EurekaServer地址
          defaultZone: http://127.0.0.1:10086/eureka
      instance:
        prefer-ip-address: true # 当调用getHostname获取实例的hostname时,返回ip而不是host名称
        ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找
    

    7.4.4 效果

    7.5 user-consume从eureka中获取服务

    7.5.1 添加依赖

    同上7.4.1

    7.5.2 开启EurekaClient功能

    同上7.4.2

    7.5.3 配置eureka客户端属性

    server:
      port: 8082
    spring:
      application:
        name: user-consume # 应用名称
    eureka:
      client:
        service-url: # EurekaServer地址
          defaultZone: http://127.0.0.1:10086/eureka
      instance:
        prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
        ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找
    

    7.5.4 修改UserService

    • 之前是调用UserDao,UserDao使用RestTemplate请求远程接口得到数据。
    • 现在我们在service层,从eureka中拉取服务列表,得到接口地址后请求数据。
    @Service
    public class UserService {
        @Autowired
        private RestTemplate restTemplate;
    
        @Autowired
        private DiscoveryClient discoveryClient;// Eureka客户端,可以获取到服务实例信息
    
        public List<User> queryUserByIds(List<Long> ids) {
            List<User> users = new ArrayList<>();
            // String baseUrl = "http://localhost:8081/user/";
            // 根据服务名称,获取服务实例
            List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
            // 因为只有一个UserService,因此我们直接get(0)获取
            ServiceInstance instance = instances.get(0);
            // 获取ip和端口信息
            String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/";
            ids.forEach(id -> {
                // 我们测试多次查询,
                users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
                // 每次间隔500毫秒
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            return users;
        }
    
    }
    

    7.6 eureka集群

    7.6.1 搭建集群

    Eureka可以集群搭建形成高可用的注册中心。多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。

    我们要两个Eureka,端口号分别是10086、10087

    可以使用Idea的启动器复制功能复制eureka的启动器。

    将应用A的启动器复制出一个A2,A启动后,修改配置再启动A2,这样我们就可以得到两个配置不同的应用了。

    先把原来的eureka配置信息改为

    server:
      port: 10086 # 端口
    spring:
      application:
        name: eureka-server # 应用名称,会在Eureka中显示
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:10087/eureka
    

    启动EurekaApplication,启动起来后再将配置改为

    server:
      port: 10087 # 端口
    spring:
      application:
        name: eureka-server # 应用名称,会在Eureka中显示
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:10086/eureka
    

    再启动EurekaApplication2,此时两个注册中心就形成了集群。

    两个eureka相互注册,10086的defaultZone的url地址的端口号为10087,注意这一点。

    再将两个客户端的Eureka相关配置改为

    defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
    

    访问http://localhost:10086http://localhost:10087都可以看到效果:

    7.7 Eureka相关配置

    7.7.1 服务提供者

    服务提供者主要做两个动作:服务注册服务续约

    • 服务注册的对应配置,true表示服务启动后会向注册中心发送注册请求,默认就是开着的。
    eureka
    	client
    		register-with-erueka: true
    
    • 服务续约的对应配置,这些都是默认值
    eureka:
      instance:
        #服务失效时间,默认值90秒
        lease-expiration-duration-in-seconds: 90
        #服务续约(renew)的间隔,默认为30秒
        lease-renewal-interval-in-seconds: 30
    

    默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,注意,这是服务提供者告诉注册中心我每30秒续约一次,90秒没有续约,就表示我失效了,是配置在服务提供者上的,不是配置在注册中心上的。

    我实验的时候,关闭一个服务,eureka几乎是瞬间就知道服务down了

    试了好几次都是这样,可能我的关闭动作出发了什么东西吧,时间有限,先不管这个问题,留个坑将来看。

    后来我注意到服务关闭后会输出一句:Unregistering ...,推测服务正常关闭时会自己通知注册中心。

    • 实例ID名的修改

    在页面上,服务提供者实例ID显示为:localhost:user-service:8081,

    格式为:${hostname} + ${spring.application.name} + ${server.port},对应配置为

    eureka:
      instance:
        instance-id: ${spring.application.name}:${server.port}
    

    修改后启动,变成了

    7.7.2 服务消费者

    消费者需要拉取服务列表,拉取时间间隔默认为30秒1次,对应配置为,如果是开发环境可适当缩小方便开发

    eureka:
      client:
        registry-fetch-interval-seconds: 30
    

    7.7.3 失效剔除和自我保护

    • 失效剔除:每个一段时间剔除掉失效的服务,默认为60s,为了方便开发设置为1s。

      服务器怎么知道失效了?看看上面服务提供者的配置,注意这几个配置的联系。

    • 自我保护:生产环境中,由于网络延迟等原因,失效服务并不一定是真正失效了。如果被标记为失效的服务太多,超过了85%,此时eureka会把这些服务保护起来,先不剔除,保证大多数服务还可用。在开发中将自我保护模式关掉,方便开发。

    eureka:
      server:
        enable-self-preservation: false # 关闭自我保护模式(默认为打开)
        eviction-interval-timer-in-ms: 1000 # 	扫描失效服务的间隔时间为1s(默认为60s)
    

    8. Robbin负载均衡

    8.1 开启两个user-service

    具体操作参照7.6.1,这里设置两个service的端口为8080和8081

    8.2 开启负载均衡

    注意,是在调用端即consume上开启负载均衡,Eureka中已经集成了Ribbon,无需引入新的依赖,只需要在调用端的RestTemplate的注册Bean方法上添加注解:@LoadBalanced即可。这个方法位于UserConsumerApplication里。

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

    具体的调用方式也需要修改,将UserService中的queryUserByIds方法修改成这样。

    public List<User> queryUserByIds(List<Long> ids) {
           List<User> users = new ArrayList<>();
            // 地址直接写服务名称即可
            String baseUrl = "http://user-service/user/";
            ids.forEach(id -> {
                // 我们测试多次查询,
                users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
                // 每次间隔500毫秒
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            return users;
        }
    }
    

    8.3 默认负载均衡策略分析

    org.springframework.cloud.client.loadbalancer包里面有一个LoadBalancerInterceptor,它就是实现负载均衡的拦截器。

    跟踪源码,找到了RibbonLoadBalancerClient,用consume多次请求接口,断点调试

    execute(String serviceId, LoadBalancerRequest<T> request, Object hint)方法:

    就是轮询

    8.4 修改负载均衡策略

    一个配置即可修改,有多种配置规则,这里使用随机规则。

    注意格式,是以服务名称开头的。

    server:
      port: 8082
    spring:
      application:
        name: user-consume # 应用名称
    eureka:
      client:
        service-url: # EurekaServer地址
          defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
        registry-fetch-interval-seconds: 5
      instance:
        prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
        ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找
    ###################################负载均衡配置###############################################
    user-service:
      ribbon:
        NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    

    8.5 重试机制

    如果有一台服务突然挂掉了,而eureka还来不及将其清除出服务列表,或者消费者拉取的服务列表还有缓存,一旦请求到这台挂掉的服务就会报错。虽然多次请求后结果也能出来,但体验非常不好。

    Ribbon的重试机制就是解决这个问题的。

    引入依赖

    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
    </dependency>
    

    开启Spring Cloud的重试机制并配置

    server:
      port: 8082
    spring:
      application:
        name: user-consume # 应用名称
      cloud:
        loadbalancer:
          retry:
            enabled: true # 开启Spring Cloud的重试功能
    eureka:
      client:
        service-url: # EurekaServer地址
          defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
        registry-fetch-interval-seconds: 5
      instance:
        prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
        ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找
    
    user-service:
      ribbon:
        ConnectTimeout: 250 # Ribbon的连接超时时间
        ReadTimeout: 1000 # Ribbon的数据读取超时时间
        OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
        MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
        MaxAutoRetries: 1 # 对当前实例的重试次数
    

    就好了。

  • 相关阅读:
    第一章 重构
    Android View的事件分发
    java.lang.NoSuchMethodError: android.view.View.setBackground
    handler消息机制
    魅族手机Listview下拉出现hold字样的奇葩问题解决方案
    数据结构--树,二叉树
    数据结构之栈和队列
    设计模式--六大原则
    ListView上下线添加
    Python 入门(七)函数
  • 原文地址:https://www.cnblogs.com/qianbixin/p/10936456.html
Copyright © 2020-2023  润新知