服务架构演变
单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
分布式结构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务
微服务是一种经过良好架构设计的分布式架构方案,微服务特征:
SpringCloud
服务拆分注意事项
1.不同微服务,不要开发重复相同业务
2.微服务数据独立,不要访问其他微服务的数据库
3.微服务可以将自己的业务暴露为接口,供其它微服务调用
案例:根据订单id查询订单功能
如上:按需求需要订单模块调用用户模块
订单模块的用户信息是Null ,模块相互之间又是隔离的,所以订单模块不能直接调用用户模块,如果订单模块能象客户端一样发起一个对用户模块的请求访问不就得到用户的信息了吗,再和订单模块的结果合并,就是一个完整的订单信息,那又怎样在订单模块发起对用户模块的请求呢?
如下:
修改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;
}
}
提供者与消费者
提供者与消费者角色是相对的
环境的不同访问地址也会不同
集群部署多个时,硬编码只能访问一个,其他无法访问
1.引依赖 2.在启动类上加注解 3.配置文件里配置信息
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
@EnableEurekaServer
@SpringBootApplication
public class EurkaApplication {
}
server:
port: 10086 #服务端口
#以下为了做服务注册
spring:
application:
name: eurekaserver
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
eureka也是微服务,所以也会将自己注册上
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访问的某个微服务
懒加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
1.在父工程中添加spring-cloud-alibaba的管理依赖
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.2.5.RELEASE
2.注释order-service和user-service中的eureka依赖
3.添加nacos客户端依赖:
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
2.2.5.RELEASE
4.修改user和order-service中的application.yml,注释eureka地址,添加nacos地址:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
服务调用尽可能选择本地集群的服务,跨集群调用延迟较高
本地集群不可访问时,再访问其他集群
1.修改application.yml,添加如下内容:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: SH
2.在Nacos控制台可以看到集群变化
根据权重负载均衡
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
1.在Nacos控制台可以创建namespace,用来隔离不同环境
2.然后填写一个新的命名空间信息:
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 # 环境
服注册到Nacos时,可以选择注册为临时或非临时实例,通过下面的配置来设置:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ
ephemeral: false # 是否为临时实例
统一配置管理
1.引入Nacos的配置管理客户端依赖:
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
2.2.5.RELEASE
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中的配置文件变更后,微服务无需重启就可以感知。不过需要下面两种配置实现:
@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));
}
}
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;
}
配置共享
RestTemplate方式调用存在的问题
存在问题:
第一步导入依赖
org.springframework.cloud
spring-cloud-starter-openfeign
第二步在启动类上添加注解@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日志有两种方式:
方式一:配置文件方式
方式二:java代码方式,需要先声明一个Bean:
Feign底层的客户端实现:
因此优化Feign的性能主要包括:
① 使用连接池代替默认的URLConnection
② 日志级别,最好用basic或none
Feign的性能优化-连接池配置
Feign添加HttpClient的支持:
引依赖
io.github.openfeign
feign-httpclient
配置连接池
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)
搭建网关服务
1.创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖:
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
2.2.5.RELEASE
org.springframework.cloud
spring-cloud-starter-gateway
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/**
网关路由可以配置的内容包括:
过滤器:针对某个服务
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!
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFile的作用一样.
区别于GatewayFile通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现。
定义方式是实现GlobalFilter接口
案例
步骤一:自定义过滤器
自定义类,实现GlobalFilter接口,添加@Order注解:
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap 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();
}
}
跨域:域名不一致就是跨域,主要包括:
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS
网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现:
Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?
Docker如何解决开发、测试、生产环境有差异的问题
镜像(Image): Docker将应用程序及其所需要的依赖、函数库、环境、配置等文件打包在一起,称为镜像.
容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。
Docker和DockerHub
Docker是一个CS架构的程序,由两部分组成:
拉取镜像
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
Dockerfile就是一个文本文件,其中包含一个个的指令,用指令来说明要执行什么操作来构建镜像,每个指令都会形成一层Layer.
# 指定基础镜像
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 .
初始DockerCompose
简化版镜像仓库
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