如果一个个的 Bean 使用注解 @Bean 注入 Spring IoC 容器中,那将是一件很麻烦的事情。好在 Spring 还允许我们进行扫描装配 Bean 到 IoC 容器中,那将是一件很麻烦的事情。好在 Spring 还允许我们进行扫描装配 Bean 到 IoC 容器中,对于扫描装配而言使用的注解是 @Component 和 @ComponentScan。 @Component 是标明哪个类被扫描进入 Spring IoC 容器,而@ComponentScan 则是标明采用何种策略去扫描装配 Bean。
@Service(该标准注入了@Component,所以在默认的情况下它会被 Spring 扫描装配到 IoC 容器中)。
通过@ComponentScan注解能够定义扫描哪些包。但是需要特别注意的是,它提供的 exclude 和 excludeName 两个方法是对于其内部的自动配置类才会生效。为了能够排除其他类,还可以再加入 @ComponentScan 以达到我们的目的。例如,扫描 User 而不扫描 UserService,可以把启动配置文件写成:
@SpringBootApplication
@ComponentScan(basePackages={"com.springboot.chapter3"},excludeFilters={@Filter(classes=Service.class)})
现实的 Java 的应用往往需要引入许多来自第三方的包,并且很可能希望把第三方包的类对象也放入到 Spring IoC 容器中,这时 @Bean 注解就可以发挥作用了。
例如,要引入一个 DBCP 数据源,我们现在 pom.xml 上加入项目所需要 DBCP 包和数据库 MySQL 驱动程序的依赖。
这样 DBCP 和数据库驱动就被加入到了项目中,接着将使用它提供的机制来生成数据源。
代码:使用 DBCP 生成数据源
@Bean(name = "dataSource")
public DataSource getDataSource(){
Properties props = new Properties();
props.setProperty("driver","com.mysql.jdbc.Driver");
props.setProperty("url","jdbc:mysql:localhost:3306/chapter3");
props.setProperty("password","123456");
DataSource dataSource = null;
try{
dataSource = BasicDataSourceFactory.createDataSource(props);
}catch(Exception e){
e.printStackTrace();
}
return dataSource;
}
这里通过 @Bean 定义了其配置项 name 为“DataSource”,那么 Spring 就会把它返回的对象用名称 “DataSource” 保存在 IoC 容器中。当然,你可以不填写这个名称,那么它就会用你的方法民称作为 Bean 名称保存到 IoC 容器中。通过这样,就可以将第三方包的类装配到 Spring IoC 容器中。
@Autowired 是我们使用的最多的注解之一,因此在这里需要进一步地探讨它。它注入的机制最基本的一条是根据类型(by type)。
@Autowired 首先会根据类型找到对应的 Bean,如果对应类型的 Bean 不是唯一的,那么它会根据其属性名称和 Bean 的名称进行匹配。如果匹配的上,就会使用该 Bean;如果还无法匹配,就会抛出异常。
@Autowired 是一个默认必须找到对应 Bean 的注解,如果不能确定其标注属性一定会存在并且允许这个被标注的属性为 null,那么你可以设置 @Autowired 属性 required 为 false。 @Autowired(required = false)
@Primary 的含义是告诉 Spring IoC 容器,当发现有多个同样类型的 Bean 时,优先使用进行注入。
@Quelifier 与 @Autowired 组合在一起,通过类型和名称一起找到 Bean。Bean 名称在 Spring IoC 容器中是唯一的标识,通过这个可以消除歧义性。
Spring IoC 初始化和销毁 Bean 的过程,这便是 Bean 的生命周期的过程,它大致分为 Bean 定义、Bean 的初始化、Bean的生存期和 Bean 的销毁4个部分。
Bean定义
a. Spring 通过我们的配置,如 @ComponentScan 定义的扫描路径去找到带有 @Component 的类,这个过程就是一个资源定位的过程。
b. 一旦找到了资源,那么它就开始解析,并且将定义的信息保存起来。注意,此时还没有初始化 Bean,也就是没有 Bean 的实例,它有的仅仅是 Bean 的定义。
c. 然后就会把 Bean 定义发布到 Spring IoC 容器中。此时, IoC容器也只有 Bean 的定义,还是没有 Bean 的实例生成。
Spring Bean 的初始化流程
1. 资源定位(例如 @ComponentScan 所定义的扫描包)
2. Bean 定义(将 Bean 的定义保存到 BeanDefinition 的实例中)
3. 发布 Bean 定义(IoC容器装载 Bean 定义)
4. 实例化(创建 Bean 的实例对象)
5. 依赖注入(DI) 例如 @Autowired 注入的各类资源
初始化 ——> 依赖注入 ——> setBeanName 方法(接口 BeanNameAware)——> setBeanFactory 方法(接口 BeanFactoryAware)——> setApplicationContext方法(接口 ApplicationContextAware,需要容器实现 ApplicationContext接口才会被调用)——> postProcessBeforeInitialization方法(BeanPostProcessor 的预初始化方法,注意,它是针对全部 Bean 生效)——> 自定义初始化方法(@PostConstruct标注方法)——>afterPropertiesSet 方法(接口InitializingBean)——>postProcessAfterInitialization 方法(BeanPostProcessor的后初始化方法,注意,它是针对全部 Bean 生效)——> 生存期——> 自定义销毁方法(@PreDestroy 标注方法)——> destroy 方法(接口 DisposableBean)
有时候 Bean 的定义可能使用的是第三方的类,此时可以使用注解 @Bean 来配置自定义初始化和销毁方法:
@Bean(initMethod = "init",destroyMethod="destroy")
在一般的容器中,Bean 都会存在单例 Singleton 和原型 Prototype 两种作用域,而在 Web 容器中,则存在 页面(page)、请求(request)、会话(session)和应用(application)4种作用域。
singleton 所有Spring 应用 默认值,IoC容器只存在单例
prototype 所有Spring 应用 每当从IoC容器中取出一个Bean,则创建一个新的Bean
session Spring Web 应用 HTTP 会话
application Spring Web 应用 Web工程生命周期
request Spring Web 应用 Web工程单次请求
AOP 最为典型的应用实际就是数据库事务的管控。 就是要么一起成功,要么一起失败,这样 OOP 就无能为力了。
AOP还可以减少大量重复的工作。在Spring流行之前,我们可以使用JDBC代码实现很多的数据库操作,例如,插入一个用户的信息,我们可以用 JDBC 代码来实现。我们获取数据库事务连接、事务操控和关闭数据库连接的过程,都需要使用大量的 try...catch...finally...语句去操作,这显然存在大量重复的工作。是否可以替换这些没有必要的重复的工作呢?答案是肯定的。
使用 Spring AOP 可以处理一些无法使用 OOP 实现的业务逻辑。其次,通过约定,可以将一些业务逻辑织入流程中,并且可以将一些通用的逻辑抽取出来,然后给予默认实现,这样你只需要完成部分的功能就可以了。
Spring AOP :切点 连接点 切面
全映射框架 Hibernate,在以管理系统为主的时代,它的模型化十分有利于公司业务的分析和理解,但是在近年兴起的移动互联网时代,这样的模式却走到了尽头。Hibernate 的模式重模型和业务分析,移动互联网虽然业务相对简单,但却更关注大数据和大并发下的性能问题。全表映射规则下的Hibernate无法满足 SQL 优化和互联网灵活多变的业务,于是 Hibernate 近年来受到新兴持久框架 MyBatis 的严重冲击。
MyBatis 是一个不屏蔽 SQL 且提供动态 SQL、接口式编程和简易SQL 绑定POJO 的半自动化框架,它的使用十分简单,而且能非常容易定制 SQL,以提高网站性能,因此在移动互联网兴起的时代,它占据了强势的地位。
1. 引入 spring-boot-starter-data-jpa 的依赖
2. 配置数据源
spring.datasource.url = jdbc:mysql://localhost:3306/chapter5
spring.datasource.username = root
spring.datasource.password = 123456
#spring.datasource.driver-class-name = com.mysql.jdbc.Driver
#最大等待连接中的数量,设 0 为没有限制
spring.datasource.tomcat.max-idle =10
#最大连接活动数
spring.datasource.tomcat.max-active=50
#最大等待毫秒数,单位为ms,超过时间会出错误信息
spring.datasource.tomcat.max-wait = 10000
#数据库连接池初始化连接数
spring.datasource.tomcat.initial-size = 5
MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。
MyBatis 的配置文件包括两个大的部分,一是基础配置文件,一个是映射文件。在 MyBatis 中也可以使用注解来实现映射,只是由于功能和可读性的限制,在实际的企业中使用得比较少,因此本书不介绍使用注解配置SQL的方式。
用户映射文件(userMapper.xml)
在 Spring 中,数据库事务是通过 AOP 技术来提供服务的。在 JDBC 中存在着大量的 try...catch...finally...语句,也同时存在着大量的冗余代码,如那些打开和关闭数据库连接的代码以及事务回滚的代码。
在 Spring 数据库事务中可以使用编程式事务,也可以使用声明式事务。大部分的情况下,会使用声明式事务。编程式事务这种比较底层的方式已经基本被淘汰了,Spring Boot 也不推荐我们使用,因此这里不再讨论编程式事务。
Spring 可以使用 JDK 动态代理,也可以使用 CGLIG 动态代理。
@Transactional 注解
ACID : 原子性 一致性 隔离性 持久性
传播行为是方法之间调用事务采取的策略问题。在绝大部分的情况下,我们会认为数据库事务要么全部成功,要么全部失败。但现实中也许会有特殊的情况。例如,执行一个批量程序,它会处理很多的交易,绝大部分交易是可以顺利完成的,但是也有极少数的交易因为特殊原因不能完成而发生异常,这时我们不应该因为极少数的交易不能完成而回滚批量任务调用的其他交易,使得那些本能完成的交易也变为不能完成了。此时,我们真实的需求是,在一个批量任务执行的过程中,调用多个交易时,如果有一些交易发生异常,只是回滚那些出现异常的交易,而不是整个批量任务,这样就能够使得那些没有问题的交易可以顺利完成,而有问题的交易则不做任何事情。
在 Spring 中,当一个方法调用另外一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。当前方法调用子方法的时候,让每一个子方法不在当前事务中执行,而是创建一个新的事务去执行子方法,我们就说当前方法调用子方法的传播行为为新建事务。此外,还可能让子方法在无事务、独立事务中执行,这些完全取决于你的业务需求。
在 Spring 事务机制中对数据库存在 7 种传播行为,它是通过枚举类定义的。常用的传播行为有三种:
a. REQUIRED 需要事务,它是默认传播行为,如果当前存在事务,就沿用当前事务,否则新建一个事务运行子方法。
b. REQUIRES_NEW 无论当前事务是否存在,都会创建新事务运行方法,这样新事务就可以拥有新的锁和隔离级别等特性,与当前事务相互独立。
c. NESTED 在当前方法调用子方法时,如果子方法发生异常,只回滚子方法执行过的 SQL,而不回滚当前方法的事务。
@ Transactional在某些场景下会失效,这是要注意的问题。
Spring 数据库事务的约定,其实现原理是 AOP,而 AOP 的原理是动态代理,在自调用的过程中,是类自身的调用,而不是代理对象去调用,那么就不会产生 AOP,这样 Spring 就不能把你的代码织入到约定的流程中,于是就产生了失败场景。为了克服这个问题,我们可以用一个 Serivice 去调用另一个 Service,这样就是代理对象的调用,Spring 会将你的代码织入事务流程。
Redis 是一种运行在内存的数据库,支持 7 种数据类型的存储。Redis 是一个开源、使用 ANSIC 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、键值数据库,并提供多种语言的 API。Redis 是基于内存的,所以运行速度很快,大约是关系数据库的几倍到几十倍的速度。
Redis 在 2.6 版本之后开始增加 Lua 语言的支持,这样 Redis 的运算能力就大大提高了,而且在 Redis 中 Lua 语言的执行是原子性的,也就是在 Redis 执行 Lua 时,不会被其他命令所打断,这样就能够保证在高并发场景下的一致性。
还需要讨论的技术:分布式事务、分布式数据一致性、消息总线等。
Spring Cloud 是一套组件,可以细分为多种组件,如服务发现、配置中心、消息总线、负载均衡、断路器和数据监控等。限于篇幅,只介绍下面的最基础的技术。
在 Spring Clound 中主要是使用 Netflix Eureka 作为服务治理的,Spring Cloud 对其进行了一次封装,使得开发者可以以 Spring Boot 的风格使用它,这样就为它的使用带来了极大的便利。通过服务注册将单个微服务节点注册给服务治理中心,这样服务治理中心就可以治理单个微服务节点。服务发现则是微服务节点可以对服务治理中心发送消息,使得服务治理中心可以将新的微服务节点纳入管理。
在微服务的开发中,会将一个大的系统拆分为多个微服务系统,而各个微服务系统之间需要相互协作才能完成业务需求。每一个微服务系统可能存在多个节点,当一个微服务(服务消费者)调用另外一个微服务(服务提供者)时,服务提供者需要负载均衡算法提供一个节点进行响应。而负载均衡是分布式必须实施的方案,例如,系统在某个时刻存在 3 万笔业务请求,使用单点服务器就很可能出现超负载,导致服务器瘫痪,进而使得服务不可用。而使用3台服务节点后,通过负载均衡的算法使得每个节点能够比较平均地分摊请求,这样每个点服务只是需要处理 1 万笔请求,这样就可以分摊服务的压力,及时响应。除此之外,在服务的过程中,可能出现某个节点故障的风险,通过均衡负载的算法就可以将故障节点排除,使后续请求分散到其他可用节点上,这就体现了 Spring Cloud 的高可用。 Spring Cloud 为此提供了 Ribbon 来实现这些功能,主要使用的就是在第 11 章中谈到的 RestTemplate。
对于 REST 风格的调用,如果使用 RestTemplate 会比较烦琐,可读性不高。为了简化多次调用的复杂度,Spring Cloud 提供了接口式的声明服务调用编程,它就是 Feign。通过它请求其他微服务时,就如同调度本地服务的 Java 接口一样,从而在多次调用的情况下可以简化开发者的编程,提高代码的可读性。
在分布式中,因为存在网络延迟或者故障,所以一些服务调用无法及时响应。如果此时服务消费者还在大量地调用这些网络延迟或者故障的服务提供者,那么很快消费者也会因为大量的等待,造成积压,最终导致其自身出现服务瘫痪。为了克服这个问题,Spring Cloud 引入了 Netflix 的开源框架 Hystrix 来处理这些问题。当服务提供者响应延迟或者故障时,就会使得服务消费者长期得不到响应,Hystrix 就会对这些延迟或者故障的服务进行处理。这如同电路负荷过大,保险丝会烧毁从而保障用电安全一样,于是大家就形象地称之为断路器。这样,当服务消费者长期得不到服务提供者响应时,就可以进行降级、服务断路、线程和信号隔离、请求缓存或者合并等处理,这些较为复杂,为了节省篇幅,本书只讨论最常用降级的使用。
在 Spring Cloud 中 API 网关是 Zuul。对于网关而言,存在两个作用:第一个作用是将请求的地址映射为真实服务器的地址,例如,用户请求
http://localhost/user/1 去获取用户 id 为 1 的信息,而真实的服务是 http://localhost:8001/user/1 和 http://localhost:8002/user/1 都可以获取用户的信息,这时就可以通过网关使得 localhost/user 映射为对应真实服务器的地址。显然这个作用就起到路由分发的作用,从而降低单个节点的负载。从这点来说,可以把它称为服务端负载均衡。从高可用的角度来说,则一个请求地址可以映射到多台服务上,如果单点出现故障,则其他节点也能提供服务,这样这就是一个高可用的服务了。Zuul 网关的第二个作用是过滤服务,在互联网中,服务器可能面临各种攻击,Zuul 提供了过滤器,通过它过滤那些恶意或者无效的请求,把他们排除在服务网站之外,这样就可以降低网站服务的风险。
首先需要将产品和用户这两个服务注册给服务治理中心,让服务治理中心能够管理它们,而且每一个服务包括服务治理中心都存在两个节点,这样在请求大的时候服务就可以有两个节点共同承担。
Spring Cloud 的服务治理是使用 Netflix 的 Eureka 作为服务治理器的,它是我们构建 Spring Cloud 分布式最为核心和最为基础的模块,它的作用是注册和发现各个 Spring Boot 微服务,并且提供监控和管理的功能。搭建服务治理节点并不是很复杂,甚至可以说十分简单,为此先搭建一个注册中心。首先是新建工程,如 chapter 17-server,然后通过 Maven 引入对应的 jar 包。
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
这样就引入了 Eureka 模块的包。然而要启用它只需要在 Spring Boot 的启动文件上加入注解 @EnableEurekaServer 便可以了。
package com.springboot.chapter17.server.main;
@SpringBootApplication
@EnableEurekaServer
public class Chapter17ServerApplication{
public static void main(String [] args){
SpringApplication.run(Chapter17ServerApplication.class,args);
}
}
有了这个注解,就意味着 Spring Boot 会启动 Eureka 模块。我们还需要进一步配置 Eureka 模块的一些基本内容,为此可以使用 application.properties 进行配置(在一些服务可能会使用 yml 文件进行配置)。
# Spring 项目名称
spring.application.name = server
# 服务器端口
server.port = 7001
# Eureka 注册服务器名称
eureka.instance.hostname = localhost
# 是否注册给服务中心
eureka.client.register-with-eureka = false
# 是否检索服务
eureka.client.fetch-registry = false
# 治理客户端服务域
eureka.client.serviceUrl.defaultZone = http://localhost:7001/eureka/
注册服务会用到服务发现。这里新建一个 Spring Boot 的工程,取名 chapter17-product,并且通过 Maven 引入服务发现相关的包。
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
服务发现配置: @EnableDiscoveryClient
具体注册到哪个服务治理中心则需要自己配置。这里使用 application.properties 进行配置。
配置新的服务治理中心节点
# Spring 应用名称
spring.application.name = server
# 端口
server.port = 7001
# 服务治理中心名称
eureka.instance.hostname = localhost
# 将当前服务治理中心注册到 7002 端口的服务治理中心
eureka.client.serviceUrl.defaultZone = http://localhost:7002/eureka/
配置 7002 端口服务治理中心节点
# 微服务名称依旧保持不变
spring.application.name = server
server.port = 7002
eureka.instance.hostname = localhost
# 将 7002 端口服务治理中心,注册给 7001 端口服务治理中心
eureka.client.serviceUrl.defaultZone = http://localhost:7001/eureka/
这里的关键是 spring.application.name 依旧配置为 server,而端口修改为了 7002。与此同时,配置的注册微服务治理域为 7001端口,这样就可以使得 7002 端口的服务治理中心被注册到 7001 的服务治理中心。
两个服务治理中心是通过相互注册来保持相互监控的,关键点是属性 spring.application.name 保持一致都为 server,这样就可以形成两个甚至多个服务治理中心。
将应用注册到多个服务治理中心
# 服务器端口
server.port = 9001
# Spring 服务名称
spring.application.name = product
# 注册多个治理客户端服务域
eureka.client.serviceUrl.defaultZone = http://localhost:7001/eureka/,http://localhost:7002/eureka/
对于 Ribbon,其实也没有什么神秘的,它实际就是一个 RestTemplate 对象。只是上面还讨论了多个节点的问题,例如调度用户微服务时,因为用户微服务存在多个节点,具体会使用哪个节点提供服务呢?关于这点 Spring Cloud 已经屏蔽了一些底层的细节,它只需要一个简单的 @ LoadBalance 注解就可以提供负载均衡的算法。这十分符合 Spring Boot 的原则,提供默认的实现方式,减少开发者的工作量。
在默认的情况下,它会提供轮询的负载均衡算法。
先在用户微服务上构建 rest 风格的访问接口,然后在产品微服务上通过 Maven 加入对 Ribbon 的依赖。
spring-cloud-starter-netflix-ribbon
package com.springboot.chapter17.product.main;
@SpringBootApplication(scanBasePackages="com.springboot.chapter17.product")
public class Chapter17ProductApplication{
//初始化 RestTemplate
@LoadBalanced //多节点负载均衡
@Bean(name = "restTemplate")
public RestTemplate initRestTemplate(){
return new RestTemplate();
}
public static void main(String[] args){
SpringApplication.run(Chapter17ProductApplication.class,args);
}
}
这段代码中在 RestTemplate 上加入了注解 @LoadBalanced。它的作用是让 RestTemplate 实现负载均衡,也就是,通过这个 RestTemplate 对象调用用户微服务请求的时候,Ribbon 会自动给用户微服务节点实现负载均衡,这样请求就会被分摊到微服务的各个节点上,从而降低单点的压力。
package com.springboot.chapter17.product.controller;
@RestController
@RequestMapping("/product")
public class ProductController{
//注入 RestTemplate
@Autowored
private RestTemplate restTemplate = null;
@GetMapping("/ribbon")
public UserPo testRibbon(){
UserPo user = null;
for(int i=0;i<10;i++){
user = restTemplate.getForObject(
"http://USER/user/"+(i+1),UserPo.class);
}
return user;
}
}
上节中使用了 RestTemplate,但是有时某个微服务 REST 风格请求需要多次调用,如类似上面的通过用户编号(id)查询用户信息的服务。如果多次调用,使用 RestTemplate 并非那么友好。因为除了要编写 URL,还需要注意这些参数的组装和结果的返回等操作。为了克服这些不友好,除了 Ribbon 外,Spring Cloud 还提供了声明式调用组件 —— Feign。
Feign 是一个基于接口的编程方式,开发者只需要声明接口和配置注解,在调度接口方法时,Spring Cloud 就根据配置来调度对应的 REST 风格的请求,从其他微服务系统中获取数据。使用 Feign,首先需要在产品微服务中使用 Maven 引入依赖包。
org.springframework.cloud
spring-cloud-starter-openfeign
这样就把 Feign 所需要的依赖包加载进来了。为了启动 Feign,首先需要在 Spring Boot 的启动文件中加入注解 @EnableFeignClients,这个注解代表该项目会启动 Feign 客户端。
package com.springboot.chapter17.product.main;
@SpringBootApplication
(scanBasePackages = "com.springboot.chapter17.product")
//启动 Feign
@EnableFeignClients(basePackages="com.springboot.chapter17.product")
public class Chapter17ProductApplication{
public static void main(String[] args){
SpringApplication.run(Chapter17ProductApplication.class,args);
}
}
加入了注解 @EnableFeignClients,并制定了扫描的包,这样 Spring Boot 就会启动 Feign 并且到对应的包中进行扫描。然后在产品微服务中加入接口声明,注意这里仅仅是一个接口声明,并不需要实现类。
package com.springboot.chapter17.product.service;
//指定服务 ID(Service ID)
@FeignClient("user")
public interface UserService{
//指定通过 HTTP 的 GET 方法请求路径
@GetMapping("/user/{id}")
//这里会采用 Spring MVC 的注解配置
public UserPo getUser(@PathVariable("id") Long id);
}
@FeignClient("user"),它代表这是一个 Feign 客户端,而配置的“user”是一个服务的 ID(Service ID),它指向了用户微服务,这样 Feign 就会知道向用户微服务请求,并会实现负载均衡。这里的注解 @GetMapping 代表启用 HTTP 的GET请求用户微服务,而方法中的注解 @PathVariable 代表从 URL 中获取参数,这显然还是 Spring MVC 的规则。
上述接口在注解 @EnableFeignClients 所定义的扫描包里,这样,Spring 就会将这个接口扫描到 IoC 容器中。
//注入 Feign 接口
@Autowired
private UserService userService = null;
//测试
@GetMapping("/feign")
public UserPo testFeign(){
UserPo user = null;
//循环 10 次
for(int i=0;i<10;i++){
Long id = (Long)(i+1);
user = userService.getUser(id);
}
return user;
}
与 Ribbon 相比, Feign 屏蔽掉了 RestTemplate 的使用,提供了接口声明式的调用,使得程序可读性更高,同时在多次调用中更为方便。
在互联网中,可能存在某一个微服务的某个时刻压力变大导致服务缓慢,甚至出现故障,导致服务不能响应。假设用户微服务请求中出现压力过大,服务响应速度变缓,进入瘫痪状态,而这时产品微服务响应还是正常响应。但是如果出现产品微服务大量调用用户微服务,就会出现大量的等待,如果还是持续地调用,则会造成大量请求的积压,导致产品微服务最终也不可用。可见在分布式中,如果一个服务不可用,而其他微服务还大量地调用这个不可用的微服务,也会导致其自身不可用,其自身不可用之后又可能继续蔓延到其他与之相关的微服务上,这样就会使得更多的微服务不可用,最终导致分布式服务瘫痪。
为了防止这样的蔓延,微服务提出了断路器的概念。在微服务系统之间大量调用可能导致服务消费者自身出现瘫痪的情况下,断路器就会将这些积压的大量请求“熔断”,来保证其自身服务的可用,而不会蔓延到其他微服务系统上。通过这样的断路机制可以保持各个微服务持续可用。
在 Spring Cloud 中断路器是由 NetFlix 的 Hystrix 实现的,它默认监控微服务之间的调用超时时间为 2000ms(2s),如果超过这个超时时间,它就会根据你的配置使用其他方法进行响应。
要启动断路器,首先需要在产品微服务中加入引用 Hystrix 的包,因此需要在 Maven 中引入它们。
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
package com.springboot.chapter17.product.main;
@SpringBootApplication(scanBasePackages="com.springboot.chapter17.product")
@EnableFeignClients(basePackages="com.springboot.chapter17.product")
@EnableCircuitBreaker
public class Chapter17ProductApplication{
.....
}
当加入注解 @EnableCircuitBreaker 后,Spring Cloud 就会启用断路机制,在后续的代码中使用注解@HystrixCommand 就能指定哪个方法启用断路机制。
@HystrixCommand 注解,它表示将在方法上启用断路机制,而其属性 fallbackMethod 则可以指定降级方法,指定为error。
对于 Hystrix,Spring Cloud 还提供了一个仪表盘进行监控断路的情况。
通过上面的内容,已经可以搭建一个基于 Spring Cloud 分布式的应用。在传统的网站中,我们还会引入如 Nginx、F5的网关功能。
网关的功能对于分布式网站是十分重要的,首先它可以将请求路由到真实的服务器上,进而保护真实服务器的 IP 地址,避免直接地攻击真实服务器,其次它也可以作为一种负载均衡的手段,使得请求按照一定的算法平摊到多个节点上,减缓节点的压力,最后,它还能提供过滤器,过滤器的使用可以判定请求是否为有效请求,一旦判定失败,就可以将请求阻止,避免发送到真实的服务器,这样就能降低真实服务器的压力。
在 Spring Cloud 的组件中,Zuul 是支持 API 网关开发的组件。Zuul 来自NetFlix 的开源网关,它的使用十分简单,下面给予举例说明。首先新建工程,取名为 chapter17-zuul,然后引入关于 Zuul 的包。
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-zuul
package com.springboot.chapter17.zuul.main;
@SpringBootApplication(scanBasePackages="com.springboot.chapter17.zuul")
@EnableZuulProxy
public class Chapter17ZuulApplication{
public static void main(String[] args){
SpringApplication.run(Chapter17ZuulApplication.class,args);
}
}
这样就能够启用 Zuul 网关代理功能了。@ EnableZuulProxy 注解中引入了 断路机制 @EnableCircuitBreaker 注解。
# 服务端口
server.port = 80
# Spring 应用名称
spring.application.name = zuul
#用户微服务映射规则
#指定 ANT 风格的 URL 匹配
zuul.routes.user-service.path =/u/**
# 指定映射的服务用户地址,这样 Zuul 就会将请求转发到用户微服务上了
zuul.routes.user-service.url = http://localhost:8001/
#产品微服务映射规则
zuul.routes.product-service.path = /p/**
#映射产品服务中心服务 ID,Zuul 会自动使用服务端负载均衡,分摊请求
zuul.routes.product-service.serviceId = product
#注册给服务治理中心
eureka.client.serviceUrl.defaultZone=http://localhost:7001/eureka,http://localhost:7002/eureka/
上面只是将请求转发到具体的服务器或者具体的微服务上,但是有时候还希望网关功能更强大一些。例如,监测用户登录、黑名单用户、购物验证码、恶意刷请求攻击等场景。如果这些在过滤器内判断失败,那么就不要再把请求转发到其他微服务上,以保护微服务的稳定。
下面模拟这样一个场景。假设当前需要提交一个表单,而每一个表单都存在一个序列号,并且这个序列号对应一个验证码,在提交表单的时候,这两个参数都会一并提交到 Zuul 网关。对于 Redis 服务器会以序列号为 键,而以验证码为值进行存储。当路由网关过滤器判定用户提交的验证码与 Redis 服务器保存不一致的时候,则不再转发请求到微服务。这里验证码使用 Redis 进行存储,所以会比使用数据库快得多,这有助于性能的提高,避免造成瓶颈。
上面的内容中,对于启动文件采用了很多注解,如 @SpringBootApplication、@EnableDiscoveryClient 和@EnableCircuitBreaker 等。Spring Cloud 还提供了自己的注解 @SpringCloudApplication来简化使用 Spring Cloud的开发。
@SpringCloudApplication 源码
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
显然,springCloud 会启用 Spring Boot的应用,以及开发服务发现和断路器的功能。而它目前还缺乏配置扫描包的配置项,所以往往需要配合注解 @ComponentScan 来定义扫描的包。