Spring Cloud

前言

系统架构演变

随着互联网的发展,网站应用的规模不断扩大,需求的激增,随之而来的是技术上的压力。系统架构也因此不断的演进、升级、迭代。从单一应用,到垂直拆分,到分布式服务,到SOA,以及现在火热的微服务架构。

集中式架构

网站流量很小时,只需要一个应用,将所有的功能都部署在一起,以减少部署节点和成本。

优点:系统开发速度快、维护成本低、适用于并发要求较低的系统。
缺点:代码耦合度高,后期维护困难、无法针对不同模块进行优化、无法水平扩展、单点容错率低,并发能力差。

垂直拆分

当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分。

优点:系统拆分实现了流量分担,解决了并发问题、可以针对不同模块进行优化、方便水平扩展,负载均衡,容错率提高。
缺点:系统间相互独立,会有很多重复开发工作,影响开发效率。

分布式服务

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。

优点:将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率
缺点:系统间耦合度变高,调用关系错综复杂,难以维护

服务治理(SOA)

SOA(Service Oriented Architecture)面向服务的架构:它是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务通常以独立的形式存在于操作系统进程中。各个服务之间通过网络调用。

SOA缺点:每个供应商提供的ESB产品有偏差,自身实现较为复杂;应用服务粒度较大,ESB集成整合所有服务和协议、数据转换使得运维、测试部署困难。所有服务都通过一个通路通信,直接降低了通信速度。

微服务

微服务架构是使用一套小服务来开发单个应用的方式或途径,每个服务基于单一业务能力构建,运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,并能够通过自动化部署机制来独立部署。

微服务的特点:
单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供REST的接口即可。
自治:自治是说服务间互相独立,互不干扰
团队独立:每个服务都是一个独立的开发团队。
技术独立:因为是面向服务,提供REST接口,使用什么技术没有别人干涉
前后端分离:采用前后端分离开发,提供统一REST接口,后端不用再为PC、移动段开发不同接口
数据库分离:每个服务都使用自己的数据源

微服务和SOA比较:
功能          SOA                      微服务
组件大小   大块业务逻辑           单独任务或小块业务逻辑
耦合      通常松耦合                  总是松耦合
管理      着重中央管理                着重分散管理
目标      确保应用能够交互操作     易维护、易扩展、更轻量级的交互
远程调用方式

常见的远程调用方式有以下几种:

  • RPC:Remote Procedure Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度
    快,效率高。早期的Web Service,现在热门的Dubbo,都是RPC的典型。
  • HTTP:HTTP其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用HTTP协议。也可以用来进行远程服务调用。缺点是消息封装臃肿。

Spring Cloud 简介

Spring Cloud是一个基于Spring Boot实现的微服务架构开发工具。它为微服务架构中涉及的配置管理、服务治理、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。

其主要涉及的组件包括:

  • Spring Cloud Netflix:核心组件,对多个Netfix OSS开源套件进行整合。
  • Eureka:服务治理组件,包括服务注册中心、服务注册与发现机制的实现。
  • Hystrix:熔断器,容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和故障提供强大的容错能力。
  • Ribbon:客户端负载均衡的服务调用组件。
  • Feign:基于Ribbon和Hystrix的声明式服务调用组件。
  • Zuul:服务网关,网关组件,提供智能路由、访问过滤的功能。
  • Archaius:外部化配置组件。

版本

Spring Cloud的版本命名比较特殊,因为它不是一个组件,而是许多组件的集合,它的命名是以A到Z为首字母的一些单词组成(其实是伦敦地铁站的名字)。

Spring Cloud入门

创建Spring Boot项目

创建一个简单的项目,方便以后使用。

创建父工程

创建一个父工程,打包为pom,后续的工程都以这个工程为父,使用Maven的聚合和继承。统一管理子工程的版本和配置。

添加以下jar包,例如:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.0.RELEASE</version>
        <relativePath/>
    </parent>
    
     <dependencyManagement>
        <dependencies>
            <!-- springCloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- 通用Mapper启动器 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>2.1.5</version>
            </dependency>
            <!-- mysql驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.46</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>

    </dependencies>
    
    <build>
        <plugins>
          <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
        </plugins>
    </build>
服务提供者

用于提供服务进行测试。

创建Module

在父类工程下创建子类,例如:user-service

添加以下jar包

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
 </dependencies>

步骤:

  1. 编写配置文件,例如:application.yml
server:
  port:8086
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db8?characterEncoding=utf8&useSSL=false
    username: root
    password: 123456
  
mybatis:
  type-aliases-package: com.sp.pojo
  mapper-locations: classpath:mapper/*.xml
  1. 编写实体类,例如:
@Data
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "t_id")
    private int id;
    @Column(name = "t_name")
    private String name;
    @Column(name = "age")
    private int age;
    private String sex;
    private String address;
    @Column(name = "telephone")
    private String phone;
}
  1. 编写mapper接口,例如:
@Repository
public interface UserMapper extends Mapper<User> {
}
  1. 编写service方法,例如:
public interface UserService {

    User findById(int id);
}

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User findById(int id) {
        return userMapper.selectByPrimaryKey(id);
    }
}
  1. 编写controller(RESTful风格)
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/findById/{id}")
    public User findById(@PathVariable("id")int id){
        return userService.findById(id);
    }
}

注:
  @RestController的作用等同于@Controller + @ResponseBody。 
  @Controller注解: 在一个类上添加@Controller注解,表明了这个类是一个控制器类。
  @ResponseBody 它的作用简短截说就是指该类中所有的API接口返回的数据,甭管你对应的方法返回Map或是其他Object,它会以Json字符串的形式返回给客户端。
  @GetMapping用于处理请求方法的GET类型
  1. 编写启动类
@SpringBootApplication
@MapperScan("com.sp.dao")
public class ServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceApplication.class,args);
    }
}
  1. 启动测试
服务调用者

在父类工程下创建一个子类,例如:user-client ,调用提供者的服务。

  1. 导入jar包,例如:
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
  1. 编写配置文件
server:
  port: 8087
  1. 编写启动器
@SpringBootApplication
public class ClientApplication {

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

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
  1. 编写控制器
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/find/{id}")
    public User find(@PathVariable("id") int id){
         String url = "http://localhost:8086/user/findById/" + id;
        return restTemplate.getForObject(url,User.class);
    }
}
  1. 启动测试
服务治理:Spring Cloud Eureka

服务治理

服务治理可以说是微服务架构中最为核心和基础的模块,主要用来实现各个微服务实例的自动化注册与发现。

  • **服务注册:**在服务治理框架中,通常都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。
  • **服务发现:**由于在服务治理框架下运作,服务间的调用不在通过指定具体的实例地址来实现,而是通过向服务发起请求调用实现。所以,服务调用方在调用服务提供接口方法的时候,并不知道具体的服务实例位置。

Netflix Eureka

Spirng Cloud Eureka使用Netflix Eureka来实现服务注册与发现。它既包含了服务端组件,也包含了客户端组件,并且服务端与客户端均采用java编写,所以Eureka主要适用于通过java实现的分布式系统,或是JVM兼容语言构建的系统。Eureka的服务端提供了较为完善的REST API,所以Eureka也支持将非java语言实现的服务纳入到Eureka服务治理体系中来,只需要其他语言平台自己实现Eureka的客户端程序。目前.Net平台的Steeltoe、Node.js的eureka-js-client等都已经实现了各自平台的Ereka客户端组件。

  • **Eureka服务端:**即服务注册中心。它同其他服务注册中心一样,支持高可用配置。依托于强一致性提供良好的服务实例可用性,可以应对多种不同的故障场景。
  • **Eureka客户端:**主要处理服务的注册和发现。客户端服务通过注册和参数配置的方式,嵌入在客户端应用程序的代码中。在应用程序启动时,Eureka客户端向服务注册中心注册自身提供的服务,并周期性的发送心跳来更新它的服务租约。同时,他也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期行的刷新服务状态。
搭建服务中心
  1. 在父类项目下创建一个名为eureka-server的子项目,并在pom.xml中添加以下内容:
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>      
 </dependency>
  1. 编写启动类
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class,args);
    }
}

注:

  • @EnableEurekaServer:启动一个服务注册中心提供给其他应用进行对话。
  1. 编写配置文件(application.yml)
server:
  port: 8090
spring:
  application:
    name: eureka-server
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8090/eureka
    register-with-eureka: false
    fetch-registry: false

注:

  • register-with-eureka:由于该应用注册中心,所以设置为false,代表不向注册中心注册自己。
  • fetch-registry:由于用注册中心的职责就是维护实例,它并不需要去检索服务,所以设置为false。
  1. 启动服务,并访问:http://localhost:8090/
注册服务提供者
  1. 在原来子项目user-servcie中的pom.xml添加以下内容:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  1. 在原来的启动类上添加@EnableDiscoveryClient 来开启Eureka客户端功能
@SpringBootApplication
@MapperScan("com.sp.dao")
@EnableDiscoveryClient
public class ServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceApplication.class,args);
    }
}
  1. 在配置文件中添加
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8090/eureka

注:

  • service-url:指定服务注册中心地址。
  1. 启动Eureka启动器,访问端口查询结果。查看注册是否成功。
服务发现
  1. 在子项目user-client中的pom.xml添加以下内容:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  1. 在原来的启动类上添加@EnableDiscoveryClient 来开启Eureka客户端功能,同上配置。
  2. 在配置文件中添加内容,同上配置。
  3. 修改控制器,用DiscoveryClient类的方法,根据服务名称,获取服务实例:
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("/find/{id}")
    public User find(@PathVariable("id") int id){
       
     List<ServiceInstance> serviceInstances = discoveryClient.getInstances("user-service");
        ServiceInstance serviceInstance = serviceInstances.get(0);
        String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/user/findById/"
                + id;
        System.out.println(serviceInstance.getHost());
        System.out.println(serviceInstance.getPort());
        return restTemplate.getForObject(url,User.class);

    }
}
  1. 启动测试。
Eureka客户端和服务端配置
  1. 服务提供者
  • 服务注册
    “服务提供者”在启动的时候会通过发送REST请求的方式将自己注册到Eureka Server上,同时带上了自身服务的一些元数据信息。
    在服务注册时,需开启注册服务:register-with-eureka: true
  • 服务同步
    当服务提供者发送注册请求到一个服务中心时,它会将该请求转发给集群中相连得其他注册中心,从而实现注册中心之间的服务同步。
  • 服务续约
    在注册服务完成后,服务提供者会维护一个心跳用来持续告诉Eureka Server:“我还活着”,以防止Eureka Server 的“剔除任务”将该服务实例从服务列表中排出,我们称该操作为服务续约。
    并进行以下配置:
eureka:
  instance:
    prefer-ip-address: true
    ip-address: 127.0.0.1
    #服务续约
    #seconds:服务失效时间,默认值90秒
    lease-expiration-duration-in-seconds: 120
    #服务续约(renew)的间隔,默认为30秒
    lease-renewal-interval-in-seconds: 20

注:

lease-expiration-duration-in-seconds:服务失效时间,默认为90秒。

lease-renewal-interval-in-seconds:服务续约任务的调整间隔时间,默认为30秒。

  1. 服务消费者
  • 获取服务
    发送一个REST请求给注册服务中心,来获取上面注册的服务清单。为了性能考虑,Eureka Server会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30秒更新一次。
    可进行如下配置:
eureka:
    fetch-registry: true
    registry-fetch-interval-seconds: 20

注:

registry-fetch-interval-seconds:更新缓存清单时间,默认为30秒。

  • 服务调用
    服务消费者在获取清单后,通过服务名可以获得具体提供服务的实例名和该实例的元数据。
  • 服务下线
    当服务实例进行正常的关闭操作时,它会触发一个服务下线的REST请求给Eureka Server’,告诉服务中心要下线了。服务端收到请求后,将该服务状态设置为下线。
  1. 服务注册中心
  • 失效剔除
    有时候,服务实例会因为一些原因而导致下线,而服务中心并未收到“服务下线”的请求。为了从服务列表中将这些无法提供的服务实例剔除,默认每隔一段时间(默认60秒)将清单中超时(默认90秒)的没有续约的服务剔除出去。
  • 自我保护
    当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服
    务实例的比例是否超过了85%,当EurekaServer节点在短时间内丢失过多客户端(可能发生了网络分区故障)。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
    可进行如下配置:
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
客户端负载均衡:Spring Cloud Ribbon

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用。

通过Spring Cloud Ribbon的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需要如下两部:

  • 服务提供者只需要启动多个服务实例并注册到一个注册中心或多个或多个相关联的服务注册中心。
  • 服务消费者直接通过调用被@LoadBalance注解修饰过的RsetTemplate来实现面向服务的接口调用。
RestTemplate使用
  1. 在消费者中修改启动器
@SpringBootApplication
@EnableDiscoveryClient
public class ClientApplication {

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

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
  1. 在控制器中进行使用
  • GET 请求
String url = "http://user-service/user/findById/" + id;
return restTemplate.getForObject(url,User.class);
  • POST请求
String url = "http://user-service/user/findById/"
User user = new User(1);
restTemplate.postForObject(url,user,String.class);
  • PUT请求
Long id = 1L;
User user = new User("张三",30);
restTemplate.put("http://user-service/user/{1}",user,id};
  • DELETE请求
Long id = 1L;
restTemple.delete(url,id);
  1. 例如:
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    RestTemplate restTemplate;
    
    
    @GetMapping("/find/{id}")
    public User find(@PathVariable("id") int id){
        String url = "http://user-service/user/findById/" + id;

        return restTemplate.getForObject(url,User.class);

    }
}
负载均衡策略
  • AbstractLoadBalancerRule(抽象类)
    该对象能够在具体实现选择服务策略,获取到一些负载均衡器中维护的信息来作为分配依据,并以此设计一些算法来实现针对特定场景的高效策略。
  • RandomRule
    该策略实现从服务实例中随机选择一个服务实例的功能。
  • RoundRoinRule
    该策略实现了安装线性轮询的方式依次选择每个服务实例的功能。
  • RetryRule
    该策略实现了一个具备重试机制的实例选择功能。
  • WeightedResposeTimeRule
    该策略是对RoundRoinRule的扩张,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例,以达到更优先的分配效果。
  • PredicateBasedRule
    抽象策略。先通过子类中实现的Predicate逻辑来过滤一部分服务实例,然后再以线性轮询的方式从过滤后实例清单中选出一个。
  • AvailabilityFilteringRule
    该策略承自上面介绍的抽象策略PredicateBasedRule,所以它也继承了“先过滤,在轮询选择”,过滤使用AvailabilityPredicate。
使用方式

SpringBoot也帮我们提供了修改负载均衡规则的配置入口,在消费者进行配置:

格式是: {服务名称}.ribbon.NFLoadBalancerRuleClassName ,值就是IRule的实现类。

user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

测试:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ClientApplication.class)
public class UserTest {

    @Autowired
    private RibbonLoadBalancerClient client;

    @Test
    public void test01(){
        for (int i = 0; i < 10; i++) {
            ServiceInstance instance = this.client.choose("user-service");
            System.out.println(instance.getHost() + ":" + instance.getPort());
        }
    }
}
服务容错保护:Spring Cloud Hystrix

Spring Cloud Hystrix实现了断路器、线程隔离等一系列服务保护功能。它也是基于Netfilx的开源框架Hystrix实现的,该框架的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供强大的容错能力。Hystrix具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。

快速入门
  1. 在消费者的pom.xml加入以下配置:
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
  1. 编写服务层接口,例如:
public interface UserService {

    String findById(int id);
}

@Service
@Slf4j
public class UserServiceImpl implements UserService{

    @Autowired
    private RestTemplate restTemplate;

    @Override
    @HystrixCommand(fallbackMethod = "queryByIdFallback")
    public String findById(int id) {
        String url = "http://user-service/user/findById/" + id;
        return restTemplate.getForObject(url,String.class);
    }

    public String queryByIdFallback(int id) {
        log.error("查询用户信息失败。id:{}", id);
        return "对不起,网络太拥挤了!";
    }
}

注:

  • @HystrixCommand(fallbackMethod = "queryByIdFallback"):用来声明一个降级逻辑的方法
  1. 编写控制器,例如:
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;

    @GetMapping("/find/{id}")
    public String find(@PathVariable("id") int id){

        return userService.findById(id);

    }
}
  1. 在启动器中加入:
@SpringCloudApplication
public class ClientApplication {

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

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

注:

  • @SpringCloudApplication
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
  1. 关闭服务器,启动测试
工作流程
  1. 创建HystrixCommand或HystrixObservableCommand对象
    首先,构建一个HystrCommand或是HystrixObservableCommand对象,用来表示对依赖服务的操作请求,同时传递所有需要的参数。采用“命令模式”来实现服务调用操作封装。
  • HystrixCommand:用在依赖的服务返回单个操作结果的时候。
  • HystrixObservableCommand:用在依赖的服务返回多个操作结果的时候。
  1. 命令执行
    Hystrix在执行时会根据创建的Command对象及具体情况来选择一个执行。
  2. 结果是否被缓存
    若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。
  3. 断路器是否打开
  • 如果断路器是打开,那么Hystrix不会执行命令,而是转接到fallback处理逻辑。
  • 如果断路器是关闭,那么Hystrix跳转到第五步,检查是否有可以用来资源执行命令。
  1. 线程池/请求队列/信号量是否占满
    如果与命令相关的线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满,那么Hystrix也不会执行命令,而是转到fallback处理逻辑。
  2. HystrixObservableCommand.construct()或HystrixCommand.run()
    Hystrix会根据我们编写的方法来决定采取什么样的方法去请求依赖服务。
  • HystrixCommand.run():返回一个单一的结果,或抛出异常。
  • HystrixObservableCommand.construct():返回一个Observable对象来发射多个结果,或通过onError发送错误通知。
  1. 计算断路器的健康值
    Hystrix会将“成功”、“失败”、“拒绝”、“超时”等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些数据来决定是否将断路器打开,来对某个依赖服务的请求进行“断路/短路”,直到恢复期结束。
  2. fallback处理
    当命令执行失败的时候,Hystrix会进入fallback尝试回退处理,我们通常也称该操作为“服务降级”。而引起服务降级处理的情况有下几种:
  • 当前命令处于“熔断/短路”状态,断路器打开。
  • 线程池、请求队列或信号量被占满。
  • HystrixObservableCommand.construct()或HystrixCommand.run()抛出异常的时候。
  1. 返回成功响应
    当Hystrix命令执行成功后,他会将处理结果直接返回或以Observable的形式返回。
断路器原理

状态机有3个状态:

  • Closed:关闭状态(断路器关闭),所有请求都正常访问。
  • Open:打开状态(断路器打开),所有请求都会被降级。Hystrix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。
  • Half Open:半开状态,不是永久的,断路器打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会关闭断路器,否则继续保持打开,再次进行休眠计时
常用配置
(1)熔断触发最小请求次数,默认值是20
hystrix.command.default.circuitBreaker.requestVolumeThreshold=10
(2)熔断后休眠时长,默认值5秒
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=10000
(3)触发熔断错误比例阈值,默认值50%
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
(4)服务降级超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
声明式服务调用:Spring Cloud Feign

Spring Cloud Feign基于Netfix Feign实现,整合了Spring Cloud Ribbon 与 Spring Cloud Hystrix,除了提供两者的强大功能之外,还提供了一中声明式的Web服务客户端定义方式。

Spring Cloud Fegin在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义。在Spring Cloud Feign的实现下,我们只需创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了在使用Spring Cloud Ribbon时自行封装服务调用客户端的开发量。

Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。

快速入门
  1. 创建一个子类springboot项目,在pom.xml中添加以下内容:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
 <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
 </dependency>
  1. 编写业务层,例如:
@FeignClient("user-service")
public interface UserService {

    @GetMapping("/user/findById/{id}")
    User find(@PathVariable("id")int id);

}
  • 首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟Mybatis的mapper很像
  • @FeignClient ,声明这是一个Feign客户端,同时通过 value 属性指定服务名称接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果
  • @GetMapping中的/user,请不要忘记;因为Feign需要拼接可访问的地址
  1. 编写控制层,例如:
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;

    @GetMapping("/get/{id}")
    public User getById(@PathVariable("id")int id){
        return userService.find(id);
    }
}
  1. 编写配置文件,例如:
server.port=8088

spring.application.name=user-feign

eureka.client.service-url.defaultZone=http://127.0.0.1:8090/eureka
eureka.client.fetch-registry=true
eureka.client.registry-fetch-interval-seconds=30
  1. 编写启动器,例如:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(FeignApplication.class,args);
    }
}
  1. 启动测试
Ribbon 配置
全局配置

直接使用ribbon.<key>=<value>的方式来设置ribbon的各项参数。例如:

ribbon.ReadTimeout=5000
ribbon.ConnectTimeout=1000
指定服务配置

采用<client>.ribbon.key=value的格式进行设置。例如:

user-service.ribbon.ReadTimeout=2000 # 读取超时时长
user-service.ribbon.ConnectTimeout=1000 # 建立链接的超时时长

一些配置

ribbon:
ConnectTimeout: 1000 # 连接超时时长
ReadTimeout: 2000 # 数据通信超时时长
MaxAutoRetries: 0 # 当前服务器的重试次数
MaxAutoRetriesNextServer: 0 # 重试多少次服务
OkToRetryOnAllOperations: false # 是否对所有的请求方式都重试
Hystrix配置
全局配置

开启断路功能:feign.hystrix.enabled=true

(1)熔断触发最小请求次数,默认值是20
hystrix.command.default.circuitBreaker.requestVolumeThreshold=10
(2)熔断后休眠时长,默认值5秒
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=10000
(3)触发熔断错误比例阈值,默认值50%
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
(4)服务降级超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
请求压缩

Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:

feign:
compression:
request:
enabled: true # 开启请求压缩
response:
enabled: true # 开启响应压缩

同时,我们也可以对请求的数据类型,以及触发压缩的大小下限进行设置:

feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
min-request-size: 2048 # 设置触发压缩的大小下限
日志级别

在配置文件下配置:

logging.level.com.sp=debug

在启动类中加入:

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

这里指定的Level级别是FULL,Feign支持4种级别:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
网关:Spring Cloud Gateway

Spring Cloud Gateway是Spring官网基于Spring 5.0、 Spring Boot 2.0、Project Reactor等技术开发的网关服务。
Spring Cloud Gateway基于Filter链提供网关基本功能:安全、监控/埋点、限流等。
Spring Cloud Gateway为微服务架构提供简单、有效且统一的API路由管理方式。
Spring Cloud Gateway是替代Netflix Zuul的一套解决方案。

Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护。Spring Cloud Gateway本身也是一个微服务,需要注册到Eureka服务注册中心。

核心概念

  • 路由(route) 路由信息的组成:由一个ID、一个目的URL、一组断言工厂、一组Filter组成。如果路由断言为真,说明请求URL和配置路由匹配。
  • **断言(**Predicate) Spring Cloud Gateway中的断言函数输入类型是Spring 5.0框架中的ServerWebExchange。Spring Cloud Gateway的断言函数允许开发者去定义匹配来自于HTTP Request中的任何信息比如请求头和参数。
  • 过滤器(Filter) 一个标准的Spring WebFilter。 Spring Cloud Gateway中的Filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理
快速入门
  1. 创建一个子类项目,名为gateway-server,在pom.xml中添加以下内容:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>
  1. 创建启动器,例如:
@SpringBootApplication
@EnableDiscoveryClient
public class GateWayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GateWayApplication.class,args);
    }
}
  1. 编写配置文件,例如:
server:
  port: 8089
eureka:
  client:
    service-url:
     defaultZone: http://127.0.0.1:8090/eureka
  instance:
    prefer-ip-address: true
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        - ip: user-route
          uri: http://127.0.0.1:8086
          predicates:
            - Path=/user/**

注:

  • ip:路由id,可以随意写
  • uri:代理的服务地址
  • predicates:路由断言,可以配置映射路径
  • Path:请求路径,例如:路径中包含有 /user/** 开头的请求
  1. 启动测试,输入:localhost:8089/user/findById/3进行测试。
localhost:8089/user/findById/3->localhost:8086/user/findById/3
面向服务的路由
修改映射配置,通过服务名称获取
cloud:
    gateway:
      routes:
        - ip: user-route
          uri: lb://user-service
          predicates:
            - Path=/user/**

注:

路由配置中uri所用的协议为lb时(以uri: lb://user-service为例),gateway将使用 LoadBalancerClient把user-service通过eureka解析为实际的主机和端口,并进行ribbon负载均衡。

路由前缀

客户端的请求地址与微服务的服务地址如果不一致的时候,可以通过配置路径过滤器实现路径前缀的添加和去除。

  • 添加前缀:对请求地址添加前缀路径之后再作为代理的服务地址。
    例如:
    http://127.0.0.1:8089/findById/3 --> http://127.0.0.1:8089/user/findById/3 添加前缀路径/user
predicates:
      - Path=/**
 filters:
      - PrefixPath=/user

注:

PrefixPath:添加请求路径的前缀

  • 去除前缀:将请求地址中路径去除一些前缀路径之后再作为代理的服务地址。
    例如:
    http://127.0.0.1:8089/api/user/findById/3 -->http://127.0.0.1:8089/user/findById/3
predicates:
     - Path=/api/user/**
filters:
     - StripPrefix=1

注:

StripPrefix:表示过滤1个路径,2表示两个路径,以此类推

过滤器

Gateway作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作往往是通过网关提供的过滤器来实现的。前面的 路由前缀 章节中的功能也是使用过滤器实现的。

配置全局默认过滤器
default-filters:
        - AddResponseHeader=X-Response-Foo, Bar
        - AddResponseHeader=abc-myname,lwx

注:

过滤器类型:Gateway实现方式上,有两种过滤器:

  • 局部过滤器:通过 spring.cloud.gateway.routes.filters 配置在具体路由下,只作用在当前路由上;如果配置spring.cloud.gateway.default-filters 上会对所有路由生效也算是全局的过滤器;但是这些过滤器 的实现上都是要实现GatewayFilterFactory接口。
  • 全局过滤器:不需要在配置文件中配置,作用在所有的路由上;实现 GlobalFilter 接口即可。
自定义过滤器

例如:

@Component
public class MyParamGatewayFilterFactory extends
AbstractGatewayFilterFactory<MyParamGatewayFilterFactory.Config> {
static final String PARAM_NAME = "param";
public MyParamGatewayFilterFactory() {
super(Config.class);
}
public List<String> shortcutFieldOrder() {
return Arrays.asList(PARAM_NAME);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// http://localhost:10010/api/user/8?name=lxs config.param ==> name
//获取请求参数中param对应的参数名 的参数值
ServerHTTPRequest request = exchange.getRequest();
if(request.getQueryParams().containsKey(config.param)){
request.getQueryParams().get(config.param).
forEach(value -> System.out.printf("------------局部过滤器--------%s = %s-
-----", config.param, value));
}
return chain.filter(exchange);
};
}
public static class Config{
//对应在配置过滤器的时候指定的参数名
private String param;
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
}
}

修改配置文件

filters:
# 表示过滤1个路径,2表示两个路径,以此类推
- StripPrefix=1
# 自定义过滤器
- MyParam=name
自定义全局过滤器

例如:

@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("--------------全局过滤器MyGlobalFilter------------------");
String token = exchange.getRequest().getHeaders().getFirst("token");
if(StringUtils.isBlank(token)){
//设置响应状态码为未授权
exchange.getResponse().setStatusCode(HTTPStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
//值越小越先执行
return 1;
}
}
Gateway与Feign的区别
  • Gateway 作为整个应用的流量入口,接收所有的请求,如PC、移动端等,并且将不同的请求转- 发至不同的处理微服务模块,其作用可视为nginx;大部分情况下用作权限鉴定、服务端流量控制。
  • Feign 则是将当前微服务的部分服务接口暴露出来,并且主要用于各个微服务之间的服务调用。
分布式配置中心:Spring Cloud Config
简介

Spring Cloud Config项目是一个解决分布式系统的配置管理方案。它包含了Client和Server两个部分,server提供配置文件的存储、以接口的形式将配置文件的内容提供出去,client通过接口获取数据、并依据此数据初始化自己的应用。Config,它支持配置文件放在配置服务的本地,也支持放在远程Git仓库(GitHub、码云)。

快速入门
Git配置管理

知名的Git远程仓库有国外的GitHub和国内的码云(gitee)。建议使用码云。

地址:https://gitee.com/

  1. 创建远程仓库
    首先要使用码云上的私有远程git仓库需要先注册帐号;请先自行访问网站并注册帐号,然后使用帐号登录码云控制台并创建公开仓库。
  • 自行创建仓库名称
  • “是否开源”选择“公开”
  • 语言选自“Java”
  • 使用Readme文件初始化
  1. 创建配置文件
    在新建的仓库中创建需要被统一配置管理的配置文件。
    配置文件的命名方式:{application}-{profile}.yml 或 {application}-{profile}.properties
  • application为应用名称
  • profile用于区分开发环境,测试环境、生产环境等

例如:user-dev.yml

搭建配置中心微服务
  1. 创建项目
    创建一个子项目工程,名为config-server,并在pom.xml中加入以下内容:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
  1. 创建启动类,例如:
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {

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

注:

@EnableConfigServer:开启配置服务

  1. 编写配置文件,例如:
erver:
  port: 8095
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/JD9J9WRJF9JQW9EED9/my-config.git
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8090/eureka

注:

  • uri:配置Git仓库位置
  1. 启动测试
    启动eureka注册中心和配置中心;然后访问http://localhost:8095/user-dev.yml ,查看能否输出在码云存储管理的user-dev.yml文件。并且可以在gitee上修改user-dev.yml然后刷新上述测试地址也能及时到最新数据。
获取配置中心配置

将user-service项目进行改造,配置文件将从配置中心获取,不在由微服务提供。

  1. 在user-service项目的pom.xml中添加以下内容:
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
  1. 修改配置文件
    删除原来项目中的application.yml,创建一个bootstrap.yml,在里面添加以下内容:
spring:
  cloud:
    config:
      name: user
      profile: dev
      label: master
      discovery:
        enabled: true
        service-id: config-server
  
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8090/eureka

注:

  • name: 要与仓库中的配置文件的application保持一致
  • profile:要与仓库中的配置文件的profile保持一致
  • label:要与仓库中的配置文件所属的版本(分支)一样
  • enabled:使用配置中心
  • service-id:配置中心服务名
  1. 启动测试
    启动注册中心 eureka-server 、配置中心 config-server 、用户服务 user-service ,如果启动没有报错其实已经 使用上配置中心内容,可以到注册中心查看,也可以检验 user-service 的服务。
服务总线:Spring Cloud Bus
简介

Spring Cloud Bus 使用轻量级的消息代理来连接微服务架构中的各个服务,可以将其用于广播状态更改(例如配置中心配置更改)或其他管理指令。

通常会使用消息代理来构建一个主题,然后把微服务架构中的所有服务都连接到这个主题上去,当我们向该主题发送消息时,所有订阅该主题的服务都会收到消息并进行消费。

Spring Cloud Bus是用轻量的消息代理将分布式的节点连接起来,可以用于广播配置文件的更改或者服务的监控管理。也就是消息总线可以为微服务做监控,也可以实现应用程序之间相互通信。 Spring Cloud Bus可选的消息代理有RabbitMQ和Kafka。

快速入门
修改远程Git配置

修改在码云上的user-dev.yml文件,添加一个属性test.name 。例如:

test:
  name: lwx
修改项目
  1. 修改user-service中的控制器,例如:
@RestController
@RefreshScope
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @Value("${test.name}")
    private String name;

    @GetMapping("/findById/{id}")
    public User findById(@PathVariable("id")int id){
        System.out.println("配置文件中的test.name为:" + name);
        return userService.findById(id);
    }
}
  1. 启动测试
    依次启动注册中心 eureka-server 、配置中心 config-server 、用户服务 user-service ;然后修改Git仓库中的配置信息,访问用户微服务,查看输出内容。
安装软件

安装otp_win64_23.0.exe和rabbitmq-server-3.8.5.exe

改造配置中心

在 config-server 项目的pom.xml文件中加入Spring Cloud Bus相关依赖

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-bus</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>

在 config-server 项目修改application.yml文件如下:

#配置rabbitmq信息;如果是都与默认值一致则不需要配置
rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    
management:
  endpoints:
    web:
      exposure:
      # 暴露触发消息总线的地址
        include: bus-refresh
改造用户服务
  1. 在用户微服务 user-service 项目的pom.xml中加入Spring Cloud Bus相关依赖:
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-bus</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 修改 user-service 项目的bootstrap.yml如下
rabbitmq:
    host: localhost
    username: guest
    password: guest
    port: 5672
  1. 改造用户微服务 user-service 项目的UserController
@RestController
@RefreshScope
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @Value("${test.name}")
    private String name;

    @GetMapping("/findById/{id}")
    public User findById(@PathVariable("id")int id){
        System.out.println("配置文件中的test.name为:" + name);
        return userService.findById(id);
    }
}
  1. 测试
第一步:依次启动注册中心 eureka-server 、配置中心 config-server 、用户服务 user-service
第二步:访问用户微服务http://localhost:8086/user/findById/7;查看IDEA控制台输出结果
第三步:修改Git仓库中配置文件 user-dev.yml 的 test.name 内容
第四步:使用Postman或者RESTClient工具发送POST方式请求访问地址
http://127.0.0.1:8095/actuator/bus-refresh