源码地址:https://github.com/GeeCoder-zyl/springcloud
请注意:源码与教程有出入,现源码将服务提供者中的mapper、service、serviceImpl包全部移到了api模块中。
Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中涉及的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。Spring Cloud包含了多个子项目(针对分布式系统中涉及的多个不同开源产品),比如:Spring Cloud Config、Spring Cloud Netflix、Spring Cloud0 CloudFoundry、Spring Cloud AWS、Spring Cloud Security、Spring Cloud Commons、Spring Cloud Zookeeper、Spring Cloud CLI等项目。
微服务(Microservices Architecture)是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。在所有情况下,每个任务代表着一个小的业务能力。
微服务的概念源于2014年3月Martin Fowler所写的章“Microservices”http://martinfowler.com/articles/microservices.html
那我们在微服务中应该怎样设计呢。以下是微服务的设计指南:
由于Spring Cloud为服务治理做了一层抽象接口,所以在Spring Cloud应用中可以支持多种不同的服务治理框架,比如:Netflix Eureka、Consul、Zookeeper。在Spring Cloud服务治理抽象层的作用下,我们可以无缝地切换服务治理实现,并且不影响任何其他的服务注册、服务发现、服务调用等逻辑。
Spring Cloud Eureka来实现服务治理。
Spring Cloud Eureka是Spring Cloud Netflix项目下的服务治理模块。而Spring Cloud Netflix项目是Spring Cloud的子项目之一,主要内容是对Netflix公司一系列开源产品的包装,它为Spring Boot应用提供了自配置的Netflix OSS整合。通过一些简单的注解,开发者就可以快速的在应用中配置一下常用模块并构建庞大的分布式系统。它主要提供的模块包括:服务发现(Eureka),断路器(Hystrix),智能路由(Zuul),客户端负载均衡(Ribbon)等。
创建maven项目
<!--导入springBoot依赖包 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<!--依赖管理,用于管理spring-cloud的依赖 -->
<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>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<!--导入springCloudjar包 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<!--引入springBoot jar包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--支持热部署 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>1.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!--整合redis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<!--导入pojo插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--使用druid整合mysql数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--阿里数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<!--mybatis-plus配置 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.6</version>
</dependency>
<!--引入springBoot监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
在springcloud项目上创建新的Modul
父项目选择springcloud
<dependencies>
<!--添加 eureka 注册中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
server:
port: 7000 #定义注册中心端口
eureka:
server:
enable-self-preservation: true #设定自我保护模式 默认值为true 不建议关闭
instance:
hostname: localhost #eureka服务的实例名称
client:
register-with-eureka: false #表示注册中心不会注册自己本身
fetch-registry: false #表示自己就是注册中心,不需要检索服务
service-url:
defaultZone: http://${
eureka.instance.hostname}:${
server.port}/eureka/
package com.zyl.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class})
@EnableEurekaServer//定义为Eureka服务端
public class Eureka_7000 {
public static void main(String[] args) {
SpringApplication.run(Eureka_7000.class, args);
}
}
该项目主要定义公用实体类、公用的对象方法接口、公用fallback类
<dependencies>
<!--引入Feign支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
通过lombok插件生成实体类的getter和setter方法等,使用前请参照各IED教程安装lombok插件。
package com.zyl.springcloud.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @ClassName: User
* @Description: 描述
* @author: ZYL
* @date: 2020/7/2 17:42
*/
@Data//开启lombok
@Accessors(chain = true)//开启级联操作,为true时setter方法返回当前对象
@ToString//生成toString()方法
@TableName(value = "tb_user")//开始mybatis-plus表映射,当表名与类名不一样时,需要配置value属性
public class User implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
private String name;
private Integer age;
private String sex;
}
<dependencies>
<dependency>
<groupId>com.zyl.springcloud</groupId>
<artifactId>springcloud-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--添加eureka 客户端地址 -->
<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-config</artifactId>
</dependency>
<!--添加断路器配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--添加监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
server:
port: 8001
servlet:
context-path: /
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource #引入druid数据源
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db_test?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: 123456
application:
name: provider-user #定义服务名称
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka #注册到指定的eureka
instance:
instance-id: provider-user-8001 #定义微服务的名称
prefer-ip-address: true #是否显示IP和端口
#mybatis-plush配置
mybatis-plus:
type-aliases-package: com.zyl.springcloud.pojo #配置别名包路径
mapper-locations: classpath:/mybatis/mappers/*.xml #添加mapper映射文件
configuration:
map-underscore-to-camel-case: true #开启驼峰映射规则
feign:
hystrix:
enabled: true #启动熔断器机制
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000 #设定断路器超时时间
package com.zyl.springcloud;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
/**
* @className: Provider_8001
* @description: 描述
* @author: ZYL
* @date: 2020/7/3 13:55
*/
@SpringBootApplication//声明为SpringBoot应用
@MapperScan("com.zyl.springcloud.mapper")//扫描数据库映射文件
@EnableEurekaClient//注册到Eureka注册中心
@EnableHystrix//开启断路器机制
public class Provider_8001 {
public static void main(String[] args) {
SpringApplication.run(Provider_8001.class, args);
}
}
继承mybatis-plus提供的BaseMapper,其提供基本的crud操作
package com.zyl.springcloud.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zyl.springcloud.pojo.User;
/**
* @className: UserMapper
* @description: 描述
* @author: ZYL
* @date: 2020/7/3 13:55
*/
public interface UserMapper extends BaseMapper<User>{
}
package com.zyl.springcloud.service;
import com.zyl.springcloud.pojo.User;
import java.util.List;
/**
* @className: UserService
* @description: 描述
* @author: ZYL
* @date: 2020/7/3 14:03
*/
public interface UserService {
//获取所有用户的信息
List<User> getAllUser();
//根据用户名获取用户信息
User getUserByName(String name);
//新增用户
int addUser(User user);
//删除用户
int deleteUser(Integer id);
//修改用户信息
int updateUser(User user);
}
注意QueryWrapper的使用
package com.zyl.springcloud.serviceImp;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zyl.springcloud.mapper.UserMapper;
import com.zyl.springcloud.pojo.User;
import com.zyl.springcloud.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @className: UserServiceImp
* @description: 描述
* @author: ZYL
* @date: 2020/7/3 14:04
*/
@Service
public class UserServiceImp implements UserService {
@Autowired(required = false)
public UserMapper userMapper;
//获取所有用户的信息
@Override
public List<User> getAllUser() {
List<User> userList = userMapper.selectList(null);
return userList;
}
//根据用户名获取用户信息
@Override
public User getUserByName(String name) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", name);
User user = userMapper.selectOne(queryWrapper);
return user;
}
//新增用户
@Override
public int addUser(User user) {
int num = userMapper.insert(user);
return num;
}
//删除用户
@Override
public int deleteUser(Integer id) {
int num = userMapper.deleteById(id);
return num;
}
//修改用户信息
@Override
public int updateUser(User user) {
int num = userMapper.updateById(user);
return num;
}
}
package com.zyl.springcloud.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.zyl.springcloud.pojo.User;
import com.zyl.springcloud.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @className: UserController
* @description: 服务提供者控制器
* @author: ZYL
* @date: 2020/7/3 14:11
*/
@RestController//使用RESTful风格
public class UserController {
@Autowired
private UserService userService;
//获取所有用户的信息
@GetMapping("/getAllUser")
@HystrixCommand(fallbackMethod = "getAllUser")
public List<User> getAllUser() {
List<User> userList = userService.getAllUser();
return userList;
}
//根据用户名获取用户信息
@GetMapping("/getUserByName/{name}")
@HystrixCommand(fallbackMethod = "getUserByName")
public User getUserByName(@PathVariable String name) {
User user = userService.getUserByName(name);
return user;
}
//新增用户
@PostMapping("/addUser/{name}/{age}/{sex}")
@HystrixCommand(fallbackMethod = "addUser")
public int addUser(User user) {
int num = userService.addUser(user);
return num;
}
//删除用户
@DeleteMapping("/deleteUser/{id}")
@HystrixCommand(fallbackMethod = "deleteUser")
public int deleteUser(@PathVariable Integer id) {
int num = userService.deleteUser(id);
return num;
}
//修改用户信息
@PutMapping("/updateUser/{id}/{name}/{age}/{sex}")
@HystrixCommand(fallbackMethod = "updateUser")
public int updateUser(User user) {
int num = userService.updateUser(user);
return num;
}
}
得到后端返回的json数据
Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随即连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。
均为接口类型,有以下几个
ServerList
ServerListFilter
IRule
Ribbon在工作时首选会通过ServerList来获取所有可用的服务列表,然后通过ServerListFilter过虑掉一部分地址,最后在剩下的地址中通过IRule选择出一台服务器作为最终结果。
简单轮询负载均衡(RoundRobin)
以轮询的方式依次将请求调度不同的服务器,即每次调度执行i = (i + 1) mod n,并选出第i台服务器。
随机负载均衡 (Random)
随机选择状态为UP的Server
加权响应时间负载均衡 (WeightedResponseTime)
根据相应时间分配一个weight,相应时间越长,weight越小,被选中的可能性越低。
区域感知轮询负载均衡(ZoneAvoidanceRule)
复合判断server所在区域的性能和server的可用性选择server
Feign
是一个声明式的伪Http
客户端,它使得写Http
客户端变得更简单。
使用Feign
,只需要创建一个接口并注解,它具有可插拔的注解特性,可使用Feign
注解和JAX-RS
注解,Feign
支持可插拔的编码器和解码器,Feign
默认集成了Ribbon
,并和Eureka
结合,默认实现了负载均衡的效果。
Feign
具有如下特性:
Feign
注解和JAX-RS
注解HTTP
编码器和解码器Hystrix
和它的Fallback
Ribbon
的负载均衡HTTP
请求和响应的压缩Feign
是一个声明式的Web Service
客户端,它的目的就是让Web Service
调用更加简单。它整合了Ribbon
和Hystrix
,从而不再需要显式地使用这两个组件。Feign
还提供了HTTP
请求的模板,通过编写简单的接口和注解,就可以定义好HTTP
请求的参数、格式、地址等信息。接下来,Feign
会完全代理HTTP
的请求,我们只需要像调用方法一样调用它就可以完成服务请求。简而言之:Feign
能干Ribbon
和Hystrix
的事情,但是要用Ribbon
和Hystrix
自带的注解必须要引入相应的jar
包才可以。
<dependencies>
<dependency>
<groupId>com.zyl.springcloud</groupId>
<artifactId>springcloud-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!--Eureka客户端配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--引入Feign支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--添加断路器配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--添加监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
server:
port: 8020
servlet:
context-path: /
spring:
application:
name: consumer-user #定义服务名称
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka #链接注册中心集群
instance:
instance-id: consumer-user-8020 #定义微服务的名称
prefer-ip-address: true #是否显示IP和端口
feign:
hystrix:
enabled: true #启动熔断器机制
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000 #设定断路器超时时间
package com.zyl.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @className: Consumer_8020
* @description: 描述
* @author: ZYL
* @date: 2020/7/3 15:40
*/
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//排除数据源
@EnableEurekaClient//链接注册中心
@EnableFeignClients//添加feign的支持
public class Consumer_8020 {
public static void main(String[] args) {
SpringApplication.run(Consumer_8020.class, args);
}
}
package com.zyl.springcloud.client;
import com.zyl.springcloud.fallback.UserFeignClientFallbackFactory;
import com.zyl.springcloud.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @className: UserService
* @description: Feign客户端
* @author: ZYL
* @date: 2020/7/3 15:40
*/
@FeignClient(value = "provider-user", fallbackFactory = UserFeignClientFallbackFactory.class)//指定服务提供者的服务名称和fallback类
public interface UserFeignClient {
//查询所有用户的信息
@GetMapping("/getAllUser")
List<User> getAllUser();
//根据用户名获取用户信息
@GetMapping("/getUserByName/{name}")
User getUserByName(@PathVariable String name);
//新增用户
@PostMapping("/addUser/{name}/{age}/{sex}")
String addUser(@PathVariable String name, @PathVariable Integer age, @PathVariable String sex);
//删除用户
@DeleteMapping("/deleteUser/{id}")
String deleteUser(@PathVariable Integer id);
//修改用户信息
@PutMapping("/updateUser/{id}/{name}/{age}/{sex}")
String updateUser(@PathVariable Integer id, @PathVariable String name, @PathVariable Integer age, @PathVariable String sex);
}
package com.zyl.springcloud.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.zyl.springcloud.client.UserFeignClient;
import com.zyl.springcloud.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @className: UserController
* @description: 服务消费者控制器
* @author: ZYL
* @date: 2020/7/3 15:43
*/
@RestController
public class UserController {
@Autowired
private UserFeignClient userFeignClient;
//查询所有用户的信息
@GetMapping("/getAllUser")
@HystrixCommand(fallbackMethod="getAllUser")
public List<User> getAllUser() {
return userFeignClient.getAllUser();
}
//根据用户名获取用户信息
@GetMapping("/getUserByName/{name}")
@HystrixCommand(fallbackMethod="getUserByName")
public User getUserByName(@PathVariable String name) {
return userFeignClient.getUserByName(name);
}
//新增用户
@PostMapping("/addUser/{name}/{age}/{sex}")
@HystrixCommand(fallbackMethod="addUser")
public String addUser(User user) {
return userFeignClient.addUser(user.getName(), user.getAge(), user.getSex());
}
//删除用户
@DeleteMapping("/deleteUser/{id}")
@HystrixCommand(fallbackMethod="deleteUser")
public String deleteUser(@PathVariable Integer id) {
return userFeignClient.deleteUser(id);
}
//修改用户信息
@PutMapping("/updateUser/{id}/{name}/{age}/{sex}")
@HystrixCommand(fallbackMethod="updateUser")
public String updateUser(User user) {
return userFeignClient.updateUser(user.getId(), user.getName(), user.getAge(), user.getSex());
}
}
package com.zyl.springcloud.config;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @className: UserService
* @description: 服务调用策略配置
* @author: ZYL
* @date: 2020/7/3 15:40
*/
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
@Bean
public RoundRobinRule myRule() {
return new RoundRobinRule();//轮询策略
// return new RandomRule();//随机策略
// return new AvailabilityFilteringRule();//首先会过滤掉故障机或者并发链接数超过阈值的服务器.剩余的机器轮询配置
// new WeightedResponseTimeRule();//服务器影响时间越快,则权重越高
// new BestAvailableRule();//最大可用策略,即先过滤出故障服务器后,选择一个当前并发请求数最小的
}
}
在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),在Spring Cloud可以用RestTemplate+Ribbon和Feign来调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。
针对上述问题,在Spring Cloud Hystrix中实现了线程隔离、断路器等一系列的服务保护功能。它也是基于Netflix的开源框架 Hystrix实现的,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备了服务降级、服务熔断、线程隔离、请求缓存、请求合并以及服务监控等强大功能。
断路器模式源于Martin Fowler的Circuit Breaker一文。“断路器”本身是一种开关装置,用于在电路上保护线路过载,当线路中有电器发生短路时,“断路器”能够及时的切断故障电路,防止发生过载、发热、甚至起火等严重后果。
在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
SpringCloud Netflix实现了断路器库的名字叫Hystrix. 在微服务架构下,通常会有多个层次的服务调用. 下面是微服架构下, 浏览器端通过API访问后台微服务的一个示意图:
一个微服务的超时失败可能导致瀑布式连锁反映,下图中,Hystrix通过自主反馈实现的断路器, 防止了这种情况发生。
图中的服务B因为某些原因失败,变得不可用,所有对服务B的调用都会超时。当对B的调用失败达到一个特定的阀值(5秒之内发生20次失败是Hystrix定义的缺省值), 链路就会被处于open状态, 之后所有所有对服务B的调用都不会被执行, 取而代之的是由断路器提供的一个表示链路open的Fallback消息. Hystrix提供了相应机制,可以让开发者定义这个Fallbak消息.
open的链路阻断了瀑布式错误, 可以让被淹没或者错误的服务有时间进行修复。这个fallback可以是另外一个Hystrix保护的调用, 静态数据,或者合法的空值. Fallbacks可以组成链式结构,所以,最底层调用其它业务服务的第一个Fallback返回静态数据.
<!--添加断路器配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
package com.zyl.springcloud.fallback;
import java.util.ArrayList;
import java.util.List;
import com.zyl.springcloud.client.UserFeignClient;
import org.springframework.stereotype.Component;
import com.zyl.springcloud.pojo.User;
import feign.hystrix.FallbackFactory;
/**
* @ClassName: UserFeignClientFallbackFactory
* @Description: 用户服务返回预期错误信息
* @author: ZYL
* @date: 2020/7/2 17:42
*/
@Component
public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeignClient> {
@Override
public UserFeignClient create(Throwable cause) {
return new UserFeignClient() {
@Override
public List<User> getAllUser() {
User user = new User();
user.setId(0).setName("后台服务器异常,查询用户失败").setAge(0).setSex("");
List<User> userList = new ArrayList<User>();
userList.add(user);
return userList;
}
@Override
public User getUserByName(String name) {
User user = new User();
user.setId(0).setName("后台服务器异常,查询用户失败").setAge(0).setSex("");
return user;
}
@Override
public String updateUser(Integer id, String name, Integer age, String sex) {
return "后台服务器异常,更新用户失败";
}
@Override
public String addUser(String name, Integer age, String sex) {
return "后台服务器异常,新增用户失败";
}
@Override
public String deleteUser(Integer id) {
return "后台服务器异常,删除用户失败";
}
};
}
}
package com.zyl.springcloud.config;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Servlet;
@Configuration
public class DashboardConfig {
@Bean
public ServletRegistrationBean<Servlet> getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean<Servlet> registrationBean = new ServletRegistrationBean<>(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/actuator/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
package com.zyl.springcloud.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.zyl.springcloud.client.UserFeignClient;
import com.zyl.springcloud.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @className: UserController
* @description: 服务消费者控制器
* @author: ZYL
* @date: 2020/7/3 15:43
*/
@RestController
public class UserController {
@Autowired
private UserFeignClient userFeignClient;
//查询所有用户的信息
@GetMapping("/getAllUser")
@HystrixCommand(fallbackMethod="getAllUser")
public List<User> getAllUser() {
return userFeignClient.getAllUser();
}
//根据用户名获取用户信息
@GetMapping("/getUserByName/{name}")
@HystrixCommand(fallbackMethod="getUserByName")
public User getUserByName(@PathVariable String name) {
return userFeignClient.getUserByName(name);
}
//新增用户
@PostMapping("/addUser/{name}/{age}/{sex}")
@HystrixCommand(fallbackMethod="addUser")
public String addUser(User user) {
return userFeignClient.addUser(user.getName(), user.getAge(), user.getSex());
}
//删除用户
@DeleteMapping("/deleteUser/{id}")
@HystrixCommand(fallbackMethod="deleteUser")
public String deleteUser(@PathVariable Integer id) {
return userFeignClient.deleteUser(id);
}
//修改用户信息
@PutMapping("/updateUser/{id}/{name}/{age}/{sex}")
@HystrixCommand(fallbackMethod="updateUser")
public String updateUser(User user) {
return userFeignClient.updateUser(user.getId(), user.getName(), user.getAge(), user.getSex());
}
}
刷新服务消费者页面,当后台服务器出现异常时,显示fallback方法返回的错误提示信息
图形化展示方法的连通情况
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API
的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud
Netflix
中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
路由在微服务体系结构的一个组成部分。例如,/可以映射到您的Web应用程序,/api/users
映射到用户服务,并将/api/shop
映射到商店服务。Zuul
是Netflix
的基于JVM
的路由器和服务器端负载均衡器。
Netflix使用Zuul进行以下操作:
Zuul
的规则引擎允许基本上写任何JVM语言编写规则和过滤器,内置Java
和Groovy
。
服务网关 = 路由转发 + 过滤器
1、路由转发:接收一切外界请求,转发到后端的微服务上去;
2、过滤器:在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成(其实路由转发也是通过过滤器实现的)。
<!--导入springBoot依赖包 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<!--依赖管理,用于管理spring-cloud的依赖 -->
<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>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.zyl.springcloud</groupId>
<artifactId>springcloud-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--导入springCloudjar包 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<!--引入springBoot jar包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--支持热部署 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>1.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!--引入springBoot监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--添加eureka 客户端地址 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--添加zuul支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--添加断路器配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
server:
port: 9050
servlet:
context-path: /
spring:
application:
name: zuul #定义服务名称
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka
instance:
instance-id: zuul-9050 #定义微服务的名称
prefer-ip-address: true #是否显示IP和端口
zuul:
prefix: /api #通过统一的公共前缀访问
# ignored-services: springcloud-user #禁止通过某个服务名访问
ignored-services: "*" #禁止通过全部服务名访问
routes:
user-service:
serviceId: consumer-user #映射的服务名称
path: /user/** #浏览器输入地址
management: #路由映射显示
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: ALWAYS
package com.zyl.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableZuulProxy
public class Zuul_9050 {
public static void main(String[] args) {
SpringApplication.run(Zuul_9050.class, args);
}
@Bean
public TokenFilter tokenFilter() {
return new TokenFilter();
}
@Bean
public PasswordFilter PasswordFilter() {
return new PasswordFilter();
}
}
package com.zyl.springcloud;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
/**
* @className: ZuulFallback
* @description: 描述
* @author: ZYL
* @date: 2020/7/6 17:14
*/
@Component
public class ZuulFallback implements FallbackProvider {
@Override
public String getRoute() {
return "*";//对所有服务调用支持回退
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.OK.value();
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.OK.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("服务不可用".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
headers.setContentType(mt);
return headers;
}
};
}
}
package com.zyl.springcloud;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
/**
* @className: TokenFilter
* @description: 过滤器token
* @author: ZYL
* @date: 2020/7/7 9:33
*/
public class TokenFilter extends com.netflix.zuul.ZuulFilter {
private final Logger LOGGER = LoggerFactory.getLogger(TokenFilter.class);
@Override
public String filterType() {
return "pre";//在请求通过路由之前调用
}
@Override
public int filterOrder() {
return 0;// filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
}
@Override
public boolean shouldFilter() {
return true;// 是否执行该过滤器,此处为true,说明需要过滤
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
LOGGER.info("TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString());
String token = request.getParameter("token");// 获取请求的参数
if (StringUtils.isNotBlank(token)) {
ctx.setSendZuulResponse(true); //对请求进行路由
ctx.setResponseStatusCode(200);
ctx.set("isSuccess", true);
return null;
} else {
ctx.setSendZuulResponse(false); //不对其进行路由
ctx.setResponseStatusCode(400);
ctx.setResponseBody("token is empty");
ctx.set("isSuccess", false);
return null;
}
}
}
package com.zyl.springcloud;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
/**
* @className: PasswordFilter
* @description: 过滤器password
* @author: ZYL
* @date: 2020/7/7 10:12
*/
public class PasswordFilter extends com.netflix.zuul.ZuulFilter {
private final Logger LOGGER = LoggerFactory.getLogger(TokenFilter.class);
@Override
public String filterType() {
return "post"; //请求处理完成后执行过滤器
}
@Override
public int filterOrder() {
return 1; //优先级为1,数字越大,优先级越低
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return (boolean) ctx.get("isSuccess");//判断上一个过滤器结果是否为true,否则就不走下面过滤器,直接跳过后面的所有过滤器并返回
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
LOGGER.info("PasswordFilter {},{}", request.getMethod(), request.getRequestURL().toString());
String password = request.getParameter("password");
if (null != password && password.equals("123456")) {
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
ctx.set("isSuccess", true);
return null;
} else {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(400);
ctx.setResponseBody("The password cannot be empty");
ctx.set("isSuccess", false);
return null;
}
}
}
在地址栏输入token的值和password的值
中心服务器,从而能够提供更好的管理、发布能力。SpringCloudConfig
分服务端和客户端,服务端负责将git svn
中存储的配置文件发布成REST
接口,客户端可以从服务端REST接口获取配置。但客户端并不能主动感知到配置的变化,从而主动去获取新的配置,这需要每个客户端通过POST
方法触发各自的/refresh
。
SpringCloudBus
通过一个轻量级消息代理连接分布式系统的节点。这可以用于广播状态更改(如配置更改)或其他管理指令。SpringCloudBus
提供了通过POST
方法访问的endpoint/bus/refresh
,这个接口通常由git
的钩子功能调用,用以通知各个SpringCloudConfig
的客户端去服务端更新配置。
注意:这是工作的流程图,实际的部署中SpringCloudBus
并不是一个独立存在的服务,这里单列出来是为了能清晰的显示出工作流程。
下图是SpringCloudConfig
结合SpringCloudBus
实现分布式配置的工作流
在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud
中,有分布式配置中心组件spring cloud config
,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。在spring cloud config
组件中,分两个角色,一是config server
,二是config client
,业界也有些知名的同类开源产品,比如百度的disconf
。
相比较同类产品,SpringCloudConfig
最大的优势是和Spring
无缝集成,支持Spring
里面Environment
和PropertySource
的接口,对于已有的Spring
应用程序的迁移成本非常低,在配置获取的接口上是完全一致,结合SpringBoot
可使你的项目有更加统一的标准(包括依赖版本和约束规范),避免了应为集成不同开软件源造成的依赖版本冲突。
<dependencies>
<!--springcloud配置中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!--引入springBoot监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--添加eureka 客户端地址 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
server:
port: 9080
servlet:
context-path: /
spring:
application:
name: config-service #定义服务名称
config:
name: config-service #定义配置文件名
cloud:
config:
server:
git:
uri: https://gitee.com/geecoder/springcloud-config #连接远程git账户
search-paths: /** #查找git仓库的路径
label: master #查找的git分支
# username: #私有仓库需要配置用户名和密码
# password:
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka
instance:
instance-id: config-service-9080 #定义微服务的名称
prefer-ip-address: true #是否显示IP和端口
package com.zyl.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication(exclude= {
DataSourceAutoConfiguration.class})//排除数据源启动,否则会报错
@EnableConfigServer//开启配置中心设置
public class ConfigService_9080 {
public static void main(String[] args) {
SpringApplication.run(ConfigService_9080.class, args);
}
}
http请求地址和资源文件映射如下:
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
<dependencies>
<!--springcloud客户端配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入springBoot监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--添加eureka 客户端地址 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
bootstrao.yml会在程序引导时执行,应用于更加早期配置信息读取。
application.yml:
server:
port: 9090
servlet:
context-path: /
spring:
application:
name: config-client #定义服务名称
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka
instance:
instance-id: config-client-9090 #定义微服务的名称
prefer-ip-address: true #是否显示IP和端口
bootstrap.yml:
spring:
cloud:
config:
name: springcloudconfig #对应config server Url中的{
application}
profile: dev #动态加载开发环境配置文件
label: master #获取master默认分支
uri: http://localhost:9080/ #配置服务中心地址
package com.zyl.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class ConfigClient_9090 {
public static void main(String[] args) {
SpringApplication.run(ConfigClient_9090.class, args);
}
}
package com.zyl.springcloud;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @className: ConfigController
* @description: 描述
* @author: ZYL
* @date: 2020/7/7 15:50
*/
@RestController
public class ConfigController {
@Value("${content}")//content是git文件中的key
String content;
@GetMapping("/config")
public String config() {
return "content: " + content;
}
}