第1章 SpringCloud
学习目标:
- 掌握架构演进过程
- 理解微服务拆分流程及远程调用过程
- 掌握注册中心Eureka的使用
- 掌握负载均衡Ribbon的使用
- 能够基于Feign实现服务远程调用
1 服务架构演进
章节知识点
- 单体架构
- 分布式架构
- 微服务架构
- SpringCloud
过去的互联网:
1:用户量不多
2:并发低
3:数据少
现在的互联网:
1:用户多
2:并发高
3:数据庞大
互联网架构从简到繁的演进经历了单体架构、分布式架构、SOA架构、微服务架构以及最新的service mesh的演进过程。
1.1 单体架构
1)概念
早期互联网产品用户量少,并发量低,数据量小,单个应用服务器可以满足需要,这就是最早互联网架构。我们用一句话总结什么是单体架构:将业务的所有功能集中在一个项目中开发,部署为一个节点。
2)架构图
3)优缺点
#优点:
1)架构简单
2)部署成本低
3)性能极强
#缺点:
1)耦合度高
1.2 分布式架构
1)概念
根据业务功能对系统进行拆分,每个业务模块称为一个服务。
2)架构图
3)优缺点
#优点
1)降低服务耦合度
2)有利于服务升级拓展
#缺点
1)维护成本增加
2)服务间调用复杂度增加
1.3 微服务
1)概念
微服务是系统架构的一种设计风格,将一个原本独立的服务拆分成多个小型服务,每个服务独立运行在在各自的进程中,服务之间通过 HTTP RESTful API 进行通信.每个小型的服务都围绕着系统中的某个耦合度较高的业务进行构建。
#微服务是一种经过良好设计的分布式架构方案,而全球的互联网公司都在积极尝试自己的微服务落地方案。其中在java领域最引人注目的是SpringCloud提供的方案。
2)架构图
3)微服务架构特征
单一职责:微服务拆分粒度更小,每个服务都应对唯一的业务能力,做到单一职责
自治:团队独立、技术独立、数据独立,独立部署和交付
面向服务:服务提供统一标准的接口,与语言无关、与技术无关
隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
4)需要解决的问题
1)服务拆分粒度如何?
2)服务之间如何实现调用?
3)服务关系如何管理?
1.4 SpringCloud
- SpringCloud是目前国内使用最广泛的微服务技术栈。官网地址:https://spring.io/projects/spring-cloud。
- SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:
- SpringCloud与SpringBoot的版本兼容关系如下:
- 我们课堂学习的版本是 Hoxton.SR10,因此对应的SpringBoot版本是2.3.x版本。
总结
- 单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
- 分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝
- 微服务:一种良好的分布式架构方案
- 优点:拆分粒度更小、服务更独立、耦合度更低
- 缺点:架构非常复杂,运维、监控、部署难度提高
- SpringCloud是微服务架构的一站式解决方案的技术栈并不是框架,而是集成了各种优秀微服务功能组件
2 服务拆分及远程调用
章节知识点
- 远程调用案例业务介绍
- 工程导入
- 使用RestTemplate实现远程调用
- RestTemplate源码剖析
- 生产者、消费者概念
案例说明:管理员查询订单详情->根据订单id查询订单的同时,把订单所属的用户信息一起返回,如下图:
2.1 工程导入
1)SQL导入
将资料\工程\springcloud-parent\sql脚本
中的springcloud-order.sql
和springcloud-user.sql
分别导入到数据库中(分别是两个独立数据库)。
2)工程导入
将资料\工程\springcloud-parent
导入到IDEA中
查询某用户详情信息:http://localhost:18081/user/zhangsan
查询某订单详情信息:http://localhost:18082/order/1
2.2 远程调用
1)RestTemplate介绍
RestTemplate 是spring家族中一款基于http协议的组件,他的作用就是:用来实现基于http的协议方式的服务之间的通信(也就是远程服务调用)。
RestTemplate 采用同步方式执行 HTTP 请求,底层使用 JDK 原生 HttpURLConnection API 。
#概念总结:RestTemplate是spring提供的一个用来模拟浏览器发送请求和接收响应的一个类,它能基于Http协议实现远程调用。
2)注册RestTemplate
在itheima-order
的OrderApplication
中把RestTemplate
注册到容器中:
上图代码如下:
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
/***
* 注册RestTemplate
*/
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
3)远程调用
修改itheima-order
中的OrderServiceImpl
的one
方法
上图代码如下:
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private RestTemplate restTemplate;
/**
* 根据ID查询订单信息
*/
@Override
public OrderInfo one(Integer id) {
//1.查询订单
OrderInfo orderInfo = orderDao.one(id);
//2.根据订单查询用户信息->需要调用 【item-user】 服务
User user = restTemplate.getForObject("http://localhost:18081/user/"+orderInfo.getUserName(), User.class);
//3.封装user
orderInfo.setUser(user);
//4.返回订单信息
return orderInfo;
}
}
2.3 RestTemplate源码剖析
下面是RestTemplate部分源码,我们可以看到执行过程中采用了Http请求。
沿着RestTemplate.doExecute()
往下看相关源码:
一直往后跟踪,可以发现会调用sun.net.www.protocol.http.HttpURLConnection.connect()
实现远程调用:
2.4 提供者与消费者
服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
在上面案例中itheima-order
调用了itheima-user
提供的接口,所以itheima-order
是服务消费者,itheima-user
是服务提供者。
总结
- RestTemplate使用有2个步骤:
- 1)注册RestTemplate到springboot容器中
- 2)使用restTemplate.getForObject(url,T.class)远程调用
- RestTemplate底层是封装了Http请求
- 生产者、消费者
- 服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
- 服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
3 注册中心-Eureka
章节知识点
- 注册中心的作用讲解
- EurekaServer搭建
- 生产者注册
- 消费者注册
3.1 Eureka的作用
按照上面调用流程,消费者调用服务者存在很多问题:
1:服务消费者该如何获取服务提供者的地址信息?
2:如果有多个服务提供者,消费者该如何选择?
3:消费者如何得知服务提供者的健康状态?
Eureka注册中心如何解决上面的问题?
Eureka工作原理
#1:消费者该如何获取服务提供者具体信息?
服务提供者启动时向eureka注册自己的信息
eureka保存这些信息
消费者根据服务名称向eureka拉取提供者信息
#2:如果有多个服务提供者,消费者该如何选择?
服务消费者利用负载均衡算法,从服务列表中挑选一个
#3:消费者如何感知服务提供者健康状态?
服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
eureka会更新记录服务列表信息,心跳不正常会被剔除
消费者就可以拉取到最新的信息
3.2 Eureka注册中心实战
3.2.1 搭建EurekaServer
搭建EurekaServer服务步骤如下:
1)创建EurekaServer工程
2)pom.xml 导入依赖
创建项目itheima-eurekaserver
,引入spring-cloud-starter-netflix-eureka-server
的依赖:
<!--EurekaServer包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
3)启动类
创建启动类com.itheima.EurekaServerApplication
,代码如下:
@SpringBootApplication
@EnableEurekaServer // 打上启动eurekaserver服务的注解
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class,args);
}
}
注解说明:
@EnableEurekaServer:开启EurekaServer功能
4)核心配置文件application.yml
server:
port: 8001 #端口号
spring:
application:
name: eureka-server # 应用名称,会在Eureka中作为服务的id标识(serviceId)
eureka:
client:
register-with-eureka: false #是否将自己注册到Eureka中
fetch-registry: false #是否从eureka中获取服务信息
service-url:
defaultZone: http://localhost:8001/eureka #eureka服务器地址
此时我们访问EurekaServer地址http://localhost:8001/
,效果如下:
3.2.2 生产者注册
将itheima-user服务注册到EurekaServer步骤如下:
1)pom.xml
在itheima-user
添加如下依赖:
<!--EurekaClient包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2)修改application.yml
修改itheima-user
的application.yml
,添加如下配置:
配置如下:
server:
port: 18081
spring: # 下面会用到拼接
application:
name: user
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springcloud-user?characterEncoding=UTF-8&&serverTimezone=GMT
username: root
password: 123456
eureka:
client:
service-url:
# EurekaServer的地址
defaultZone: http://localhost:8001/eureka
instance:
prefer-ip-address: true
# 下面是配置文件中的spring:
application:
name:
的拼接 server.port同理
@project.version@是项目工程加工程版本号
instance-id: ${spring.application.name}:${server.port}:@project.version@
3)多实例启动
分别启动3个服务配置,Eureka(http://localhost:8001/)信息如下:
3.3.3 消费者注册
itheima-order
虽然是消费者,但与itheima-user
一样都是使用eureka
的client
端,同样可以实现服务注册:
在itheima-order
项目引入spring-cloud-starter-netflix-eureka-client
的依赖
1)pom.xml
在itheima-order
的pom.xml
中引入如下依赖
<!--EurekaClient包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2)修改application.yml
修改itheima-order
的application.yml
,添加如下配置:
代码如下:
消费者配置跟提供者配置是基础相同的
server:
port: 18082
spring: # 下面会用到拼接
application:
name: user # 服务的名字(远程调用的)
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springcloud-user?characterEncoding=UTF-8&&serverTimezone=GMT
username: root
password: 123456
eureka:
client:
service-url:
# EurekaServer的地址
defaultZone: http://localhost:8001/eureka
instance:
prefer-ip-address: true
# 下面是配置文件中的spring:
application:
name:
的拼接 server.port同理
@project.version@是项目工程加工程版本号
instance-id: ${spring.application.name}:${server.port}:@project.version@
3.3.4 远程调用
在itheima-order
完成服务拉取实现远程调用,服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡。
修改itheima-order
的OrderServiceImpl的代码,修改访问的url路径,用服务名代替ip、端口,代码如下:
在itheima-order项目的启动类OrderApplication中的RestTemplate添加负载均衡注解:
上图代码如下:
/***
* 注册RestTemplate
*/
@Bean
@LoadBalanced//开启负载均衡
public RestTemplate restTemplate(){
return new RestTemplate();
}
注解说明:
@LoadBalanced //开启负载均衡 注{无论是单台还是集群都需要打上eureka的负载均衡注解}
我们访问http://localhost:18082/order/1
测试效果如下:
3.3.5 Eureka配置
服务注册时默认使用的是主机名,如果我们想用ip进行注册,可以在application.yml添加配置:
eureka:
client:
service-url:
# EurekaServer的地址
defaultZone: http://localhost:8001/eureka
instance:
prefer-ip-address: true # 在服务器中默认以ip地址的形式展示,否则会以电脑别名展示
instance-id: ${spring.application.name}:${server.port}:@project.version@
lease-renewal-interval-in-seconds: 30 # 心跳周期,默认是30秒
lease-expiration-duration-in-seconds: 90 # 心跳失败最长超时间,默认90秒
ip-address: localhost # ip地址
总结
- 搭建EurekaServer
- 引入eureka-server依赖
- 添加@EnableEurekaServer注解
- 在application.yml中配置eureka地址
- 服务注册
- 引入eureka-client依赖
- 在application.yml中配置eureka地址
- 服务发现
- 引入eureka-client依赖
- 在application.yml中配置eureka地址
- 给RestTemplate添加@LoadBalanced注解
- 用服务提供者的服务名称远程调用
4 负载均衡Ribbon
章节知识点
- 负载均衡流程讲解
- 负载均衡算法学习
- Ribbon负载均衡使用
什么是Ribbon?
Ribbon是Netflix发布的负载均衡器,有助于控制HTTP客户端行为。为Ribbon配置服务提供者地址列表后,Ribbon就可基于负载均衡算法,自动帮助服务消费者请求。
概念:Ribbon是基于Http协议请求的客户端负载均衡器,能实现很丰富的负载均衡算法。
4.1 负载均衡流程
负载均衡流程如上图所示:
1:用户发起请求,会先到达itheima-order服务
2:itheima-order服务通过Ribbon负载均衡器从eurekaserver中获取服务列表
3:获取了服务列表后,轮询(负载均衡算法)调用
4.2 负载均衡算法
轮询调用会涉及到很多负载均衡算法,负载均衡算法比较多,关系图如下:
Ribbon的负载均衡算法策略如下表:
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的 <clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit 属性进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略哪些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻辑 |
4.3 Ribbon负载均衡算法使用
Ribbon负载均衡算法的使用有2种方式
1)代码方式
- 注册IRule接口的实现类(负载均衡算法):在
itheima-order
的启动类中添加如下负载均衡注册代码:
```java /**
- 随机负载均衡算法
- @return */ @Bean public IRule randomRule() { return new RandomRule(); } ```
- 注册IRule接口的实现类(负载均衡算法):在
2)配置方式
- 为指定服务配置负载均衡算法:在
itheima-order
的核心配置文件中添加如下配置:#指定服务使用指定负载均衡算法 user: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡规则
- 为指定服务配置负载均衡算法:在
饥饿加载:
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
#饥饿加载
ribbon:
eager-load:
clients: user #开启饥饿加载
enabled: true #指定对user这个服务饥饿加载
总结
- Ribbon负载均衡规则
- 规则接口是IRule
- 默认实现是ZoneAvoidanceRule,根据zone选择服务列表,然后轮询
- 负载均衡自定义方式
- 代码方式:配置灵活,但修改时需要重新打包发布
- 配置方式:直观,方便,无需重新打包发布,但是无法做全局配置
- 饥饿加载
- 开启饥饿加载
- 指定饥饿加载的微服务名称
5 http客户端Feign
章节知识点
- Feign介绍
- Feign入门案例学习
- Feign日志功能、性能优化、最佳实践讲解
5.1 Feign介绍
先来看我们以前利用RestTemplate发起远程调用的代码:
User user = restTemplate.getForObject("http://user/user/"+orderInfo.getUserName(), User.class);
存在下面的问题:
- 代码可读性差,编程体验不统一
- 参数复杂URL难以维护
上面RestTemplate存在的问题可以使用Feign解决,那么什么是Feign?
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
5.2 Feign入门案例
定义和使用Feign客户端的步骤如下:
1:引入依赖包 spring-cloud-starter-openfeign
2:添加注解@EnableFeignClients开启Feign功能
3:定义远程调用接口,在接口中知名远程调用的【服务名字】、【方法签名】
4:注入接口,执行远程调用
1)引入依赖
在itheima-order
中引入如下依赖:
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2)开启Feign功能
在itheima-order
的启动类OrderApplication启动类
添加@EnableFeignClients
注解开启Feign功能,代码如下:
@SpringBootApplication
@EnableFeignClients // 添加feign注解
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
//...其他略
}
3)定义远程调用接口
在itheima-order
中创建接口UserClient
,代码如下:
代码如下:
package com.itheima.feign;
import com.itheima.user.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// p1:value=user:是配置文件的服务标识、p2:path=user:是下面代码的访问路径
@FeignClient(value = "user" ,path = "/user")
public interface UserClient {
//@GetMapping(value = "/user/{username}")
@GetMapping(value = "/{username}")
User one(@PathVariable(value = "username")String username);
}
主要是基于SpringMVC的注解来声明远程调用的信息,比如:
- 服务名称:user
- 请求方式:GET
- 请求路径:/user/{username}
- 请求参数:String username
- 返回值类型:User
4)远程调用
修改itheima-order
的OrderServiceImpl.one()
方法,执行远程调用,代码如下:
上图代码如下:
@Autowired
private UserClient userClient;
/**
* 根据ID查询订单信息
*/
@Override
public OrderInfo one(Integer id) {
//1.查询订单
OrderInfo orderInfo = orderDao.one(id);
//2.根据订单查询用户信息->需要调用 【item-user】 服务
//基于RESTful风格调用
User user = userClient.one(orderInfo.getUserName());
//3.封装user
orderInfo.setUser(user);
//4.返回订单信息
return orderInfo;
}
5.3 Feign其他功能
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
一般我们需要配置的就是日志级别。
5.3.1 Feign日志配置
Feign的日志级别有4种,但要想让日志生效,得结合着SpringBoot的日志配置一起使用
NONE:默认的,不显示任何日志
BASIC:仅记录请求方法、URL、响应状态码以及执行时间
HEADERS:除了BASIC中定义的信息以外,还有请求和响应的头信息
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据
SpringBoot日志配置;
logging:
level:
# feign 日志以什么级别监控哪个接口
com.itheima: debug
配置Feign日志有两种方式:
配置文件方式
全局生效
feign: client: config: default: #这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置 loggerLevel: FULL #日志级别
局部生效
feign: client: config: user: #指定服务 loggerLevel: FULL #日志级别
代码方式
- 注册日志级别
```java /**
- 注册日志级别
- @return */ @Bean public Logger.Level feignLogLevel() { return Logger.Level.FULL; } ```
全局生效
#如果是全局配置,则把它放到@EnableFeignClients这个注解中 @EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
局部生效
#如果是局部配置,则把它放到@FeignClient这个注解中 @FeignClient(value = "user",configuration = FeignClientConfiguration.class)
- 注册日志级别
5.3.2 Feign性能优化
Feign底层的客户端实现:
- URLConnection:默认实现,不支持连接池
- Apache HttpClient :支持连接池
- OKHttp:支持连接池
因此优化Feign的性能主要包括:
- 使用连接池代替默认的URLConnection
- 日志级别,最好用basic或none
Feign切换Apache HttpClient步骤如下:
1:引入依赖
2:配置连接池
1)引入依赖
在itheima-order
中引入如下依赖:
<!--httpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2)配置连接池
在itheima-order
的核心配置文件application.yml
中添加如下配置:
feign:
client:
config:
default: #这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: BASIC #日志级别
user: #指定服务
loggerLevel: BASIC #日志级别
httpclient:
enabled: true #开启feign对HttpClient的支持
max-connections: 200 #最大的连接数
max-connections-per-route: 50 #每个路径的最大连接数
5.3.3 Feign最佳实现
方式一(继承):给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。
- 服务紧耦合
- 父接口参数列表中的映射不会被继承
方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种方式解决:
方式一:指定FeignClient所在包
@EnableFeignClients(basePackages = "com.itheima.user.feign")
方式二:指定FeignClient字节码
@EnableFeignClients(clients = {UserClient.class})
总结
- Feign的使用步骤
- 引入依赖
- 添加@EnableFeignClients注解
- 编写FeignClient接口
- 使用FeignClient中定义的方法代替RestTemplate
- Feign的日志配置:
- 方式一是配置文件,feign.client.config.xxx.loggerLevel
- 如果xxx是default则代表全局
- 如果xxx是服务名称,例如userservice则代表某服务
- 方式二是java代码配置Logger.Level这个Bean
- 如果在@EnableFeignClients注解声明则代表全局
- 如果在@FeignClient注解中声明则代表某服务
- Feign的优化
- 日志级别尽量用basic
- 使用HttpClient或OKHttp代替URLConnection
- 引入feign-httpClient依赖
- 配置文件开启httpClient功能,设置连接池参数
- Feign的最佳实践:
- 让controller和FeignClient继承同一接口
- 将FeignClient、POJO、Feign的默认配置都定义到一个项目中,供所有消费者使用