一、认识微服务
服务架构演变
1.1单体架构
单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
1.2分布式结构
分布式结构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务
1.3微服务
微服务是一种经过良好架构设计的分布式架构方案,微服务特征:
- 单一职责:微服务拆分粒度更小,每个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
- 面向服务:微服务对外暴露业务接口
- 自治:团队独立、技术独立、数据独立、部署独立
- 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
1.4微服务结构
1.5微服务技术对比
SpringCloud
二、服务拆分及远程调用
2.1服务拆分
服务拆分注意事项
1.不同微服务,不要开发重复相同业务
2.微服务数据独立,不要访问其他微服务的数据库
3.微服务可以将自己的业务暴露为接口,供其它微服务调用
2.2服务间调用
案例:根据订单id查询订单功能
如上:按需求需要订单模块调用用户模块
原本订单模块的用户信息是Null ,模块相互之间又是隔离的,所以订单模块不能直接调用用户模块,如果订单模块能像客户端一样发起一个对用户模块的请求访问不就得到用户的信息了吗,再和订单模块的结果合并,就是一个完整的订单信息,那又怎样在订单模块发起对用户模块的请求呢?
如下:
2)服务远程调用RestTemplate
修改order-service中的OrderService中的queryOrderById方法:
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2.获取用户id
Long userId = order.getUserId();
//3.利用RestTemplate发送http请求查询用户
//3.1url路径
String url = "http://localhost:8081/user/" + userId;
//3.2发送http:请求,实现远程调用
User user = restTemplate.getForObject(url, User.class);
//4.封装user到Order
order.setUser(user);
//5.返回
return order;
}
}
提供者与消费者
- 服务提供者:一次业务中,被其他微服务调用的服务。(提供接口给其他微服务)
- 服务消费者:一次业务中,调用其他微服务的服务。(调用其它微服务提供的接口)
提供者与消费者角色是相对的
三、Eureka注册中心
3.1远程调用的问题
环境的不同访问地址也会不同
集群部署多个时,硬编码只能访问一个,其他无法访问
3.2Eureka的作用
3.3eureka原理
- 消费者该如何获取服务提供者具体信息?
- 服务提供者启动时向eureka注册自己的信息
- eureka保存这些信息
- 消费者根据服务名称向eureka拉取提供者信息
- 如果有多个服务提供者,消费者该如何选择?
- 服务消费者利用负载均衡算法,从服务列表中挑选一个
- 消费者如何感知服务提供者健康状态?
- 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
- eureka会更新记录服务列表信息,心跳不正常会被剔除
- 消费者就可以拉取到最新的信息
在Eureka架构中,微服务角色有两类:
- EurekaServer:服务端,注册中心
- 记录服务信息
- 心跳监控
- EurekaClient::客户端
- Provider:服务提供者,例如案例中的user-service
- 注册自己的信息到EurekaServier
- 每隔30秒向EurekaServier发送心跳
- consumer:服务消费者,例如案例中的order-service
- 根据服务名称从EurekaServer拉取服务列表
- 基于服务列表做负载均衡,选取一个微服务后发起远程调用
- Provider:服务提供者,例如案例中的user-service
3.4搭建EurekaServer
1.创建项目,引依赖 2.在启动类上加注解 3.配置文件里配置信息
<!--eureka服务端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
@EnableEurekaServer
@SpringBootApplication
public class EurkaApplication {
}
server:
port: 10086 #服务端口
#以下为了做服务注册
spring:
application:
name: eurekaserver
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
eureka也是微服务,所以也会将自己注册上
3.5服务注册
注册user-service步骤如下:
1.在user-service项目引入spring-cloud-starter-nerflix-erueka-client依赖
<!--eureka依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.在application.yml文件,编写下面的配置:
eureka和spring同级
application:
name: userservice
eureka:
client:
service-url:
dafaultZone: http://localhost:10086/eureka/
服务注册步骤
引入eureka-client依赖
在applicaion.ym中配置eureka地址
3.6服务发现
在oredier-service完成服务拉取
服务拉取是基于服务名称获取服务列表,然后再对服务列表做负载均衡
1.修改OrderService代码,修改访问的url路径,用服务名代替ip、端口:
String url = "http://userservice/user/" + userId;
2.在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解:
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
总结:
1.搭建EurekaServer
- 引入eureka-server依赖
- 在启动类上 添加@EnableEurekaServer注解
- 在application.yml中配置eureka地址
2.服务注册
- 引入eureka-client依赖‘
- 在application中配置eureka地址
3.服务发现
- 引入eureka-client依赖‘
- 在application中配置eureka地址
- 给RestTemplate添加@LoadBalanced主键
- 用服务提供者的服务名称远程调用
四、Ribbon负载均衡
4.1负载均衡原理
4.2负载均衡策略
Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则:
通过定义IRule实现可以修改负载均衡规则,有两种方式:
1.代码方式:在order-service中的OrderApplication类中,定义一个新的IRule:
@Bean
public IRule randomRule(){
return new RandomRule();
}
2.配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:
userserve:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
方式一:针对全局微服务,order访问任何服务器都是
方式二:只针对order访问的某个微服务
4.3饥饿加载
懒加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
五、Nacos注册中心
5.1Nacos快速入门
服务注册到Nacos
1.在cloud-demo父工程中添加spring-cloud-alibaba的管理依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
1.在父工程中添加spring-cloud-alibaba的管理依赖
<!--nacos管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
2.注释order-service和user-service中的eureka依赖以及yml中的配置
3.添加nacos客户端依赖:
<!--nacos客服端依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
4.修改user和order-service中的application.yml,注释eureka地址,添加nacos地址:
cloue和application同级
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
1.Nacon服务搭建
下载安装包、解压、在bin目录下运行:startup.cmd -m standalone
2.Nacos服务注册或者发现
引入nacos.discovery依赖、配置nacos地址spring.cloud.nacos.server-addr
5.2Nacos服务分级存储模型
5.3服务跨集群调用问题
服务调用尽可能选择本地集群的服务,跨集群调用延迟较高
本地集群不可访问时,再访问其他集群
5.4服务集群属性
1.修改application.yml,添加如下内容:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: SH
2.在Nacos控制台可以看到集群变化
1.Naocs服务分级存储模型
- 一级是服务,例如userservice
- 二级是集群,例如杭州或者上海
- 三级是实例,例如杭州机房的某台部署了userservcie的服务器
2.如何设置实例的集群属性
.修改application.yml文件,添加spring.cloud.nacos.discovery.cluster-name属性即可
5.5Nacos负载均衡(根据集群)
NacosRule负载均衡策略
- 优先选择同集群服务实例列表
- 本地集群找不到提供者,才去其他集群寻找,并且会报警告
- 确定了可用实例列表后,再采用随机负载均衡挑选实例
根据权重负载均衡
5.6Nacos环境隔离
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
1.在Nacos控制台可以创建namespace,用来隔离不同环境
2.然后填写一个新的命名空间信息:
3.保存后在控制台看到这个命名空间的id
4.修改order-service的application.yml,添加namespace:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/heima?useSSL=false
username: root
password: 1234
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ
# namespace: 8eae1788-ee70-45b6-b5ad-4500d3d5f902 # 环境
1.Nacos环境隔离
- namespace用来做环境隔离
- 每个namespace都有唯一id
- 不同namespace下的服务不可见
临时实例和非临时实例
服注册到Nacos时,可以选择注册为临时或非临时实例,通过下面的配置来设置:
Nacos与eureka的共同点
- 都支持服务注册和服务拉取
- 都支持服务提供者心跳方式做健康检测
2.Nacos与Eureka的区别
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当集群中存在非临时实例,采用CP模式;Eureka采用AP方式
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ
ephemeral: false # 是否为临时实例
5.7Nacos配置管理
统一配置管理
f
添加配置
1.引入Nacos的配置管理客户端依赖:
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
2.在userservice中的resource目录添加一个bootstrap.yml文件,这个文件时引导文件,优先级高于application.yml
spring:
application:
name: userserver
profiles:
active: public # 环境
cloud:
nacos:
server-addr: localhost:8848 # nacos 地址
config:
file-extension: yaml # 文件后缀名
配置热更新
Nacos中的配置文件变更后,微服务无需重启就可以感知。不过需要下面两种配置实现:
- 方式一:在@Value注入的变量所在类上添加注解@RefreshScope
@RefreshScope
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
- 方式二:使用@ConfigurationProperties注解
-
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "pattern") public class PatternProperties { private String dateformat; }
配置共享
5.8搭建Nacos集群
六、Feign远程调用
RestTemplate方式调用存在的问题
存在问题:
- 代码可读性差,编程体验布不统一
- 参数复杂URL难以维护
第一步导入依赖
<!--Fegin客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二步在启动类上添加注解@EnableFeignClients
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
}
第三步接口声明
package cn.itcast.order.clients;
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("userserver")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
第四步调用
@Autowired
private UserClient userClient;
User user = userClient.findById(userId);
自定义Fegin的配置
Fegin运行自定义配置来覆盖默认配置,可以修改的配置如下:
一般需要配置的就是日志级别.
修改Fegin日志有两种方式:
方式一:配置文件方式
方式二:java代码方式,需要先声明一个Bean:
Feign的性能优化
Feign底层的客户端实现:
- URLConnection:默认实现,不支持连接池
- Apache HttpClient:支持连接池
- OKHttp:支持连接池
因此优化Feign的性能主要包括:
① 使用连接池代替默认的URLConnection
② 日志级别,最好用basic或none
Feign的性能优化-连接池配置
Feign添加HttpClient的支持:
引依赖
<!--引入HttpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
配置连接池
feign:
httpclient:
enabled: true # 支持httpclient的开关
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个路径最大连接数
Feign的最佳实践
方式一(继承):给消费者的FeignClient和提供者的controller定义统一的父接口作为标准
方式二(抽取):将FeigenClient抽取为独立模块,并且把接口有关的POJO、默认的Fegin配置都放到这个模块中,提供给所有消费者使用
抽取FeignClient
实现最佳实践方式二的步骤如下:
1.首先创建一个module,命名为feign-api,然后引入feign的starter依赖
2.将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
3.在order-service中引入feign-api的依赖
4.修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api的包
5.重启测试
当定义的FeignClient不在SpringBootApplication扫描包范围时,这些FeignClient无法使用。有两种方式解决:
方式一:指定FeignClient所在包(批量)
方式二:指定FeignClien字节码(指定client)
@EnableFeignClients(clients = UserClient.class)
七、Gateway服务网关
7.1为什么需要网关
7.2gateway快速入门
搭建网关服务
1.创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖:
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
2.编写路由配置及nacos地址
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userserver # 路由的目标地址
predicates: #路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是,则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
7.3断言工厂
网关路由可以配置的内容包括:
- 路由id:路由唯一标示
- rui:路由目的地,支持lb和http两种
- predicates:路由断言,判断请求是否符合要求,符号则转发到路由目的地
- filters:路由过滤器,处理请求或响应
7.4过滤工厂
过滤器:针对某个服务
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userserver # 路由的目标地址
predicates: #路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是,则符合
filters:
- AddRequestHeader=Truth,Itcast is freaking aowsome!
默认过滤器:对所有微服务都生效
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userserver # 路由的目标地址
predicates: #路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是,则符合
# filters:
# - AddRequestHeader=Truth,Itcast is freaking aowsome!
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters:
- AddRequestHeader=Truth,Itcast is freaking aowsome!
过滤器的作用是什么?
- 对路由的请求或响应做加工处理,比如添加请求头
- 配置在路由下的过滤器只对当前路由的请求生效
defaultFilters的作用是什么?
- 对所有路由都生效的过滤器
7.5全局过滤器
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFile的作用一样.
区别于GatewayFile通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现。
定义方式是实现GlobalFilter接口
案例
步骤一:自定义过滤器
自定义类,实现GlobalFilter接口,添加@Order注解:
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
//2.获取参数的authorization参数
String authorization = params.getFirst("authorization");
//3.判断参数是否等于admi
if("admin".equals(authorization)){
//4.是,放行
return chain.filter(exchange);
}
//5.否,拦截
//5.1设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
7.6过滤器执行顺序
7.7跨域问题
跨域:域名不一致就是跨域,主要包括:
- 域名不同:www.taobao.com和www.taobao.org和www.jd.com和miaosha.jd.com
- 域名相同,端口不同:localhost:8080和localhost:8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS
网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现:
八、Docker
1.初识Docker
Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?
- Docker允许开发中将应用、依赖、函数库、配置一起打包,形成可移植镜像
- Docker应用允许在容器中,使用沙箱机制,相互隔离
Docker如何解决开发、测试、生产环境有差异的问题
- Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行
镜像和容器
镜像(Image): Docker将应用程序及其所需要的依赖、函数库、环境、配置等文件打包在一起,称为镜像.
容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。
Docker和DockerHub
- DockerHub:DockerHub是一个Docker镜像的托管平台。这样的平台称为Docker Registry
- 国内也有类似DockerHub的公开服务,比如网易云镜像服务、阿里云镜像库等。
docker架构
Docker是一个CS架构的程序,由两部分组成:
- 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
- 客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。
2.Docker的基本操作
镜像操作
- 镜像名称一般分两部分组成:[repository]:[tag]
- 在没有指定tag时,默认是latest,代表最新版本的镜像
拉取镜像
docker pull nginx
查看镜像
docker images
案例利用docker save 将nginx镜像导出磁盘,然后再通过load加载回款
docker save --help
docker save -o nginx.tar nginx:latest
docker rmi nginx:latest
docker load -i nginx.tar
练习
docker pull redis
docker images
docker save -o redis.tar redis:latest
docker rmi redis:latest
docker load -i redis.tar
容器操作
案例
步骤一:去docker hub查看Nginx的容器运行命令
docker run --name mn -p 80:80 -d nginx
数据卷(容器数据管理)
容器与数据耦合问题
数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录。
操作数据卷
案例:创建一个数据卷,并查看数据卷在宿主机的目录位置
1.创建数据卷
docker volume create html
2.查看所有数据
docker volume ls
3.查看数据卷详细信息卷
[root@VM-8-6-centos ~]# docker volume inspect html
[
{
"CreatedAt": "2023-10-23T15:14:30+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/html/_data",
"Name": "html",
"Options": null,
"Scope": "local"
}
]
挂载数据卷
在创建容器是,可以通过-v参数来挂载一个数据卷到某个容器目录
docker run --name mn -p 80:80 -v html:/var/lib/docker/volumes/html/_data -d nginx
docker inspect html
cd /var/lib/docker/volumes/html/_data
vi index.html
docker run \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123 \
-p 3306:3306 \
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/mysql \
-d \
mysql:5.7.25
3.Dockerfile自定义镜像
3.1镜像结构
3.2Dockerfile语法
Dockerfile就是一个文本文件,其中包含一个个的指令,用指令来说明要执行什么操作来构建镜像,每个指令都会形成一层Layer.
3.3构建Java项目
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local
# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
docker build -t javaweb:1.0 .
4.Docker-Compose
初始DockerCompose
- Docker Compose可以基于Compose文件 帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!
- Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行
部署微服务集群
5.Docker镜像服务
5.1搭私有镜像仓库
简化版镜像仓库
docker run -d \
--restart=always \
--name registry \
-p 5000:5000 \
-v registry-data:/var/lib/registry \
registry
图形化界面
创建docker compose文件docker-compose.yml
version: '3.0'
services:
registry:
image: registry
volumes:
- ./registry-data:/var/lib/registry
ui:
image: joxit/docker-registry-ui:static
ports:
- 8080:80
environment:
- REGISTRY_TITLE=传智教育私有仓库
- REGISTRY_URL=http://registry:5000
depends_on:
- registry
docker-compose up -d
配置Docker信任地址
我们的私服采用的是http协议,默认不被Docker信任,所以需要做一个配置:
# 打开要修改的文件
vi /etc/docker/daemon.json
# 添加内容:
"insecure-registries":["http://192.168.150.101:8080"]
# 重加载
systemctl daemon-reload
# 重启docker
systemctl restart docker