day-01
1. 微服务技术栈
2. 初识微服务框架
2.1 微服务架构的演变
- 单体架构
- 将业务的所有功能集中在一个项目中开发,打成一个包部署
- 优点:
- 架构简单
- 部署成本低
- 缺点:
- 耦合度高
- 扩展性差
- 分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务
- 优点:
- 降低服务耦合
- 有利于服务升级拓展
- 缺点:
- 架构复杂
- 难度大
- 优点:
- 微服务:是一种经过良好架构设计的分布式架构方案
- 优点:
- 拆分粒度更小
- 服务更独立
- 耦合度更低
- 缺点:
- 架构非常复杂
- 运维、监控、部署难度提高
- 特征:
- 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
- 面向服务:微服务对外暴露业务接口
- 自治:团队独立、技术独立、数据独立、部署独立
- 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
- 优点:
2.2 微服务技术对比
day-02
1. Eureka
提供者与消费者
- 提供者
- 一次业务中,被其他为服务调用的服务。(提供接口给其他微服务)
- 消费者
- 一次业务中,调用其他微服务的服务。(调用其他微服务提供的接口)
- 一个服务既可以是提供者也可以是消费者
- 比如 服务A调用服务B,服务B调用服务C
服务调用出现的问题
案例中,通过RestTemplete,发起http请求得到user数据,然后在订单模块显示出来,完成服务间的调用,但是出现一个问题,就是在地址编写的时候使用的是硬编码,即
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.利用restTemplate发起http请求
String url = "http://localhost:8081/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
// 3. 封装user到Oreder
order.setUser(user);
// 4.返回
return order;
}
意味着,要对其他服务发起请求,就需要更改代码,并且不能保证那台服务的健康性
- 服务消费者该如何获取服务提供者的地址信息?
- 服务提供者启动时向eureka注册自己的信息
- eureka保存这些信息
- 消费者根据服务名称向eureka拉取提供者信息
- 如果有多个服务提供者,消费者该如何选择?
- 服务消费者利用负载均衡算法,从服务列表中挑选一个
- 消费者如何得知服务提供者的健康状况?
- 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
- eureka会更新记录服务列表信息,信条不正常会被踢出
- 消费者就可以拉取到最新的信息
在Eureka架构中,微服务角色有两类
- EurekaServer:服务端(也被称为注册中心)
- 记录服务信息
- 心跳监控
- EurekaClient:客户端
- provider:服务提供者
- 注册自己的信息到EurekaServer
- 每隔30秒向注册中心发送心跳
- consumer:服务消费者
- 根据服务名称从注册中心中拉取服务列表
- 基于服务列表做负载均衡,选中一个微服务后发起远程调用
- provider:服务提供者
2. 搭建Eureka
2.1 搭建环境
搭建Eureka的三大步骤
- 引入eureka-server依赖
- 添加@EnableEurekaServer注解
- 在application.yml中配置Eureka地址
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
添加注解
添加到springBoot的启动类上
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class,args);
}
}
配置地址
server:
port: 12345 #服务端口
spring:
application:
name: eurekaserver #eureka的服务名称
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://127.0.0.1:12345/eureka
注:eureka注册中心,在启动时,也会对自己进行注册
2.2 服务注册
引入依赖
注意当前依赖为client
<!-- 引入eureka的client依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置yml文件
配置当前服务的服务名称 配置注册中心的地址信息
spring:
application:
name: userservice #user服务的服务名称
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://127.0.0.1:12345/eureka
注册成功后的注册中心控制台
2.3 服务发现
- 修改代码,根据服务名称来访问
- 添加注解,实现负载均衡
//服务名称为userService
String url = "http://userservice/user/" + order.getUserId();
@Bean
@LoadBalanced //负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
3. Ribbon负载均衡
流程
http://userservice/user并不是一个浏览器能够访问的真实地址,因此需要一个组件去进行解析,也就是Ribbon组件,当请求进入时,去注册中心去找到对应的服务名称,然后再分配到对应的服务商
通过源码可知,最后有IRule接口决定负载均衡策略
总结:
- Ribbon负载均衡规则
- 规则接口是IRule
- 默认实现是ZoneAvoidanceRule,根据zone选择服务列表,然后轮训
- 规则可言进行改变
- 负载均衡自定义方式
- 代码方式:配置灵活,但修改时需要重新打包发布
- 配置方式: 资管,方便,无序==无需重新打包发布,但是无法做全局配置
- 饥饿加载
- 开启饥饿加载
- 指定饥饿加载的微服务名称
day-03
1. NACOS注册中心
1.1 NACOS的下载和安装
gitHub地址:https://github.com/alibaba/nacos/tags
选择对应的安装包,以1.4.1为例
下载好后,解压到一个没有中文的路径下
解压好的nacos结构如下:
1.2 NACOS配置
三个文件夹中的内容:
- bin
- 启动和关闭的命令
- conf
- peoperties配置文件,可以配置端口,默认为8848
- target
- NACOS的可执行jar包
1.4 NACOS启动
在文件目录下输入cmd命令进入控制台
startup.cmd -m standalone
- 其中m代表的是模式 当前启动命令表示单机启动
启动成功:
可以通过显示的路径访问网页,用户名、密码都是nacos
2. NACOS使用
2.1 服务注册到NACOSS
- 引入父工程的管理依赖
<!-- NACOS管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
- 在user-service模块中引入nacos依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 修改yml文件
- nacos的配置属于spring配置,需要把之前eurka的配置注释掉
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
application:
name: userservice #user服务的服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos服务地址 默认就是8848
#eureka:
# client:
# service-url: # eureka的地址信息
# defaultZone: http://127.0.0.1:12345/eureka
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
- 启动成功,可以在nacos的服务列表中找到注册的服务
3. NACOS服务分级存储模型
- 服务跨集群调用问题
- 服务调用尽可能的选择本地集群的服务
- 跨集群调用延迟较高
- 本地集群不可访问时,再去访问其他集群
- 服务调用尽可能的选择本地集群的服务
在yml文件中进行配置
#在spring下面添加
discovery:
cluster-name: HZ #集群名称
具体配置如下
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
application:
name: userservice #user服务的服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos服务地址
discovery:
cluster-name: HZ #集群名称
然后进行启动,选中哪两个服务,启动后就变成哪个集群下
- 当前先启动两个为杭州集群 HZ,后修改yaml文件,在启动另外一个集群为上海集群
4. 权重配置
可以在nacos控制台对不同的服务之间,配置权重,权重越大,访问到的概率就会越高
5. 环境隔离
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
创建方式:
-
控制台创建命名空间
-
修改yml文件
-
注意只能使用命名空间的ID
注意:
- namespace用来做环境隔离
- 每个namespace都有唯一的id
- 不同的namespace下的服务不可见
- 也就是说,不同的命名空间下不能进行访问
6. Nacos和Eureka的区别
day-04
1. http客户端Feign
1.1 Feign替代RestTemplate
RestTemplate方式调用存在的问题
发起远程调用的代码
String url = " http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url,User,class)
- 存在两个问题
- 代码可读性差
- 多参数情况下很难去维护
Feign是一个声明式的http客户端,官方地址:https:github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现实现http请求的发送,解决上面提到的问题
使用Feign的步骤如下:·
-
引入依赖
-
<!-- 添加一个feign的客户端--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
-
在order-service的启动类中添加注解开启Feign的功能
-
@MapperScan("cn.itcast.order.mapper") @SpringBootApplication @EnableFeignClients public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } @Bean @LoadBalanced //负载均衡 public RestTemplate restTemplate() { return new RestTemplate(); } }
-
-
编写Feign客户端
- 主要是基于SpringMVC的注解来声明远程调用的信息,比如:
- 服务名称:userservice
- 请求方式:GET
- 请求路径:/user/{id}
- 请求参数:Long id
- 返回值类型:User
- 主要是基于SpringMVC的注解来声明远程调用的信息,比如:
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
1.2 自定义配置
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
一般我们需要配置的是日志级别
1.3 Feign使用优化
- Feign底层的客户端实现:
- URLConnection:默认实现,不支持连接池
- Apache HttpClient:支持连接池
- OKHttp:支持连接池
- 因此优化Feign的性能主要包括:
- 使用连接池代替默认的URLConnection
- 日志级别,最好用basic或none
1.4 最佳实践
1.4.1 方式一(继承)
-
给消费者的FeignClient和提供者的controller定义统一的父接口作为标准
- 发送请求和接收请求的方法必须要一致才能够正常请求
- 因此可以定义一个统一的接口来处理
-
但是spring不推荐这种方案,并且springMVC不支持这种方式
1.4.2 方式二(抽取)
将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
-
实现步骤如下:
-
首先创建一个module,命名为feign-api,然后引入feign的starter依赖
-
将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
-
在order-service中引入feign-api的依赖
-
<!-- 引入feign的api--> <dependency> <groupId>cn.itcast.demo</groupId> <artifactId>feign-api</artifactId> <version>1.0</version> </dependency>
-
-
修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包
-
-
但是单独抽取一个模块后,会导致UserClient无法注入,有两种方式解决
-
方法一:指定FeignClient所在包
-
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
-
-
方法二:指定FeignClient字节码
-
@EnableFeignClients(clients = {UserClient.class})
-
-
2. 统一网关Gateway
- 网关功能:
- 身份认证和权限校验
- 服务路由、负载均衡
- 请求限流
- 技术实现
- gateway(近几个版本才出现)
- zuul(一直存在)
- Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具有更好的性能
2.1 搭建网关
-
搭建网关服务的步骤:
-
创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖
-
<!-- 网关信息--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- nacos服务发现依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
-
编写路由配置及nacos地址
-
server: port: 10010 #网关端口 spring: application: name: gateway #服务名称 cloud: nacos: server-addr: localhost:8848 #nacos地址 gateway: routes: #网关路由配置 - id: user-service #路由id,自定义,保证唯一即可 uri: lb://userservice #路由的目标地址 lb就是负载均衡的缩写 后面跟服务名称 predicates: #路由断言,也就是判断请求是否符合路由规则的条件 - Path=/user/** #按照路径匹配
-
注意事项
- 路由id:必须是唯一的,它是路由的唯一标识
- 路由目标(url):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
-
-
编写启动类
2.2 路由断言工程Route Predicate Factory
网关路由可以配置的内容包括:
- 路由id:路由唯一标识
- uri:路由目的地,支持lb和http两种
- predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地
- 我们之前在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
- filters:路由过滤器,处理请求或相应
Spring提供了11种基本的Predicate工厂:
2.3 全局过滤器
- 普通过滤器
- 配置后只对当前的模块生效
- 默认过滤器
- 配置后对所有的模块生效
但是这两种过滤器都存在一个问题,都是由配置文件来完成的,如果想做逻辑处理,就需要使用到全局过滤器来实现
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
/**
* 采用的并不是像以前封装servlet的过滤器,并没有servlet的api方法
*
* */
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
// 2. 获取参数中的authorization参数
String auth = queryParams.getFirst("authorization");
// 3. 判断参数值
if ("admin".equals(auth)) {
// 3.1 满足 放行
return chain.filter(exchange);
}
// 3.2 不满足 拦截
// 3.2.1 设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 3.2.2 拦截请求
return exchange.getResponse().setComplete();
}
}
-
order是用来指定执行顺序的,数值越小,优先级越高
-
也可以通过实现接口来指定优先级
-
@Component public class AuthorizeFilter implements GlobalFilter, Ordered { /** * 采用的并不是像以前封装servlet的过滤器,并没有servlet的api方法 * * */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1. 获取请求参数 ServerHttpRequest request = exchange.getRequest(); MultiValueMap<String, String> queryParams = request.getQueryParams(); // 2. 获取参数中的authorization参数 String auth = queryParams.getFirst("authorization"); // 3. 判断参数值 if ("admin".equals(auth)) { // 3.1 满足 放行 return chain.filter(exchange); } // 3.2 不满足 拦截 // 3.2.1 设置状态码 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // 3.2.2 拦截请求 return exchange.getResponse().setComplete(); } @Override public int getOrder() { return -1; } }
-
-
-
并且需要把过滤器注册为bean交给spring管理
2.4 过滤器链执行顺序
请求进入网关后会碰到三类过滤器:当前的路由过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
- 每一个过滤器都必须制定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
- GlobalFilter通过Ordered接口,或者添加@Order注解来指定order值,可以自定义
- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
- 当过滤器的order值一样时,会按照defaultFilter 》 路由过滤器 》 GlobalFilter的顺序执行
2.5 网关的cors跨域配置
跨域:域名不一致就是跨域,主要包括:
- 域名不同:www.taobao.com和www.taobao.org和www.jd.com和miaosha.jd.com
- 域名相同,端口不同:localhost:8080和localhost:8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被拦截器拦截的问题
解决方案:CORS