本文所有代码:
https://github.com/tianhaowang-6/cloudstudy.git
1基本概念:
1.1单一架构
一个工程对应一个war包,订单服务、库存服务、仓储服务、积分服务都在一个服务中,运行在一个tomcat上,all in one 的策略。单机模式
1.2演进过程
将单一架构进行水平拆分,垂直拆分。
水平拆分,不同层次负责不同功能。例如数据库层,请求处理层,控制跳转层
垂直拆分,不同模块负责不同功能。例如订单服务、库存服务、仓储服务、积分服务。
1.3分布式架构
分布式与集群的不同:
分布式:每个应用提供不同的功能。
集群:每个提供相同的功能。
2springcloud
springcloud微服务架构,利用它的最基本的五个组件Eureka、Ribbon、Feign、Hystrix、Zuul实现快速的构建分布式系统。
2.1基本组件简介
注册中心:Eureka
c/s架构,我们创建的Eureka应用,一般作为服务端,其他应用作为客户端
Eureka Server是一个注册中心,里面有一个注册表,保存了各服务所在的机器和端口号
主要用于注册管理springcloud微服务。
负载均衡:Ribbon
管理同一功能的应用,实现负载均衡。
声明式远程调用方法:Feign
用于处理远程调用。
熔断降级监控:Hystrix
用于处理熔断,降级
网关:Zuul
统一访问路径。
3准备工作
3.1父工程主要进行管理版本
导入依赖:
<dependencyManagement>
<dependencies>
<!-- 导入 SpringCloud 需要使用的依赖信息 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<!-- import 依赖范围表示将 spring-cloud-dependencies 包中的依赖信息导入 -->
<scope>import</scope>
</dependency>
<!-- 导入 SpringBoot 需要使用的依赖信息 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.2创建通用子工程
创建通用工程的目的是管理后续代码中使用的公用代码。
pom.xml编写
<parent>
<artifactId>cloudparent</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloudcommon</artifactId>
创建一个employee类,后面会使用到,作为数据传输的
public class Employee {
private int id;
private String name;
private double salary;
public Employee() {
}
public Employee(int id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
", salary=" + salary +
'}';
}
}
4注册中心Eureka
4.1创建一个eureka子工程
创建也给Eureka注册中心,监听client请求,用于注册应用。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
创建主启动类:注意添加@EnableEurekaServer标识为主启动类。
@EnableEurekaServer // 标记为EurefaServer
@SpringBootApplication
public class CloudMainType {
public static void main(String[] args) {
SpringApplication.run(CloudMainType.class,args);
}
}
添加application.yml配置我们需要的信息
server:
port: 5000
spring:
application:
name: cloud-consumer
eureka:
instance:
hostname: localhost
client: #关闭很多功能,这里是eureka服务端
registerWithEureka: false # 由于自己就是注册中心所以不用注册
fetchRegistry: false # 自己就是注册中心,不用给出注册信息。
serviceUrl: #客户端访问eureka的地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
再被注册应用上添加依赖,和配置信息,实现成功注册:
4.2创建一个服务提供子工程
为provider工程添加依赖
<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>
<!--这里就会用到上面提供的cloudcommon工程 -->
<dependency>
<groupId>org.example</groupId>
<artifactId>cloudcommon</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
创建主启动类:
@SpringBootApplication
public class CloudMainType {
public static void main(String[] args) {
SpringApplication.run(CloudMainType.class,args);
}
}
配置application.yml
server:
port: 1000
spring:
application:
name: cloud-consumer
编写一个handler处理请求。
@RestController
public class EmpHandler {
@RequestMapping("/provider/get/emp/remote")
public Employee getEmpRemote(HttpServletRequest request){
int serverPort = request.getServerPort();
return new Employee(555,"tom333"+serverPort,123.123);
}
}
4.3创建一个消费工程
用于向provider发起请求。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>cloudcommon</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
创建主启动类:
@SpringBootApplication
public class CloudMainType {
public static void main(String[] args) {
SpringApplication.run(CloudMainType.class,args);
}
}
创建配置类提供RestTemplate
@Configuration
public class CloudConfig {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
配置application.yml
server:
port: 4000
创建handler方法。请求处理请求。
@RestController
public class HumanResourceHandler {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/consume/get/emp")
public Employee getEmployeeRemote(){
// 远程方法调用的ip与主机
String host="http://localhost:1000";
// 请求的路径
String url="/provider/get/emp/remote";
return restTemplate.getForObject(host+url,Employee.class);
}
}
实际操作:
- 运行eureka服务,运行provider服务,运行consumer服务
- 请求到consumer服务 localhost:4000/consume/get/emp
- 请求会到provider中。
这里是eureka的界面,里面有两个服务(application),就是我们注册进去的。
这样做还是有很多问题,例如consumer写死了请求路径,既然eureka都知道服务的ip和端口,为服务指定一个名称,这样不就好标识了吗。
5ribbon
然后使用eureka+ribbon转发请求,通过微服务名代替ip+端口号的方式。降低了耦合度。
使用ribbon不用单独创立一个子工程,只需要导入依赖,使用就可。
导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
上方在创建provider和consumer的时候就对工程进行取名了。
spring:
application:
name: cloud-xxx #这里进行取名
在consumer的CloudConfig类的获取RestTemplate方法上添加一个注解。
@Bean
@LoadBalanced // 这里需要配置一下
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
开启该注解后,就能解析服务名和主机地址之间的映射关系。重新编写一个consumer的方法。
@RequestMapping("/consume/get/emp")
public Employee getEmployeeRemote(){
// String host="http://localhost:1000";
// eureka+ribbon 可以通过微服务名代替ip+端口号
String host="http://cloud-provider";
String url="/provider/get/emp/remote";
return restTemplate.getForObject(host+url,Employee.class);
}
ribbon通过服务名转换为实际ip:port的过程,eureka有服务名到ip:port的映射。
然而我们的服务往往不是单一的,我们同一个provider服务可能部署在多个服务器上,我们需要以集群的方式启动这些服务器。管理这些服务器。
5.1创建provider集群提供服务
复制cloudprovider子工程 创建3个provider。ip+port表示为一个微服务。
三个服务名都是cloud-provider
provider 使用端口与之前相同,1000端口
provider1使用端口是,2000端口
provider2使用端口是,3000端口
为了表示我们确实调用了不同服务,在provider中使用request.getServerPort()来获取端口信息.
@RestController
public class EmpHandler {
@RequestMapping("/provider/get/emp/remote")
public Employee getEmpRemote(HttpServletRequest request){
int serverPort = request.getServerPort();// 获取服务端口信息。
return new Employee(555,"tom333"+serverPort,123.123);
}
}
浏览器发送请求。三个provider轮询被访问。
6feign
6.1基本思想
consumer调用provider方法的操作就是远程调用。中间通过common进行了映射。
6.2使用feign实现远程方法调用
创建common工程
提供了一个common,通常在项目中命名为api工程,我们这里为了简单,就直接使用common工程作为api管理接口。
- 导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 创建一个远程调用的服务接口:
@FeignClient("cloud-provider")
public interface EmployeeRemoteService {
@RequestMapping("/provider/get/emp/remote")
public Employee getEmployeeRemote();
}
创建一个feign-consumer工程:与之前的consumer不同了
- 导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>cloudcommon</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
- 创建一个feign客户端
// 启用 Feign 客户端功能
@EnableFeignClients
@SpringBootApplication
public class CloudMainType {
public static void main(String[] args) {
SpringApplication.run(CloudMainType.class, args);
}
}
- 添加配置信息
server:
port: 7000
spring:
application:
name: cloud-feign-consumer
eureka:
client:
serviceUrl:
defaultZone: http://localhost:5000/eureka/
feign:
hystrix:
enabled: true
- 创建一个handler发送请求。
@RestController
public class EmpFeignHandler {
// 看似进行本地调用,实际上调用了common程序的service,然后通过feign调用了
@Autowired
private EmployeeRemoteService employeeRemoteService;
@RequestMapping("/feign/consumer/get/emp")
public Employee getEmployeeRemote() {
return employeeRemoteService.getEmployeeRemote();
}
}
我们这里没有进行参数传递演示。其实和使用springmvc传递参数流程差不多。下面有个例子看看吧
consumer
common
provider
7Hystrix
7.1介绍
最开始看不懂这些概念不重要,这里就是就理解为处理服务异常的就行了。
正常三个服务正常工作
但是现在service3由于流量大导致崩坏,导致我们三个服务寄了,实际情况更加复杂。
现在我们期望service3崩坏后,不影响我们其他服务的正常使用。于是hystrix处理了这种情况。一个服务崩坏后,导致其他服务相继死掉的现象可以理解为雪崩。
下方这些概念可以不用管。
Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多
依赖不可避免的会调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的
情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故
障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应
(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服
务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延, 乃至雪崩。
Hytrix 能够提供服务降级、服务熔断、服务限流、接近实时的监控等方面的功能。
重点关注
服务熔断:
7.2服务熔断机制:
当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该
节点微服务的调用,快速响应错误信息。当检测到该节点微服务调用响应正常后恢复调用链
路。在 SpringCloud 框架里熔断机制通过 Hystrix 实现。Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是 5 秒内 20 次调用失败就会启动熔断机制。熔断机制的注解是@HystrixCommand。
处理provider模块的错误信息。
- 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- 主启动类添加注解
@EnableCircuitBreaker // 启动断路器功能
@SpringBootApplication
public class CloudMainType {
public static void main(String[] args) {
SpringApplication.run(CloudMainType.class,args);
}
}
common添加一个类,作为请求处理返回对象。
public class ResultEntity<T> {
public static final String SUCCESS = "SUCCESS";
public static final String FAILED = "FAILED";
public static final String NO_MESSAGE = "NO_MESSAGE";
public static final String NO_DATA = "NO_DATA";
/**
* 操作成功,不需要返回数据
* @return
*/
public static ResultEntity<String> successWithoutData() {
return new ResultEntity<String>(SUCCESS, NO_MESSAGE, NO_DATA);
}
/**
* 操作成功,需要返回数据
* @param data * @return
*/
public static <E> ResultEntity<E> successWithData(E data) {
return new ResultEntity<>(SUCCESS, NO_MESSAGE, data);
}
/**
* 操作失败,返回错误消息
* @param message * @return
*/
public static <E> ResultEntity<E> failed(String message) {
return new ResultEntity<>(FAILED, message, null);
}
private String result;
private String message;
private T data;
public ResultEntity() {
// TODO Auto-generated constructor stub
}
public ResultEntity(String result, String message, T data) {
super();
this.result = result;
this.message = message;
this.data = data;
}
@Override
public String toString() {
return "ResultEntity [result=" + result + ", message=" + message + ", data=" + data + "]";
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
provider方法,添加处理方法。
@HystrixCommand(fallbackMethod = “getEmpBackup”)注解进行处理异常,跳转到指定方法。
// 第一个方法主要用于处理请求。
// 通过 fallbackMethod 属性指定断路情况下要调用的备份方法
@HystrixCommand(fallbackMethod = "getEmpBackup")
@RequestMapping("/provider/circuit/breaker/get/emp")
public ResultEntity<Employee> getEmp(@RequestParam("signal") String signal) {
System.out.println("getEmp!!!!!!!!!!");
System.out.println("signal="+signal+"bang".equals(signal));
if("bang".equals(signal)) {
// 抛出异常,会被@HystrixCommand(fallbackMethod = "getEmpBackup")捕获进行处理。
throw new RuntimeException();
}
return ResultEntity.successWithData(new Employee(666, "sam666", 666.66));
}
// 异常出现后的处理方法。
public ResultEntity<Employee> getEmpBackup(@RequestParam("signal") String signal) {
return ResultEntity.failed("circuit break workded,with signal="+signal);
}
7.3服务降级
服务降级处理是在客户端(Consumer 端)实现完成的,与服务端(Provider 端)没有关系。
当某个 Consumer 访问一个 Provider 却迟迟得不到响应时执行预先设定好的一个解决方案,
而不是一直等待。
在common添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
添加一个FallbackFactory
// 请注意自动扫描包的规则
// 比如:feign-consumer 工程需要使用 MyFallBackFactory,那么 MyFallBackFactory 应该在feign-consumer 工程的主启动类所在包或它的子包下
// 简单来说:哪个工程用这个类,哪个工程必须想办法扫描到这个类
@Component
@Component
public class MyFallBackFactory implements FallbackFactory<EmployeeRemoteService> {
// cause 对象是失败原因对应的异常对象
@Override
public EmployeeRemoteService create(Throwable cause) {
return new EmployeeRemoteService() {
@RequestMapping("/provider/save/emp")
public Employee saveEmp(@RequestBody Employee employee) {
return null;
}
@RequestMapping("/provider/get/emp/by/id")
public Employee getEmployeeById(@RequestParam("empId") Integer empId) {
return null;
}
@RequestMapping("/provider/circuit/breaker/get/emp")
public ResultEntity<Employee> getEmp(@RequestParam("signal") String signal){
System.out.println("getEmp");
return new ResultEntity<>();
}
@RequestMapping("/provider/get/emp/remote")
public Employee getEmployeeRemote() {
System.out.println("MyFallBackFactory+getEmployeeRemote");
return new Employee(444, "call provider failed,fall back here, reason is "+cause.getClass().getName()+" "+cause.getMessage(), 444.444);
}
};
}
}
然后再common的feign接口(就是远程调用的接口)中添加fallbackFactory属性,指定consumer请求provider失败的的处理规则。
// 在@FeignClient 注解中增加 fallbackFactory 属性
// 指定 consumer 调用 provider 时如果失败所采取的备用方案
// fallbackFactory 指定 FallbackFactory 类型的类,保证备用方案返回相同类型的数据
//@FeignClient("cloud-provider")
@FeignClient(value="cloud-provider", fallbackFactory= MyFallBackFactory.class)
public interface EmployeeRemoteService {
@RequestMapping("/provider/get/employee/by/id")
public Employee getEmployeeById(@RequestParam("empId") Integer empId);
@RequestMapping("/provider/get/emp/remote")
public Employee getEmployeeRemote();
@RequestMapping("/provider/circuit/breaker/get/emp")
public ResultEntity<Employee> getEmp(@RequestParam("signal") String signal);
}
在consumer的application中开启hystrix:
feign:
hystrix:
enabled: true
测试:前提手动将所有的provider停止:
然后发送请求得到效果
7.4监控provider工程
provider中导入依赖,被监控。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置 application.yml
management:
endpoints:
web:
exposure:
include: hystrix.stream
添加监控工程:cloudhystrixdashboard
添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
</dependencies>
开启仪表盘功能:
// 启用 Hystrix 仪表盘功能
@EnableHystrixDashboard
@SpringBootApplication
public class CloudMainType {
public static void main(String[] args) {
SpringApplication.run(CloudMainType.class, args);
}
}
配置application.yml
server:
port: 8000
spring:
application:
name: cloud-dashboard
使用步骤
- 访问表盘工程首页 http://localhost:8000/hystrix
- 填写参数,这里使用最简单的方式:http://localhost:1000/actuator/hystrix.stream
注意要访问有熔断功能的方法。
例如这种:
直接查看监控数据本身
- http://localhost:1000/actuator/hystrix.stream
- 说明 1:http://localhost:1000 访问的是被监控的 provider 工程
- 说明 2:/actuator/hystrix.stream 是固定格式
- 说明 3:如果从 provider 启动开始它的方法没有被访问过,那么显示的数
- 据只有“ping:”,要实际访问一个带熔断功能的方法才会有实际数据。
8Zuul网关:
Zuul 和 Eureka 进行整合,将 Zuul 自身注册为 Eureka 服务治理下的应用,同时从 Eureka 中获得其他微服务的信息,也即以后的访问微服务都是通过 Zuul 跳转后获得。
zuul提供了代理,路由,过滤功能。
配置Zuul后的访问规则
8.1基本原理
8.2创建zuul工程
导入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
配置信息:
server:
port: 9000
spring:
application:
name: zuul-gateway
eureka:
client:
serviceUrl:
defaultZone: http://localhost:5000/eureka/
开启Zuul功能:
// 启用 Zuul 代理功能
@EnableZuulProxy
@SpringBootApplication
public class CloudMainType {
public static void main(String[] args) {
SpringApplication.run(CloudMainType.class, args);
}
}
测试:http://localhost:9000/cloud-feign-consumer/feign/consumer/get/emp
路径解析:
cloud-feign-consumer这样的服务名不够优雅。
8.3修改配置
zuul:
routes:
emp: # 这个是配置名称,底层使用map的key
serviceId: cloud-feign-consumer # 服务名
path: /consumer/** #新的路径,**表示多层路径匹配,不加就不能进行多层路径匹配
http://localhost:9000/cloud-feign-consumer/feign/consumer/get/emp
与
http://localhost:9000/consumer/feign/consumer/get/emp
都能进行访问
由于更改了路径以前的访问就不希望被访问
zuul:
ignored-services: # 忽略指定微服务名称,让用户不能通过微服务名称访问
- cloud-feign-consumer
http://localhost:9000/cloud-feign-consumer/feign/consumer/get/emp
与
http://localhost:9000/consumer/feign/consumer/get/emp
上方不能访问了
更多配置
zuul:
# ignored-services: 忽略指定微服务名称,让用户不能通过微服务名称访问
# - cloud-feign-consumer
ignored-services: '*' # 忽略所有微服务名称
prefix: /maomi # 给访问路径添加统一前缀
routes:
employee: # 自定义路由规则的名称,在底层的数据结构中是 Map 的键
serviceId: cloud-feign-consumer # 目标微服务名称,ZuulRoute 类型的一个属性
path: /zuul-emp/** # 用来代替目标微服务名称的路径,ZuulRoute 类型的一个属性
# /**表示匹配多层路径,如果没有加/**则不能匹配后续的多层路径了
现在的访问路径:http://localhost:9000/maomi/consume/feign/consumer/get/emp
多了个前缀maomi
8.4ZuulFilter
用于过滤请求,使用规则和filter差不多。
@Component
public class MyZuulFilter extends ZuulFilter {
@Override
public String filterType() {
// 这个方法返回过滤器类型,
// 可选项,pre,route , post ,static
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
// 1.获取当前 RequestContext 对象
RequestContext context = RequestContext.getCurrentContext();
// 2.获取当前请求对象
HttpServletRequest request = context.getRequest();
// 3.获取当前请求要访问的目标地址
String servletPath = request.getServletPath();
// 4.打印
System.err.println("servletPath="+servletPath);
System.out.println("servletPath+ " +servletPath);
// true : 过滤,执行run方法
// false :不过滤,直接放行
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("run()........");// 执行具体过滤逻辑
return null; // 官方文档说:当前实现会忽略这个返回值,所以返回 null 即可
}
}