初识SpringCloud

我们先讨论一下是微服务

架构的发展历程是从单体式架构,到分布式架构,到SOA架构,再到微服务架构

单体式架构→分布式架构→SOA架构→微服务架构

1、单服务架构

所有代码都放在一个项目中,根据MVC分层,运行在web容器中

2、分布式架构

根据单服务架构的基本分层,将不同的业务拆分成多个应用,相互的业务互相调用

3、SOA架构

在基于分布式架构上升级的一种面向服务的架构,所有服务将注册到服务的注册中心,让服务的提供者和服务的使用者形成一种相互绑定的关系

4、微服务架构

而我们今天讨论的微服务架构则是在SOA基础上进一步的升级,官方的描述是这样的:

就目前而言,对于微服务业界并没有一个统一的、标准的定义(While there is no precise definition of this architectural style ) 。但通常在其而言,微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分成一组小的服务,每个服务运行独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于 HTTP 的 RESTful API ) 。每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。另外,应尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务。可以使用不同的语言来编写服务,也可以使用不同的数据存储。

总结一下微服务的定义:

1、符合“微”字,服务非常小,但是不是为了拆分而拆分

2、独立进程服务和独立部署,享有独立的JVM,如运行在docker上,相互隔离的资源,结合kubernetes等可以做到滚动更新,不影响其他服务(业务正常的情况下,如业务变动当然会影响其他服务)

3、轻量级通信方式,RPC、HTTP等

目前比较流行的的微服务解决方案是Dobbo和SpringCloud,我们使用的服务基本都是Spring Boot,所以也是作为我们首选的方案

以下使用到的SpringCloud版本为

2021.0.3
关于SpringCloud和SpringBoot对应的版本,我们可以去官方查看

 "spring-cloud":{
            "Hoxton.SR12":"Spring Boot >=2.2.0.RELEASE and <2.4.0.M1",
            "2020.0.6":"Spring Boot >=2.4.0.M1 and <2.6.0-M1",
            "2021.0.0-M1":"Spring Boot >=2.6.0-M1 and <2.6.0-M3",
            "2021.0.0-M3":"Spring Boot >=2.6.0-M3 and <2.6.0-RC1",
            "2021.0.0-RC1":"Spring Boot >=2.6.0-RC1 and <2.6.1",
            "2021.0.3":"Spring Boot >=2.6.1 and <3.0.0-M1",
            "2022.0.0-M1":"Spring Boot >=3.0.0-M1 and <3.0.0-M2",
            "2022.0.0-M2":"Spring Boot >=3.0.0-M2 and <3.0.0-M3",
            "2022.0.0-M3":"Spring Boot >=3.0.0-M3 and <3.0.0-M4",
            "2022.0.0-M4":"Spring Boot >=3.0.0-M4 and <3.1.0-M1"
        }

SpringCloud主要组件:

1、Eureka

由Netflix开源的基于REST的服务发现组件,其中包含了Server端和Client端

功能:

(1)服务注册和发现

(2)角色划分:注册中心、服务提供者、服务消费者

基本流程:客户端向服务端注册,将客户端的信息发送给服务端并30秒进行一次心跳检测,如果不可用则会删除该客户端,但为了防止异常情况,服务端会保留客户端的信息并提示,这是eureka的保护机制,可以设置关闭

使用:

首先,如果各位使用的idea,先打开service(mac,具体位置在:View→Tools windows→Services,若使用的是win:View→Tools windows→Run Dashboard)

引用jar包

 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

配置application.yml

server:
    port: 8001 #指定运行端口
spring:
    application:
        name: eureka-server #指定服务名称
eureka:
    instance:
        hostname: localhost #指定主机地址
    client:
        fetch-registry: false #指定是否要从注册中心获取服务(注册中心不需要开启)
        register-with-eureka: false #指定是否要注册到注册中心(注册中心不需要开启)
    server:
        enable-self-preservation: false #关闭保护模式

以上是服务端

创建另一个项目,引入jar包:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

配置application.yml


server:
    port: 8101 #运行端口号
spring:
    application:
        name: eureka-client #服务名称
eureka:
    client:
        register-with-eureka: true #注册到Eureka的注册中心
        fetch-registry: true #获取注册实例列表
        service-url:
            defaultZone: http://localhost:8001/eureka/ #配置注册中心地址

先启动server端,再启动client,启动项目后查看localhost:8001则可看到

红字提示的是我关闭了保护模式,我们可以看到有一个客户端已经注册到application中了

另:还有双注册中心的做法,把server端当做一个client注册到另一个server端去,双注册中心的做法相当于负载均衡;我们还会给生产环境中的server端增加security、sso、oauth2等用户登录增加认证提高安全性

2、Ribbon/LoadBalancer

由Netflix开源的负责均衡组件,目前已经停止更新状态了

基本原理是:通过注解@LoadBalance,把SpringBoot提供的RestTemplate注册到列表中,为这些RestTemplate增加拦截器,再将调度方法交给Ribbon负载均衡器去处理

后续被Spring官方推出的LoadBalancer替换了,使用起来差不多

此处我们需要四个服务(1)eureka-server;(2)biz业务接口,需要启动两个,作为负载均衡;(3)loadBlancer

eureka-server使用刚刚使用的

biz创建一个,使用servers,定义两个application.yml分别启动,此处我是单机运行,所以使用本机ip+不同端口,实际服务上根据具体服务器资源编排情况分配不同的ip和端口

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

添加业务代码

@RestController
@RequestMapping("/biz")
public class BizController {

    @PostMapping("/create")
    public String create() {
        return "操作成功";
    }

    @GetMapping("/{id}")
    public String getDataById(@PathVariable Long id) {
        System.out.println("获取数据" + id);
        return "获取信息";
    }

    @GetMapping("/getUserByIds")
    public String getByIds(@RequestParam List<Long> ids) {
        return "根据ids获取用户信息,用户列表为" + ids;
    }

    @GetMapping("/getByUsername")
    public String getByName(@RequestParam String name) {
        return "获取信息" + name;
    }

    @PostMapping("/update")
    public String update() {
        return "操作成功";
    }

    @PostMapping("/delete/{id}")
    public String delete(@PathVariable Long id) {
        return "操作成功";
    }

}

loadBalancer负载均衡器

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

为RestTemplate增加注解,并使用服务名调用,注意:此处必须要使用服务名,服务名的配置如下,这是业务服务的服务名,使用RestTemplate调用时必须使用服务名进行调用,如果使用ip+端口,会提示No instances available for localhost

spring:
    application:
        name: biz-service

在负载均衡器上配置application.yml:

server:
    port: 8301
spring:
    application:
        name: load-balancer-service
eureka:
    client:
        register-with-eureka: true
        fetch-registry: true
        service-url:
            defaultZone: http://localhost:8001/eureka/
# 这里用作测试所以命名随意,在生产中一般会使用配置中心,把该配置统一到配置中心
service-url:
    biz-service: http://biz-service/

在loadBalancer中调用

@RestController
@RequestMapping("/biz")
public class BizController {

    @Autowired
    @LoadBalanced
    private RestTemplate restTemplate;

    @Value("${service-url.biz-service}")
    private String bizServiceUrl;

    @PostMapping("/create")
    public String create() {
        return restTemplate.getForObject(bizServiceUrl + "biz/create", String.class);
    }

    @GetMapping("/{id}")
    public String getDataById(@PathVariable Long id) {
        ResponseEntity<String> forObject = restTemplate.getForEntity(bizServiceUrl + "biz/{1}", String.class, id);
        return forObject.getBody();
    }

    @GetMapping("/getUserByIds")
    public String getByIds(@RequestParam List<Long> ids) {
        return restTemplate.getForObject(bizServiceUrl + "getUserByIds?ids={1}", String.class, ids);
    }

    @GetMapping("/getByUsername")
    public String getByName(@RequestParam String name) {
        return restTemplate.getForObject(bizServiceUrl + "getByUsername?ids={1}", String.class, name);
    }

    @PostMapping("/update")
    public String update() {
        return restTemplate.postForObject(bizServiceUrl + "update", null, String.class);
    }

    @PostMapping("/delete/{id}")
    public String delete(@PathVariable Long id) {
        return restTemplate.postForObject(bizServiceUrl + "delete/{1}", null, String.class, id);
    }

}

最后我们来看看效果,在浏览器中调用了两次loadBalancer服务

另:关于RestTemplate的用法此处不展开,想要了解的自行去Google,这边我提出一个思考,既然两个服务通过loadBalancer负载均衡了,那么loadBalancer自己的负载怎么办呢?

我的思考是:记得刚刚说的双注册中心吗?注册多一个loadBalancer的对外服务,再利用健康检测就能使两个服务高可用了

最后loadBalancer有两种负载均衡算法:RoundRobinLoadBalancer(轮询) , RandomLoadBalancer(随机)

3、Feign/openFeign

同样由Netflix开源的负责均衡组件,目前已经停止更新状态了

Feign是一种声明式的RESTful网络请求,通过注解声明的形式注册一个client,类似于一个服务,调用时候扫描这些声明的client去根据JDK代理找到请求模板生成的http请求对象,这个请求对象是抽象的,根据实际情况使用HttpURLConnection、HttpClient、OkHttp

Spring官网同样出了一个新的OpenFeign

引入openFeign

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

在启动文件上配置注解

//扫描@FeignClient注解的MVC接口,并且动态代理产生实现类并且负载均衡到其他服务下
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class OpenFeignApplication {

    public static void main(String[] args) {
        SpringApplication.run(OpenFeignApplication.class, args);
    }

}

用我们之前使用的biz-server的接口改造一下

@Component
@FeignClient(value = "biz-service")//这里是服务名
public interface BizService {
    //这里用@RequestMapping注解,对应路径,此处有点像RestTemplate调用http://biz-server/biz/create
    @PostMapping("/biz/create")
    String create();

    @GetMapping("/biz/{id}")
    String getDataById(@PathVariable Long id);

    @GetMapping("/biz/getUserByIds")
    String getByIds(@RequestParam List<Long> ids);

    @GetMapping("/biz/getByUsername")
    String getByName(@RequestParam String name);

    @PostMapping("/biz/update")
    String update();

    @PostMapping("/biz/delete/{id}")
    String delete(@PathVariable Long id);

}

在controller引用并调用一下BizService,此次作为演示,你也可以放到其他service调用(注意不要循环依赖)

@RestController
@RequestMapping("/biz")
public class BizFeignController {

    @Autowired
    private BizService bizService;

    @PostMapping("/create")
    public String create() {
        return bizService.create();
    }

    @GetMapping("/{id}")
    public String getDataById(@PathVariable Long id) {
        return bizService.getDataById(id);
    }

    @GetMapping("/getUserByIds")
    public String getByIds(@RequestParam List<Long> ids) {
        return bizService.getByIds(ids);
    }

    @GetMapping("/getByUsername")
    public String getByName(@RequestParam String name) {
        return bizService.getByName(name);
    }

    @PostMapping("/update")
    public String update() {
        return bizService.update();
    }

    @PostMapping("/delete/{id}")
    public String delete(@PathVariable Long id) {
        return bizService.delete(id);
    }

}

完成以上操作后,依次启动eureka-server,两个biz-service(代码原封不动),open-feign

启动完成后,我们看一下eureka-server,发现所有服务都注册上了,并且biz-service有两个可用

我们在浏览器调用一下open-feign端口服务的rest api

多调用几次我们看一下控制台

同时实现了负载均衡

这里不尝试服务降级,后面会说服务降级相关的,国内常用Sentienl

我们来看看日志的使用

@Configuration
public class FeignLogConfig {

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

}
logging:
    level:
        com.jay.cloud.service.BizService: debug

4、Hystrix/Resilience4J/Sentienl

同样由Netflix开源的负责均衡组件,目前已经停止更新状态了

我们称为熔断器,是一个自我保护的装置,跟我们平时遇到的停电跳闸类似,它是为了阻止分布式系统中出现联动故障开发功能

(1)服务雪崩:服务提供者因为某些原因导致不可用,并将不可用组件放大的过程,举个例子:ABC三个服务,调用链是A→B→C,此时如果C因为业务代码编写不当的原因导致线程阻塞了,那么B跟A都无法使用,并且同样出现阻塞的情况,同时,如果ABC的线程中同时使用到数据库事务,那么其他功能模块可能同时都用不了,导致服务雪崩;

(2)断路器:为了防止服务雪崩,监控方法调用失败时,一般次数达到了阈值(要设置次数,因为要防止某些网络等因素导致的误判),这个断路器将会切断调用,直接返回异常,不会有真实的调用,这个就跟停电跳闸是一样的概念;

(3)服务降级:在整体资源不够的情况下,适当的放弃部分服务,将主要的资源投放在核心的服务中;

(4)资源隔离:将系统中的服务提供者隔离起来,一个服务提供延迟升高或者失败,不会影响整个系统失败,相当于负载均衡;

Spring官网同样出了一个Resilience4J,但国内一般都实用Sentienl,跟SpringCloud Alibaba的内容结合,后续单独一篇文章

我们用的新版是没有Hystrix的,所以需要单独引入

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-hystrix -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.2.10.RELEASE</version>
        </dependency>
@EnableHystrix
@EnableDiscoveryClient
@SpringBootApplication
public class HystrixServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(HystrixServiceApplication.class, args);
    }

}

注:当前版本@EnableCircuitBreaker已经废弃了,我们使用@EnableHystrix

@HystrixCommand(fallbackMethod = "getDefaultDataById")
    public String getDataById(Long id) {
        String url = bizServiceUrl + "/biz/" + id;
        System.out.println("调用接口:" + url);
        return restTemplate.getForObject(url, String.class);
    }

    public String getDefaultDataById(@PathVariable Long id) {
        return "服务降级";
    }

可调用一个空方法直接去看效果

还有更进阶的参数:

@HystrixCommand中的常用参数

  • fallbackMethod:指定服务降级处理方法;
  • ignoreExceptions:忽略某些异常,不发生服务降级;
  • commandKey:命令名称,用于区分不同的命令;
  • groupKey:分组名称,Hystrix会根据不同的分组来统计命令的告警及仪表盘信息;
  • threadPoolKey:线程池名称,用于划分线程池。

@HystrixCollapser的常用属性

  • batchMethod:用于设置请求合并的方法;
  • collapserProperties:请求合并属性,用于控制实例属性,有很多;
  • timerDelayInMilliseconds:collapserProperties中的属性,用于控制每隔多少时间合并一次请求;

另:Hystrix还有监控服务Hystrix Dashboard,后续我们使用SpringCloud Alibaba的Sentienl,这里不往下展开

5、Zuul/Gateway

Zuul也是由Netflix开源的网关组件,还有个Zuul2,但目前官方都推荐使用Gateway

API网关的定义类似于设计模式中的门面模式,所有的客户端的访问都会经过它来进行路由和过滤,可以实现请求路由、负载均衡、校验过滤、服务容错、服务聚合等等,官网推荐使用的Gateway是基于Spring生态下的API网关服务,并且他集成了Hystrix的断路器功能、Spring Cloud 服务发现功能、请求限流功能、Predicate(断言)和 Filter(过滤器)等

我们来补充一下相关概念:

(1)路由Route:路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由;

(2)断言Predicate:指的是Java 8 的 Function Predicate。 输入类型是Spring框架中的ServerWebExchange。 这使开发人员可以匹配HTTP请求中的所有内容,例如请求头或请求参数。如果请求与断言相匹配,则进行路由;

(3)过滤器Filter:指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前后对请求进行修改。

引入jar包

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

修改application.yml

server:
    port: 9201
service-url:
    biz-service: http://localhost:8201
spring:
    cloud:
        gateway:
            routes:
                - id: path_route #路由的ID
                  uri: ${service-url.biz-service}/biz/{id} #匹配后路由地址
                  predicates: # 断言,路径相匹配的进行路由
                      - Path=/biz/{id}

依次启动Eureka-server、biz-service、gateway

此时我们访问网关,则路由到biz-service的服务上

可以把routes的配置注入到Java Bean中,效果一样

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("path_route2", r -> r.path("/biz/getByUsername")
                        .uri("http://localhost:8201/biz/getByUsername"))
                .build();
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *