Spring Cloud入门到实战

/

1. 引入Spring Cloud

1.1 Spring Cloud简介

Spring Cloud是一系列框架的有序集合。它利用了Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,为开发人员提供了快速构建分布式系统的一些工具,包括配置管理,服务发现注册,配置中心,消息总线,负载均衡,断路由,数据监控等,都可以利用Spring Boot的开发风格做到一键启动和部署。
Spring Cloud没有重复造轮子,它只是将各个公司开发的,比较成熟的,经得起考验的框架组合起来,通过Spring Boot风格进行再封装屏蔽掉复杂的配置和实现原理,最终给开发者留出一套简单易懂,易部署和易维护的分布式系统开发工具。

1.2 单体应用开发的弊端

看一张传统的单体应用项目的开发模块图:

单体应用开发存在的问题:

  • 随着业务的发展,开发变得越来越复杂。
  • 修改,新增某个功能,需要对整个系统进行重新测试,重新部署。
  • 一个模块出现问题,可能导致整个系统崩溃。
  • 多个开发团队同时对数据进行管理,容易产生系统安全漏洞。
  • 各个模块使用同一技术进行开发,各个模块很难根据实际情况选择更合适的技术框架,局限性很大。
  • 模块内容过于复杂如果员工离职,可能需要很长的时间才能完成工作的对接。

1.3 分布式、集群的概念

这两个名词其实在很多的地方都有遇到过,但是它们指的是同一个吗?答案并不是!!

集群:一台物理机无法负荷高并发的访问量,那么就设置10台一起分担压力,10台不行就100台(物理层面),相当于很多人干同一件事情,来彼此分担压力。

分布式:将一个复杂的问题拆分成若干个简单的小问题,在项目中则是将一个复杂的大型项目架构拆分成若干个微服务来协同完成(软件设计层面),将一个庞大的工作拆分成若干个小的步骤,分别由不同的人来完成这些小步骤,最终将所有的结果进行整合实现大的需求!

1.4 Spring Cloud的出现

分布式开发的出现解决了单体应用开发的弊端,引入了Spring Cloud,定义了微服务开发的架构

不同的团队通过API网关映射到不同的微服务进行各自的开发,中间服务调用的方式借助的是REST API

1.5 为什么是Spring Cloud

  • Spring Cloud完全基于Spring Boot,服务调用方式基于REST API整合可各种成熟的产品和框架,如Netflix的eureka服务,同时基于Spring Boot也使整体的开发,配置,部署非常方便。
  • Spring 系得产品功能齐全,简单好用,性能优越,文档规范,因此Spring Cloud还是微服务架构中非常优越得一种解决方案!

对比其它微服务架构:

2. Eureka(服务治理)

服务治理的核心由三部分组成,分别是服务消费者,服务提供者,注册中心。

在分布式系统架构中,每个微服务启动时,将自己的信息存储在注册中心,叫做服务注册。

服务消费者从注册中心获取到每个服务提供者的网络信息,通过该信息调用服务的过程叫做服务发现。

举个例子:
注册中心:美团外卖
服务提供者:美团商家
服务消费者:订餐客户

服务注册:相当于商家去美团上面开了家店面
服务发现:订餐客户去美团外面上面选择订餐的店

Spring Cloud的服务治理是使用Eureka来实现的,Eureka是Netflix开源的,基于REST的服务治理解决方案,Spring Cloud集成了Eureka,提供服务注册和服务发现功能,可以基于Spring Boot搭建的微服务应用轻松完成整合,开箱即用。

Spring Cloud Eureka

  • Eureka Server:注册中心
  • Eureka Client:所有要进行注册的微服务都要通过Eureka Client连接到Eureka Server,完成注册。

2.1 实战:搭建注册中心

  1. 新建父子Maven工程,不选择任何骨架创建

  2. 父工程添加依赖

    <parent>
    		<!--父工程为Spring Boot项目-->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.0.7.RELEASE</version>
        </parent>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
        </dependencies>
    
    <!--dependencyManagement用于管理多模块开发的依赖关系-->
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>Finchley.SR2</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
  3. 子工程eureka-server添加依赖

    <!--这是一个注册中心-->
    <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
                <version>2.0.2.RELEASE</version>
            </dependency>
    </dependencies>
    
  4. 新建子工程eureka-server配置文件

    server:
      port: 8761
    eureka:
      client:
        fetch-registry: false
        register-with-eureka: false
        service-url:
          defaultZone: http://localhost:8761/eureka/
    

    属性说明:

    register-with-eureka:是否将当前的工程作为客户端进行注册,如果将注册中心注册,那就没意义的,所以默认为false
    fetch-registry:是否获取其它的Eureka Server的服务数据
    service-url.defaultZone:注册中心的访问地址

  5. 新建注册中心启动类

    @SpringBootApplication    //spring boot启动类
    @EnableEurekaServer		//开启Eureka Server注册中心
    public class EurekaServerApplication { 
    
        public static void main(String[] args) { 
            SpringApplication.run(EurekaServerApplication.class,args);
        }
    }
    
  6. 启动子工程eureka-server,访问http://localhost:8761,如果出现以下画面表示注册中心搭建成功!

    小结

    1. 搭建一个基础的Maven项目,引入Spring Boot起步依赖及相关依赖
    2. 搭建注册中心子工程,引入注册中心依赖
    3. 编写注册中心配置文件
    4. 新建注册中心启动类并且启动
    

2.2 实战:注册一个微服务提供者

新建一个服务提供者,实现了对学生的增删改查,将服务注册到注册中心,都需要借助于Eureka-Client连接到Eureka Server,完成注册!

  1. 新建一个服务Module,注册到注册中心

  2. 导入POM依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>
    </dependencies>
    
  3. 编写配置文件

    server:
      port: 8010
    spring:
      application:
        name: provider
    eureka:
      client:
        service-url:
          defaultZonoe: http://localhost:8761/eureka/
      instance:
        prefer-ip-address: true
    

    属性说明:

    spring.application.name:微服务注册到eureka-server的名称
    eureka.client.service-url.defaultZone:注册中心的访问地址
    eureka.instance.prefer-ip-address:是否将当前的服务ip注册到eureka-server

  4. 编写实体类

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Student { 
        private Long id;
        private String name;
        private Integer age;
    }
    
  5. 编写repository

    public interface StudentRepository { 
        public Collection<Student> findAll();
    
        public void  saveOrUpdate(Student student);
    
        public void deleteById(Long id);
    
        public Student findById(Long id);
    }
    
    
    
    @Repository
    public class StudentRepositoryImpl implements StudentRepository{ 
    
        private static  Map<Long, Student> studentMap;
        static { 
            studentMap = new HashMap<>();
            studentMap.put(1L, new Student(1L, "张三", 34));
            studentMap.put(2L, new Student(2L, "李四", 31));
            studentMap.put(3L, new Student(3L, "王五", 23));
        }
    
        @Override
        public Collection<Student> findAll() { 
            return studentMap.values();
        }
    
        @Override
        public void saveOrUpdate(Student student) { 
            studentMap.put(student.getId(),student);
        }
    
        @Override
        public void deleteById(Long id) { 
            studentMap.remove(id);
        }
    
        @Override
        public Student findById(Long id) { 
            return studentMap.get(id);
        }
    }
    
    
  6. 编写web层

    @RequestMapping("student")
    @Controller
    public class StudentController { 
    
        @Autowired
        private StudentRepository repository;
    
        @GetMapping("findAll")
        @ResponseBody
        public Collection<Student> findAll() { 
            return repository.findAll();
        }
    
        @PostMapping("save")
        public String save(@RequestBody Student student) { 
            repository.saveOrUpdate(student);
            return "redirect:findAll";
        }
    
        @PutMapping("update")
        public String update(@RequestBody Student student) { 
            repository.saveOrUpdate(student);
            return "redirect:findAll";
        }
    
        @DeleteMapping("deleteById/{id}")
        public String deleteById(@PathVariable("id") Long id) { 
            repository.deleteById(id);
            return "redirect:findAll";
        }
    
        @GetMapping("findById/{id}")
        @ResponseBody
        public Student findById(@PathVariable("id") Long id) { 
            return repository.findById(id);
        }
    }
    
    
  7. 使用POSTMAN测试接口

    小结

    1. 新建模块eureka-client,用于连接eureka-server
    2. 导入依赖
    3.编写配置文件application.yml
    4. 编写相关业务代码
    5.postman测试 
    

2.3 实战:RestTemplate的使用

服务消费者的实现实现思路是基于Spring Boot搭建的一个微服务应用,再通过Eureka-Client把它注册到注册中心Eureka-Server,成为一个服务消费者,那么服务消费者是如何调用服务提供者的接口呢?这个需要用到组件RestTemplate!

什么是RestTemplate
RestTemplate是Spring框架基于REST的服务组件,底层是对HTTP的请求和响应进行了封装,提供了很多访问REST服务的接口,可以简化开发!

如何使用呢RestTemplate?

  1. 新建一个Module resttemplate,不引入相关依赖,直接使用maven工程

  2. 同样需要Student 实体类

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Student { 
        private Long id;
        private String name;
        private Integer age;
    }
    
  3. web处理器去调用服务提供者的REST接口

    @Controller
    @RequestMapping("rest")
    public class RestTemplateController { 
        @Autowired
        private RestTemplate restTemplate;
    
        @GetMapping("findAll")
        @ResponseBody
        public Collection findAll(){ 
            return  restTemplate.getForEntity("http://localhost:8010/student/findAll", Collection.class).getBody();
        }
        @GetMapping("findAll2")
        @ResponseBody
        public Collection findAll2(){ 
            return  restTemplate.getForObject("http://localhost:8010/student/findAll",Collection.class);
        }
    
        @PostMapping("save")
        public String save(@RequestBody Student student){ 
            restTemplate.postForEntity("http://localhost:8010/student/save",student,null).getBody();
            return "rediret:findAll";
        }
        @PostMapping("save2")
        public String save2(@RequestBody Student student){ 
            restTemplate.postForObject("http://localhost:8010/student/findAll",student,null);
            return "rediret:findAll";
        }
    
    
        @PostMapping("update")
        public String update(@RequestBody Student student){ 
            restTemplate.postForEntity("http://localhost:8010/student/update",student,null).getBody();
            return "rediret:findAll";
        }
        @PostMapping("update2")
        public String update2(@RequestBody Student student){ 
            restTemplate.postForObject("http://localhost:8010/student/update",student,null);
            return "rediret:findAll";
        }
    
        @GetMapping("findById/{id}")
        @ResponseBody
        public Student findById(@PathVariable("id") Long id){ 
            return restTemplate.getForEntity("http://localhost:8010/student/findById/{id}", Student.class,id).getBody();
        }
        @GetMapping("findById2/{id}")
        @ResponseBody
        public Student findById2(@PathVariable("id") Long id){ 
            return restTemplate.getForObject("http://localhost:8010/student/findById/{id}", Student.class,id);
        }
    
        @GetMapping("deleteById/{id}")
        public String deleteById(@PathVariable("id") Long id){ 
            restTemplate.getForEntity("http://localhost:8010/student/deleteById/{id}", Student.class,id).getBody();
            return "rediret:findAll";
        }
        @GetMapping("deleteById2/{id}")
        public String deleteById2(@PathVariable("id") Long id){ 
            restTemplate.getForObject("http://localhost:8010/student/deleteById/{id}", Student.class,id);
            return "rediret:findAll";
        }
    }
    
    
  4. 这里的启动类要注意!需要把RestTemplate接口加入IOC容器中

    @SpringBootApplication
    public class RestApplication { 
    
        public static void main(String[] args) { 
            SpringApplication.run(RestApplication.class,args);
        }
    
        @Bean
        public RestTemplate restTemplate(){ 
            return new RestTemplate();
        }
    }
    
  5. 依次启动注册中心,服务提供者,resttemplate,访问http://localhost:8761/

    可以看到只有一个服务提供者在注册中心,因为我们并没有将resttemplate注册到注册中心

  6. 使用POSTMAN测试

    数据返回一致,发现我们可以使用resttemplate可以调用服务提供者的接口!!

    小结

    1. 新建Module resttemplate
    2.编写启动类,将RestTemplate注入IOC
    3.编写web层,去调用服务提供者 
    4.用postman测试
    

2.4 实战:注册一个服务消费者

实现思路就是基于Spring Boot环境,新建模块eureka-consumer,利用eureka-client把eureka-consumer注册到eureka-server,再借助于RestTemplate实现微服务接口的调用,调用的是服务提供者!

  1. 新建eureka-consumer,导入相关依赖

  2. 编写配置文件

    server:
      port: 8020
    spring:
      application:
        name: consumer
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka/
      instance:
        prefer-ip-address: true
    
  3. 编写实体类

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Student { 
        private Long id;
        private String name;
        private Integer age;
    }
    
  4. 编写web层

    @Controller
    @RequestMapping("cons")
    public class ConsumerController { 
        @Autowired
        private RestTemplate restTemplate;
    
        @GetMapping("findAll")
        @ResponseBody
        public Collection findAll(){ 
            return  restTemplate.getForEntity("http://localhost:8010/student/findAll", Collection.class).getBody();
        }
        @GetMapping("findAll2")
        @ResponseBody
        public Collection findAll2(){ 
            return  restTemplate.getForObject("http://localhost:8010/student/findAll",Collection.class);
        }
    
        @PostMapping("save")
        public String save(@RequestBody Student student){ 
            restTemplate.postForEntity("http://localhost:8010/student/save",student,null).getBody();
            return "rediret:findAll";
        }
        @PostMapping("save2")
        public String save2(@RequestBody Student student){ 
            restTemplate.postForObject("http://localhost:8010/student/findAll",student,null);
            return "rediret:findAll";
        }
    
    
        @PostMapping("update")
        public String update(@RequestBody Student student){ 
            restTemplate.postForEntity("http://localhost:8010/student/update",student,null).getBody();
            return "rediret:findAll";
        }
        @PostMapping("update2")
        public String update2(@RequestBody Student student){ 
            restTemplate.postForObject("http://localhost:8010/student/update",student,null);
            return "rediret:findAll";
        }
    
        @GetMapping("findById/{id}")
        @ResponseBody
        public Student findById(@PathVariable("id") Long id){ 
            return restTemplate.getForEntity("http://localhost:8010/student/findById/{id}", Student.class,id).getBody();
        }
        @GetMapping("findById2/{id}")
        @ResponseBody
        public Student findById2(@PathVariable("id") Long id){ 
            return restTemplate.getForObject("http://localhost:8010/student/findById/{id}", Student.class,id);
        }
    
        @GetMapping("deleteById/{id}")
        public String deleteById(@PathVariable("id") Long id){ 
            restTemplate.getForEntity("http://localhost:8010/student/deleteById/{id}", Student.class,id).getBody();
            return "rediret:findAll";
        }
        @GetMapping("deleteById2/{id}")
        public String deleteById2(@PathVariable("id") Long id){ 
            restTemplate.getForObject("http://localhost:8010/student/deleteById/{id}", Student.class,id);
            return "rediret:findAll";
        }
    }
    
    
  5. 编写启动类

    @SpringBootApplication
    public class ConsumerApplication { 
    
        public static void main(String[] args) { 
            SpringApplication.run(ConsumerApplication.class,args);
        }
    
        @Bean
        public RestTemplate restTemplate(){ 
            return new RestTemplate();
        }
    }
    
    
  6. postman测试

2.5 实战:Zuul服务网关


一、什么是Zuul服务网关?
Zuul是Netflix提供的一个开源的API网关服务器,是客户端网站和后端所有请求的中间层,对外开发一个API,将所有请求导入同一入口,屏蔽了服务端的具体实现逻辑,Zuul可以实现反向代理功能,在网关内部实现动态路由,身份验证,IP过滤,数据监控等,Zuul也是Spring Cloud集成的组件,用它来实现服务网关。

二、Zuul的功能列表

  • 身份认证与安全:识别每个资源的验证要求,并拒绝那些要求不符合的请求
  • 审查与监控:在边沿位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
  • 动态路由:动态的将请求路由映射到不同的后端集群。
  • 压力测试:逐渐增加指向集群的流量,以了解性能。
  • 负载分配:为每一种负载类型分配对应的容量,并弃用超出限定值的请求。
  • 静态响应处理:在边沿位置直接建立部分响应,从而避免其转发到内部集群。
  • 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB(Elastic Load Balancing) 使用的多样化,以及让系统的优越更贴近系统的使用者。

三、Zuul的代码实现

  1. 新建zuul模块,导入相关依赖

        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
                <version>2.0.2.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
                <version>2.0.2.RELEASE</version>
            </dependency>
        </dependencies>
    
  2. 编写application.yml配置文件

    server:
      port: 8030
    spring:
      application:
        name: zuul
    eureka:
      instance:
        prefer-ip-address: true
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka/
    zuul:
      routes:
        provider: /p/**
    

    属性说明

    zuul.routes.provider:为网关配映射置路由,指向注册中心的provider服务提供者,可以使用含/p/**的访问到服务提供者,可以不提供端口就可以实现服务路由

  3. 编写启动类

    @EnableAutoConfiguration
    @EnableZuulProxy
    public class ZuulApplication { 
    
        public static void main(String[] args) { 
            SpringApplication.run(ZuulApplication.class,args);
        }
    }
    

    注解说明:

    @EnableAutoConfiguration:可以帮助Spring Boot应用将满足条件的@Configration配置类加载到Spring Boot管理的IoC容器中
    @EnableZuulProxy:包含了@EnableZuulServer,设置该类为网关的启动类

访问网关:http://localhost:8030/p/student/findAll,此时网关地址可以映射为
localhost:8010/student/findAll
要注意配置文件不能出现如下画面:

这样可能会导致一些配置失效!!

留下问题:我的服务消费者可以调用服务提供者的接口,网关可以映射到服务提供者的接口,那为什么网关不能映射到服务消费者?
访问:http://localhost:8030/p/consumer/findAll 却爆404

  1. Zuul实现负载均衡

    假设有非常大的流量要访问系统,服务提供者只有一个,无法承受如此大的流量,这个时候我们可以考虑多个服务提供者实现负载分担,这样可以降低服务被压垮的风险。

    为服务提供者添加方法

        @Value("${server.port}")
    	 private Integer port;
        @GetMapping("index")
        public String index(){ 
            return "端口:"+this.port;
        }
    

    复制粘贴服务提供者提供类ProviderApplication2

    先依次启动eureka-server(注册中心),eureka-client(服务提供者),zuul(服务网关)

    再修改服务提供者的配置文件端口号为8011

    再次启动ProviderApplication2
    此时会再注册中心发现:

    当有多个服务提供者被注册到注册中心,默认就实现了负载均衡!

    两个服务提供者会互相切换使用

    Zuul服务网关是微服务架构总不可或缺的一部分,通过服务网关向外系统提供REST API的过程中,除了具备服务路由,负载均衡,它还具备权限控制等功能,它还可以和Ribbon搭配使用!

2.6 实战:Ribbon负载均衡

在前面我们已经使用RestTemplate来实现服务消费者调用服务提供者的接口,如果对某个具体的业务场景下,对某个服务的流量突然大幅度提升,这个时候我们需要使用负载均衡来满足高并发的访问情况,前面我们使用zuul实现了<mark>外部请求的负载均衡</mark>,但它并不是专业的,Spring Cloud提供了更为专业的负载均衡解决方案,那就是Ribbon,实现了<mark>微服务之间</mark>的负载均衡!

一、Ribbon介绍

Ribbon是Spring Cloud的一个组件,也是Spring Cloud解决负载均衡一个解决方案,Ribbon是Netflix发布的负载均衡器,Spring对其进行了集成,Spring Cloud Ribbon是基于Netflix Ribbon实现的,是一个对HTTP请求进行控制的负载均衡客户端。Spring Cloud Ribbon也是要结合Eureka Server来使用的,所以也要在注册中心注册!

在注册中心对Ribbon进行注册后,Ribbon就可以基于某种负载均衡算法,如轮询,随机,加权轮询,加群随机等自动自动帮助服务消费者调用接口,开发者也可以根据具体需求自定义Ribbon负载均衡算法。

实际开发中,结合Spring Cloud Eureka 来使用,Eureka Server提供所有可以调用的服务提供者列表,Ribbon基于特定的负载均衡算法从这些服务者中选择要调用的具体实例!

二、Ribbon实战

  1. 新建ribbon实例,导入相关依赖,使得加入注册中心

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>
    </dependencies>
    
  2. 编写application配置文件

    server:
      port: 8040
    spring:
      application:
        name: ribbon
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka
      instance:
        prefer-ip-address: true
    
  3. 编写实体类

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Student { 
        private Long id;
        private String name;
        private Integer age;
    }	
    
  4. 编写web控制器

    @Controller
    @RequestMapping("/ribbon")
    public class RibbonHandler { 
    
        @Autowired
        private RestTemplate restTemplate;
    
        @GetMapping("/findAll")
        @ResponseBody
        public Collection<Student> findAll(){ 
            return restTemplate.getForObject("http://provider/student/findAll",Collection.class);
        }
    
        @GetMapping("/index")
        @ResponseBody
        public String index(){ 
            return restTemplate.getForObject("http://provider/student/index",String.class);
        }
    }
    
    
  5. 编写启动类

    @SpringBootApplication
    public class RibbonApplication { 
        public static void main(String[] args) { 
            SpringApplication.run(RibbonApplication.class,args);
        }
        @Bean
        @LoadBalanced
        public RestTemplate restTemplate(){ 
            return new RestTemplate();
        }
    }
    
    
  6. 依次启动注册中心,两个服务提供者(对应的端口8010,8011),再启动ribbon,此时访问注册中心地址http://localhost:8761/

  7. 测试负载均衡,访问ribbon控制器下的地址

    点击刷新,端口在8010,8011之间切换!如果再添加一个服务提供者给定端口8012,可以看到端口的切换默认用的是轮询机制!并不是随机切换!

2.7 实战:Feign声明式接口调用

一、什么是Feign
Feign也是去实现负载均衡,但是它比Ribbon更加简化,它实际上是基于Ribbon进行了封装,让我们可以通过<mark>调用接口的方式</mark>实现负载均衡。
Fegin和Ribbon都是Netflix提供的,Feign是一个声明式,模板的Web Service客户端,它简化了开发者编写Web服务客户端的操作,开发者可以通过简单的接口和注解来调用HTTP API,使得开发更加简化,快捷。

Spring Cloud Feign 也是基于Netflix Feign的二次开发,它整合了Ribbon和Hystrix,具有可插拔,基于注解,负载均衡,服务熔断等一系列便捷功能,也就是说我们在实际开发中可以用Feign来取代Ribbon!

相比较于Ribbon+RestTemplate的方式,Feign大大简化了代码的开发,Feign支持多种注解,包括Feign注解,JAX-RS注解,SpringMVC注解等,Spring Cloud对Feign进行了优化,整合了Ribbon和Eureka,从而让Feign更加方便!

二、Feign和Ribbon的区别

Ribbon是一个通用的HTTP客户端工具,Feign是基于Ribbon实现的!

三、Feign的优点

  • Feign是一个声明式的Web Service客户端
  • 支持Feign注解,SpringMVC注解,JAX-RS注解
  • Feign是基于Ribbon实现,使用起来更加方便
  • Feign集成了Hystrix,具备服务熔断的功能

四、Feign实现负载均衡

  1. 新建feign模块,导入相关依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>
    </dependencies>
    
  2. 编写application.yml配置文件

    server:
      port: 8050
    spring:
      application:
        name: feing
    eureka:
      client:
        service-url: 
          defaultZone: http://localhost:8761/eureka
      instance:
        prefer-ip-address: true
    
  3. 编写启动类,声明为Feign的客户端

    @SpringBootApplicatKion
    @EnableFeignClients
    public class FeignApplication { 
        public static void main(String[] args) { 
            SpringApplication.run(FeignApplication.class,args);
        }
    }
    
  4. 创建声明式接口

    // 服务提供者名字
    @FeignClient(value = "provider")
    public interface IFeignSerive { 
    
        @GetMapping("/student/findAll")
        public Collection<Student> findAll();
    
        @GetMapping("/student/index")
        public String index();
    }
    
  5. 编写Handler调用声明式接口

    @RequestMapping("/feign")
    @Controller
    public class FeignHandler { 
        @Autowired
        private IFeignSerive iFeignSerive;
    
        @GetMapping("/findAll")
        @ResponseBody
        public Collection<Student> findAll(){ 
            return  iFeignSerive.findAll();
        }
        
        @GetMapping("index")
        @ResponseBody
        public String index(){ 
            return iFeignSerive.index();
        }
    }
    
    
  6. 依次启动注册中心,两个不同端口的服务提供者(8010,8011,8012),访问地址http://localhost:8761/

  7. 测试访问地址:http://localhost:8050/feign/index

    发现服务也可以实现负载均衡,服务端口的切换默认是轮询,是对ribbon的二次封装,使得服务的调用更加简便!

五、Feign实现服务熔断

所谓的服务熔断,类似于保险丝,当我们访问一个down掉的服务提供者的服务时,可以给出友好的提示!

  1. 当我们停掉所有的服务提供者,只保留下注册中心和Feign

  2. 此时我们访问服务提供者提供的接口

  3. 我们希望给出友好的提示,而不是一个异常的页面,为feign的配置文件加入熔断机制

    server:
      port: 8050
    
    spring:
      application:
        name: feign
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka
    
      instance:
        prefer-ip-address: true
    feign:
      hystrix:
        enabled: true
    
  4. 创建IFeignService接口的实现类,并在类中加入服务熔断处理(降级处理),需要将实现类加入IOC容器中

    @Component
    public class IFeignServiceError implements IFeignService { 
    
        @Override
        public Collection<Student> findAll() { 
            return null;
        }
    
        @Override
        public String index() { 
            return "服务器维护中...";
        }
    }
    
    
  5. 修改声明式接口,当出现服务熔断时,可以进行降级处理

    @FeignClient(value = "provider",fallback = IFeignServiceError.class)
    public interface IFeignSerive { 
    
        @GetMapping("/student/findAll")
        public Collection<Student> findAll();
    
        @GetMapping("/student/index")
        public String index();
    }
    
  6. 全部重新启动,然后停掉所有的服务提供者,重新访问http://localhost:8050/feign/index

2.8 实战:Hystrix容错机制

在不改变各个微服务的调用关系的前提下,针对错误情况进行<mark>预先处理</mark>。

  • 设计原则
    1. 服务隔离机制
    2. 服务降级机制
    3. 服务熔断机制
    4. 提供实时的监控和报警机制
    5. 提供实时的配置修改功能

Hystrix的服务监控需要结合Spring Boot提供的Actuator组件,Actuator提供了对服务的健康检查,数据统计,可以通过hystrix.stream获取监控的请求数据,提供了可视化的监控界面。

  1. 创建Maven模块,并引入pom.xml文件

     <dependencies>
    <!-- 注册到注册中心-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
                <version>2.0.2.RELEASE</version>
            </dependency>
    <!-- 一般结合feign使用-->	
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
                <version>2.0.2.RELEASE</version>
            </dependency>
    
    <!-- 注册组件Actuator-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
                <version>2.0.7.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
                <version>2.0.2.RELEASE</version>
            </dependency>
            <!-- 注册hystrix的可视化仪表盘-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
                <version>2.0.2.RELEASE</version>
            </dependency>
        </dependencies>
    
  2. 创建application.yml配置文件

    server:
      port: 8060
    spring:
      application:
        name: hystrix
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka
      instance:
        ip-address: true
    feign:
      hystrix:
        enabled: true
    management:
      endpoints:
        web:
          exposure:
            include: "hystrix.stream"
    
    

    配置management.endpoints.web.exposure.include的节点为hystrix.stream,达到数据监控的目的

  3. 创建启动类

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

    EnableCircuitBreaker:开启数据监控
    EnableHystrixDashboard:开启可视化数据监控

  4. 引入实体类,用于接收数据

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Student { 
        private Long id;
        private String name;
        private Integer age;
    }
    
  5. 引入Feigin的声明式接口调用

    @FeignClient("provider")
    public interface FeignProviderClient { 
    
        @GetMapping("student/findAll")
        public Collection<Student> findAll();
    
        @GetMapping("student/index")
        public String index();
    }
    

    因为这边只做数据监控,所以只需要引入接口即可,无需引入降级的实现类。

  6. 创建Hystrix的控制层

    @RequestMapping("/feign")
    @Controller
    public class FeignHandler { 
        @Autowired
        private IFeignSerive iFeignSerive;
    
        @GetMapping("/findAll")
        @ResponseBody
        public Collection<Student> findAll(){ 
            return  iFeignSerive.findAll();
        }
    
        @GetMapping("index")
        @ResponseBody
        public String index(){ 
            return iFeignSerive.index();
        }
    }
    
  7. 测试数据监控
    依次启动 注册中心—>服务提供者—>hystrix

    访问hystrix的控制层,打开数据监控

    JSON文件:

    data: { 
    	"type": "HystrixCommand",
    	"name": "FeignProviderClient#index()",
    	"group": "provider",
    	"currentTime": 1604806702960,
    	"isCircuitBreakerOpen": false,
    	"errorPercentage": 0,
    	"errorCount": 0,
    	"requestCount": 0,
    	"rollingCountBadRequests": 0,
    	"rollingCountCollapsedRequests": 0,
    	"rollingCountEmit": 0,
    	"rollingCountExceptionsThrown": 0,
    	"rollingCountFailure": 0,
    	"rollingCountFallbackEmit": 0,
    	"rollingCountFallbackFailure": 0,
    	"rollingCountFallbackMissing": 0,
    	"rollingCountFallbackRejection": 0,
    	"rollingCountFallbackSuccess": 0,
    	"rollingCountResponsesFromCache": 0,
    	"rollingCountSemaphoreRejected": 0,
    	"rollingCountShortCircuited": 0,
    	"rollingCountSuccess": 0,
    	"rollingCountThreadPoolRejected": 0,
    	"rollingCountTimeout": 0,
    	"currentConcurrentExecutionCount": 0,
    	"rollingMaxConcurrentExecutionCount": 0,
    	"latencyExecute_mean": 0,
    	"latencyExecute": { 
    		"0": 0,
    		"25": 0,
    		"50": 0,
    		"75": 0,
    		"90": 0,
    		"95": 0,
    		"99": 0,
    		"99.5": 0,
    		"100": 0
    	},
    	"latencyTotal_mean": 0,
    	"latencyTotal": { 
    		"0": 0,
    		"25": 0,
    		"50": 0,
    		"75": 0,
    		"90": 0,
    		"95": 0,
    		"99": 0,
    		"99.5": 0,
    		"100": 0
    	},
    	"propertyValue_circuitBreakerRequestVolumeThreshold": 20,
    	"propertyValue_circuitBreakerSleepWindowInMilliseconds": 5000,
    	"propertyValue_circuitBreakerErrorThresholdPercentage": 50,
    	"propertyValue_circuitBreakerForceOpen": false,
    	"propertyValue_circuitBreakerForceClosed": false,
    	"propertyValue_circuitBreakerEnabled": true,
    	"propertyValue_executionIsolationStrategy": "THREAD",
    	"propertyValue_executionIsolationThreadTimeoutInMilliseconds": 1000,
    	"propertyValue_executionTimeoutInMilliseconds": 1000,
    	"propertyValue_executionIsolationThreadInterruptOnTimeout": true,
    	"propertyValue_executionIsolationThreadPoolKeyOverride": null,
    	"propertyValue_executionIsolationSemaphoreMaxConcurrentRequests": 10,
    	"propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests": 10,
    	"propertyValue_metricsRollingStatisticalWindowInMilliseconds": 10000,
    	"propertyValue_requestCacheEnabled": true,
    	"propertyValue_requestLogEnabled": true,
    	"reportingHosts": 1,
    	"threadPool": "provider"
    }
    

    打开可视化界面http://localhost:8060/hystrix

    当我们访问hystrix的控制层的时候,也可以监控到信息

2.9 实战:本地配置中心

Spring Cloud Config,通过服务端可以为多个客户端提供配置服务,这样可以避免多处配置文件的修改,不利于配置文件的集中管理和后期的难以维护等问题。Spring Cloud Config可以将配置文件存储在本地,也可以将配置文件存储在远程Git仓库,创建Config Server,通过它管理所有的配置文件。

  1. 新建Maven模块,本地配置中心,并引入pom.xml文件

        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-config-server</artifactId>
                <version>2.0.2.RELEASE</version>
            </dependency>
        </dependencies>
    
  2. 创建application.yml

    server:
      port: 8762
    spring:
      application:
        name: nativeconfigserver
      profiles:
        active: native
      cloud:
        config:
          server:
            native:
              search-locations: classpath:/shared
    

profiles.active:配置文件的存储位置:本地
cloud.config.server.native.search-locations:配置文件的本地路径,类路径下的shared

  1. 在resources下新建shared文件夹,并引入客户端的配置文件

    server:
      port: 8070
    foo: foo version 1
    

这个命名是有学问的!!一会儿来揭晓…

  1. 创建启动类

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

    @EnableConfigServer:开启配置中心

  2. 创建Maven模块,连接配置中心的客户端服务,引入pom.xml依赖

    <!-- 用于读取配置中心的依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
        <version>2.0.2.RELEASE</version>
    </dependency>
    
  3. 创建配置文件bootstrap.yml(名字规定不变)

    spring:
      application:
        name: configclient
      profiles:
        active: dev
      cloud:
        config:
          fail-fast: true
          uri: http://localhost:8762
    

    疑惑揭晓:配置中心提供给客户端的/shared文件下的configclient-dev命名就是为了客户端能够以spring.application.namespring.profiles.active进行拼接获取到指定的配置文件。
    cloud.config.fail-fase:设置客户端优先判断Config Server获取是否正常
    cloud.config.uri:本地Config Server的访问路径

  4. 创建启动类

    @SpringBootApplication
    public class NativeClientConfigApplication { 
        public static void main(String[] args) { 
            SpringApplication.run(NativeClientConfigApplication.class,args);
        }
    }
    
  5. 创建客户端的Controller,尝试读取配置中心的配置文件

    @RestController
    @RequestMapping("native")
    public class NativeConfigController { 
        @Value("${server.port}")
        private String port;
        @Value("${foo}")
        private String version;
    
        @GetMapping("index")
        public String index(){ 
            return "port:"+this.port + "-" + "foo:"+this.version;
        }
    }
    
  6. 测试接口

依次启动:注册中心—>配置中心—>客户端

启动客户端时抛出异常:

Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder ‘foo.version’ in value “${foo.version}”

在客户端的启动类中增加方法即可解决:

@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { 
    PropertySourcesPlaceholderConfigurer c = new PropertySourcesPlaceholderConfigurer();
    c.setIgnoreUnresolvablePlaceholders(true);
    return c;
}

作用是这只@Value取值取不到时不报错!
此时我们重新访问:http://localhost:8070/native/index

至于${foo}为什么没取到值是一个谜…

3.0 实战:远程配置中心

由于IDEA没有整合Git,后续补充。

3.1 Zipkin 服务追踪

Spring Cloud Zipkin是一个可以采集并且跟踪分布式系统中请求数据的组件,让开发者可以更加直观的监控到各个微服务的请求和所耗费的时间等,它包括两部分:Zipkin Server,Zipkin Client

  1. 创建Maven工程,引入相关依赖

    <!-- 添加zipkin服务-->
            <dependency>
                <groupId>io.zipkin.java</groupId>
                <artifactId>zipkin-server</artifactId>
                <version>2.9.4</version>
            </dependency>
    <!-- 添加zipkin可视化界面-->
            <dependency>
                <groupId>io.zipkin.java</groupId>
                <artifactId>zipkin-autoconfigure-ui</artifactId>
                <version>2.9.4</version>
            </dependency>
    
  2. 创建application.yml配置文件

    server:
      port: 8090
    
  3. 创建启动类

    @SpringBootApplication
    @EnableZipkinServer
    public class ZipkinApplication { 
    
        public static void main(String[] args) { 
            SpringApplication.run(ZipkinApplication.class,args);
        }
    }
    
  4. 创建Zipkin客户端,引入相关pom.xml依赖

     <dependencies>
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-zipkin</artifactId>
             <version>2.0.2.RELEASE</version>
         </dependency>
     </dependencies>
    
  5. 创建application.yml配置文件

    server:
      port: 8090
    spring:
      application:
        name: zipkinclient
      sleuth:
        web:
          client:
            enabled: true
        sampler:
          probability: 1.0
      zipkin:
        base-url: http://localhost:9090
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka
    

    sleuth.web.client.enable:开启服务追踪
    sleuth.web.sampler.probability:设置服务采集的比例
    zipkin.base-url:zipkin的服务的地址

  6. 创建启动类

    @SpringBootApplication
    public class ZipkinClientApplication { 
        public static void main(String[] args) { 
            SpringApplication.run(ZipkinClientApplication.class,args);
        }
    }
    
  7. 创建Zipkin的controller

    @RequestMapping("zipkin")
    @RestController
    public class ZipkinClientController { 
    
        @Value("${server.port}")
        private String port;
    
        @GetMapping("index")
        public String index(){ 
            return this.port;
        }
    }
    
  8. 测试服务 追踪
    访问localhost:9090/zipkin报错:

    io.undertow.request : UT005023: Exception handling request to /zipkin/index.html
    java.lang.IllegalArgumentException: Prometheus requires that all meters with the same name have the same set of tag keys. There is already an existing meter named ‘http_server_requests_seconds’ containing tag keys 【method, status, uri】. The meter you are attempting to register has keys 【exception, method, status, uri】.

    查询网上资料,可以在zipkin server 配置文件中添加解决

    management.metrics.web.server.auto-time-requests=false
    

    但是还是无法访问

    原因有可能是jdk版本的问题,具体的原因随着学习的深入会回来继续排查。

3. 最后致谢

感谢楠哥教程:
https://www.bilibili.com/video/BV1p4411K7pz?p=10

 3 total views,  1 views today

页面下部广告