1.为什么需要使用
单体应用
一个应用,作为一个完整的服务,提供给用户。
各个模块高度耦合,不便于后期维护;
某个部分出问题了,整个服务不可用;
局部拓展,比如整个应用中的用户模块,在注册有优惠活动时,难以抵住压力,可能需要进行集群扩展微服务
很多微小的服务,组成提供给用户的完整的服务。
是一种架构模式,SpringCloud是其一个具体的实现方式。
各个服务低耦合,通过网络协议(Http,RPC)进行通信,传输数据。SpringCloud
基于SpringBoot实现的服务治理工具包,用于在微服务架构中管理和协调服务。SpringCloud和Dubbo
Dubbo是RPC的框架
SpringCloud是一系列微服务解决方案
Dubbo基于TCP
SpringCloud于Http
其实Http也是基于TCP,所以Dubbo通信方面的性能高于SpringCloud
2.搭建
2.1基础搭建
2.1.1. 注册中心
注册中心原理
微服务启动时会把自己的通信地址(ip+端口:服务名)注册到注册中心
微服务会从注册中心拉取一份地址清单
当服务发起调用时,就会从拉取的地址清单中找到目标服务地址,进行调用
注册中心使用心跳机制实现服务的动态上下线,间隔单位时间,检查服务的健康状态,将下线的微服务从地址清单中删除掉,其他微服务同步地址清单搭建
-
在Spring官网上找到SpringCloud的EurekaClient部分
选择PROJECTS
选择SpringCloud
选择Learn下的参考文档
选择一个网页
找到Eurekake Client -
Idea中新建一个Maven SpringBoot项目
修改pom.xml为如下:
这里使用的SpringBoot版本如下:<version>2.0.5.RELEASE</version>
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> </parent> <groupId>com.demo.springcloud</groupId> <artifactId>eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>eureka-server</name> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
pom.xml中加入spring-cloud的依赖和eureka的依赖
<spring-cloud.version>Finchley.SR1</spring-cloud.version>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
-
resources增加 application.yml
server: port: 1000 eureka: instance: hostname: localhost #主机名,此处为注册中心 client: registerWithEureka: false #不允许注册中心服务注册到注册中心 fetchRegistry: false #不允许注册中心服务,拉取服务地址列表 serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #注册中心地址
-
主配置类上开启Eureka服务
@SpringBootApplication
@EnableEurekaServer
-
启动后浏览器访问
localhost:1000
-
在此基础上搭建多模块的maven,结构如下:
springcloud-parent
– springcloud-eureka-server-1000
– springcloud-user-server-2000
–springcloud-order-server-3000
springcloud-parent为空的Maven父项目,包含下面3个的内容
此处的1000-3000为服务的端口,eureka-server为Eureka服务端,
user-server和order-server均为其Eureka客户端。
-
2.1.2. 提供者服务
-
引入依赖
<!--EurekaClient依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--webmvc支持,EurekaClient启动需要该依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
-
application.yml
server: port: 2000 eureka: instance: instance-id: user-server-2000 #实例id prefer-ip-address: true #使用ip注册到注册中心 client: serviceUrl: defaultZone: http://localhost:1000/eureka/ #服务名称 spring: application: name: user-server-2000
-
刷新local:1000,看是否有user-server-2000 ,有则成功
2.1.3. 消费者服务
步骤同上,增加order-server-3000
2.1.4. 修改IP注册
eureka:
instance:
instance-id: user-server-2000 #实例id
prefer-ip-address: true #使用ip注册到注册中心
#服务名称
spring:
application:
name: user-server-2000
2.2EurekaServer高可用集群
2.2.1为何使用
如果只有一个EurekaSever,如果EurekaSever挂了那么整个微服务都不可用
2.2.2搭建
-
创建两个本地域名 C:\Windows\System32\drivers\etc\hosts
127.0.0.1 peer1
127.0.0.1 peer2 -
修改注册中心eureka-server-1000
server: port: 1000 eureka: instance: hostname: peer1 client: registerWithEureka: false #禁用注册中心向自己注册 fetchRegistry: false #不让注册中心获取服务的注册列表 serviceUrl: defaultZone: http://peer2:1001/eureka/ #注册中心的注册地址 ,其他微服务需要向这个地址注册
-
搭建第二个注册中心eureka-server-1001 - 复制
server: port: 1001 eureka: instance: hostname: peer2 client: registerWithEureka: false #禁用注册中心向自己注册 fetchRegistry: false #不让注册中心获取服务的注册列表 serviceUrl: defaultZone: http://peer1:1000/eureka/ #注册中心的注册地址 ,其他微服务需要向这个地址注册
-
修改Eureka客户端
eureka: client: serviceUrl: defaultZone: http://peer1:1000/eureka/,http://peer2:1001/eureka/ #注册中心地址 ...
2.3Ribbon
2.3.1使用restTemplate发送服务间请求
- 主配置类配置RestTemplate
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
- 控制层调用
@Autowired
private RestTemplate restTemplate;
@GetMapping("/getById/{id}")
public User getById(@PathVariable("id")Long id){
通过ip地址+端口发送请求
String url = "http://localhost:2000/user/getById/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
2.3.2当服务有多个时,使用Ribbon进行负载均衡
- 引入Ribbon依赖
<!--ribbon依赖,依赖处理服务调用时,多个服务集群时,进行负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
- 主配置类的RestTemplate加上
@LoadBalanced
注解
/**
* Spring管理RestTemplate,注入即可发送请求
* @LoadBalanced : 赋予restTemplate负载均衡的能力
* @return
*/
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
- 控制层使用服务名称进行调用
@GetMapping("/getById/{id}")
public User getById(@PathVariable("id")Long id){
// 通过ip地址+端口发送请求
// String url = "http://localhost:2000/user/getById/"+id;
// 通过服务名调用:user-server
String url = "http://user-server/user/getById/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
2.3.3负载均衡策略
- 默认轮询
- 自定义配置,主配置类配置IRule即可
/**
* 负载均衡规则:随机,默认轮询
* @return
*/
@Bean
public IRule myRule(){
return new RandomRule();
}
2.4Feign
2.4.1使用场景
基于Ribbon,使得服务间调用像是本地调用一样
2.4.2搭建
- 新建模块springcloud-pay-server-4000
- pom.xml引入feign支持
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 新建接口UserFeignClient
/**
* 表明这是Feign的接口,指定服务名称
*/
@FeignClient("user-server")
public interface UserFeignClient {
/**
* 和服务调用处一致
* @param id
* @return
*/
@GetMapping("/user/getById/{id}")
public User getById(@PathVariable("id")Long id);
}
- 主配置类开启Feign支持
@EnableFeignClients
- 控制层调用
@Autowired
private UserFeignClient feignClient;
@GetMapping("/getById/{id}")
public User getById(@PathVariable("id")Long id){
// 通过feignClient接口进行调用
User user = feignClient.getById(id);
return user;
}
2.5Hystrix
2.5.1为什么使用
- 雪崩
一个微服务的故障导致整个微服务调用链全部瘫痪 - 什么是Hystrix
解决服务器故障(雪崩)的一个组件 ,它可以实现:隔离 ,熔断 ,降级,缓存 - 隔离 :包括线程池隔离和信号量隔离,限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。
- 熔断 :当请求次数达到规定的阀值都出现服务故障(超时),Hystrix就把服务标记为短路状态.
正常情况下,断路器处于关闭状态(Closed),如果调用持续出错或者超时,电路被打开进入熔断状态(Open),后续一段时间内的所有调用都会被拒绝(Fail Fast),一段时间以后,保护器会尝试进入半熔断状态(Half-Open),允许少量请求进来尝试,
如果调用仍然失败,则回到熔断状态
如果调用成功,则回到电路闭合状态; - 降级 :高并发情况下 ,为了保证一些主要的服务有足够的资源不出问题 ,会认为的关掉一些无关紧要的服务,然后返回一些托底的数据,给用户一个友好的提示。
- 缓存 :Hystrix内部会把请求做缓存
2.5.2Ribbon搭建Hystrix
- pom.xml引入Hystrix
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
- 主配置类加上
@EnableCircuitBreaker
- 调用方法处加注解,并设置托底方法,完成托底方法
/** * @HystrixCommand(fallbackMethod = "getUserIdFallback") : * 该请求需要做出问题时熔断处理,指定托底方法返回数据给用户 * @param id * @return */ @GetMapping("/getById/{id}") @HystrixCommand(fallbackMethod = "getUserIdFallback") public User getById(@PathVariable("id")Long id){ // 通过ip地址+端口发送请求 // String url = "http://localhost:2000/user/getById/"+id; // 通过服务名调用:user-server String url = "http://user-server/user/getById/"+id; User user = restTemplate.getForObject(url, User.class); return user; } public User getUserIdFallback(@PathVariable("id")Long id){ return new User(id,"不知何许人也","用户服务暂不可用"); }
- 断开调用的微服务,进行访问
2.5.3Feign搭建Hystrix
- application.yml开启feign
#feign的hsytrix开启 feign : hystrix : enabled : true
- Feign的客户端接口注解加上托底类
/** * 表明这是Feign的接口,指定服务名称 */ @FeignClient(value = "user-server",fallback = UserFeignClientFallBack.class) public interface UserFeignClient { /** * 和服务调用处一致 * @param id * @return */ @GetMapping("/user/getById/{id}") public User getById(@PathVariable("id")Long id); }
- . 托底类实现客户端接口,重写托底方法
@Component public class UserFeignClientFallBack implements UserFeignClient { @Override public User getById(Long id) { return new User(-1L,"不知何许人也","用户服务暂不可用"); } }
2.6Zuul
2.6.1基本概念
Zuul是一个SpringCloud的网关组件 , 它是微服务的入口,网络关卡 ,通过Zuul我们可以实现请求的分发(负载均衡),鉴权,限流等等操作。
2.6.2搭建基本
- 引入依赖
<!--zuul依赖路由分发,过滤-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
- 配置文件
zuul:
ignoredServices: "*" #拦截请求,使不能通过服务名访问
prefix: "/service"
routes:
pay-server: "/pays/**" #pay-server服务可以通过/pays/**访问 http://localhost:5000/service/pays/pay/getById/1
- 主配置类
@EnableZuulProxy
- 访问
http://localhost:5000/service/pays/pay/getById/1
2.6.3Zuul原理
- 正常流程:
- 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
- 异常流程:
- 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
- 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
- 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。
2.6.4自定义Filter实现登录检查
自定义过滤器,继承ZuulFilter抽象类,实现其中的方法
/**
* 自定义Zuul过滤器,实现登录检查
*/
@Component
public class MyZuulFilter extends ZuulFilter{
public static final String PRE_TYPE = "pre";
public static final int PRE_FILTER_ORDER = 5;
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return PRE_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
response.setContentType("text/json;charset=utf-8;");
if(!StringUtils.hasLength(request.getHeader("token"))){
try {
response.getWriter().write("请登录先...");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(HttpServletResponse.SC_UNAUTHORIZED);
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}